Skip to content

Commit c2ff0c9

Browse files
committed
Add BlobContainer.writeBlobAtomic()
This commit adds a new writeBlobAtomic() method to the BlobContainer interface that can be implemented by repository implementations which support atomic writes operations. When the repository does not support atomic writes, this method just delegate the write operation to the usual writeBlob() method. Related to elastic#30680
1 parent c7c0acc commit c2ff0c9

File tree

9 files changed

+113
-108
lines changed

9 files changed

+113
-108
lines changed

server/src/main/java/org/elasticsearch/common/blobstore/BlobContainer.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,28 @@ public interface BlobContainer {
7474
*/
7575
void writeBlob(String blobName, InputStream inputStream, long blobSize) throws IOException;
7676

77+
/**
78+
* Reads blob content from the input stream and writes it to the container in a new blob with the given name,
79+
* using an atomic write operation if then implementation supports it. When atomic writes are not supported,
80+
* this method delegates to {@link #writeBlob(String, InputStream, long)}.
81+
*
82+
* This method assumes the container does not already contain a blob of the same blobName. If a blob by the
83+
* same name already exists, the operation will fail and an {@link IOException} will be thrown.
84+
*
85+
* @param blobName
86+
* The name of the blob to write the contents of the input stream to.
87+
* @param inputStream
88+
* The input stream from which to retrieve the bytes to write to the blob.
89+
* @param blobSize
90+
* The size of the blob to be written, in bytes. It is implementation dependent whether
91+
* this value is used in writing the blob to the repository.
92+
* @throws FileAlreadyExistsException if a blob by the same name already exists
93+
* @throws IOException if the input stream could not be read, or the target blob could not be written to.
94+
*/
95+
default void writeBlobAtomic(final String blobName, final InputStream inputStream, final long blobSize) throws IOException {
96+
writeBlob(blobName, inputStream, blobSize);
97+
}
98+
7799
/**
78100
* Deletes a blob with giving name, if the blob exists. If the blob does not exist,
79101
* this method throws a NoSuchFileException.

server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919

2020
package org.elasticsearch.common.blobstore.fs;
2121

22-
import org.elasticsearch.core.internal.io.IOUtils;
22+
import org.elasticsearch.common.UUIDs;
2323
import org.elasticsearch.common.blobstore.BlobMetaData;
2424
import org.elasticsearch.common.blobstore.BlobPath;
2525
import org.elasticsearch.common.blobstore.support.AbstractBlobContainer;
2626
import org.elasticsearch.common.blobstore.support.PlainBlobMetaData;
27+
import org.elasticsearch.core.internal.io.IOUtils;
2728
import org.elasticsearch.core.internal.io.Streams;
2829

2930
import java.io.BufferedInputStream;
@@ -131,6 +132,28 @@ public void writeBlob(String blobName, InputStream inputStream, long blobSize) t
131132
IOUtils.fsync(path, true);
132133
}
133134

135+
@Override
136+
public void writeBlobAtomic(final String blobName, final InputStream inputStream, final long blobSize) throws IOException {
137+
final Path tempBlobPath = path.resolve("pending-" + blobName + "-" + UUIDs.randomBase64UUID());
138+
try {
139+
try (OutputStream outputStream = Files.newOutputStream(tempBlobPath, StandardOpenOption.CREATE_NEW)) {
140+
Streams.copy(inputStream, outputStream);
141+
}
142+
IOUtils.fsync(tempBlobPath, false);
143+
144+
final Path blobPath = path.resolve(blobName);
145+
// If the target file exists then Files.move() behaviour is implementation specific
146+
// the existing file might be replaced or this method fails by throwing an IOException.
147+
if (Files.exists(blobPath)) {
148+
throw new FileAlreadyExistsException("blob [" + blobPath + "] already exists, cannot overwrite");
149+
}
150+
Files.move(tempBlobPath, blobPath, StandardCopyOption.ATOMIC_MOVE);
151+
} finally {
152+
IOUtils.deleteFilesIgnoringExceptions(tempBlobPath);
153+
IOUtils.fsync(path, true);
154+
}
155+
}
156+
134157
@Override
135158
public void move(String source, String target) throws IOException {
136159
Path sourcePath = path.resolve(source);

server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -774,18 +774,8 @@ private long listBlobsToGetLatestIndexId() throws IOException {
774774
}
775775

776776
private void writeAtomic(final String blobName, final BytesReference bytesRef) throws IOException {
777-
final String tempBlobName = "pending-" + blobName + "-" + UUIDs.randomBase64UUID();
778777
try (InputStream stream = bytesRef.streamInput()) {
779-
snapshotsBlobContainer.writeBlob(tempBlobName, stream, bytesRef.length());
780-
snapshotsBlobContainer.move(tempBlobName, blobName);
781-
} catch (IOException ex) {
782-
// temporary blob creation or move failed - try cleaning up
783-
try {
784-
snapshotsBlobContainer.deleteBlobIgnoringIfNotExists(tempBlobName);
785-
} catch (IOException e) {
786-
ex.addSuppressed(e);
787-
}
788-
throw ex;
778+
snapshotsBlobContainer.writeBlobAtomic(blobName, stream, bytesRef.length());
789779
}
790780
}
791781

server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.lucene.index.IndexFormatTooNewException;
2424
import org.apache.lucene.index.IndexFormatTooOldException;
2525
import org.apache.lucene.store.OutputStreamIndexOutput;
26+
import org.elasticsearch.common.CheckedConsumer;
2627
import org.elasticsearch.common.CheckedFunction;
2728
import org.elasticsearch.common.blobstore.BlobContainer;
2829
import org.elasticsearch.common.bytes.BytesArray;
@@ -120,7 +121,7 @@ public T readBlob(BlobContainer blobContainer, String blobName) throws IOExcepti
120121
}
121122

122123
/**
123-
* Writes blob in atomic manner with resolving the blob name using {@link #blobName} and {@link #tempBlobName} methods.
124+
* Writes blob in atomic manner with resolving the blob name using {@link #blobName} method.
124125
* <p>
125126
* The blob will be compressed and checksum will be written if required.
126127
*
@@ -131,20 +132,12 @@ public T readBlob(BlobContainer blobContainer, String blobName) throws IOExcepti
131132
* @param name blob name
132133
*/
133134
public void writeAtomic(T obj, BlobContainer blobContainer, String name) throws IOException {
134-
String blobName = blobName(name);
135-
String tempBlobName = tempBlobName(name);
136-
writeBlob(obj, blobContainer, tempBlobName);
137-
try {
138-
blobContainer.move(tempBlobName, blobName);
139-
} catch (IOException ex) {
140-
// Move failed - try cleaning up
141-
try {
142-
blobContainer.deleteBlob(tempBlobName);
143-
} catch (Exception e) {
144-
ex.addSuppressed(e);
135+
final String blobName = blobName(name);
136+
writeTo(obj, blobName, bytesArray -> {
137+
try (InputStream stream = bytesArray.streamInput()) {
138+
blobContainer.writeBlobAtomic(blobName, stream, bytesArray.length());
145139
}
146-
throw ex;
147-
}
140+
});
148141
}
149142

150143
/**
@@ -157,39 +150,32 @@ public void writeAtomic(T obj, BlobContainer blobContainer, String name) throws
157150
* @param name blob name
158151
*/
159152
public void write(T obj, BlobContainer blobContainer, String name) throws IOException {
160-
String blobName = blobName(name);
161-
writeBlob(obj, blobContainer, blobName);
153+
final String blobName = blobName(name);
154+
writeTo(obj, blobName, bytesArray -> {
155+
try (InputStream stream = bytesArray.streamInput()) {
156+
blobContainer.writeBlob(blobName, stream, bytesArray.length());
157+
}
158+
});
162159
}
163160

164-
/**
165-
* Writes blob in atomic manner without resolving the blobName using using {@link #blobName} method.
166-
* <p>
167-
* The blob will be compressed and checksum will be written if required.
168-
*
169-
* @param obj object to be serialized
170-
* @param blobContainer blob container
171-
* @param blobName blob name
172-
*/
173-
protected void writeBlob(T obj, BlobContainer blobContainer, String blobName) throws IOException {
174-
BytesReference bytes = write(obj);
175-
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
161+
private void writeTo(final T obj, final String blobName, final CheckedConsumer<BytesArray, IOException> consumer) throws IOException {
162+
final BytesReference bytes = write(obj);
163+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
176164
final String resourceDesc = "ChecksumBlobStoreFormat.writeBlob(blob=\"" + blobName + "\")";
177-
try (OutputStreamIndexOutput indexOutput = new OutputStreamIndexOutput(resourceDesc, blobName, byteArrayOutputStream, BUFFER_SIZE)) {
165+
try (OutputStreamIndexOutput indexOutput = new OutputStreamIndexOutput(resourceDesc, blobName, outputStream, BUFFER_SIZE)) {
178166
CodecUtil.writeHeader(indexOutput, codec, VERSION);
179167
try (OutputStream indexOutputOutputStream = new IndexOutputOutputStream(indexOutput) {
180168
@Override
181169
public void close() throws IOException {
182170
// this is important since some of the XContentBuilders write bytes on close.
183171
// in order to write the footer we need to prevent closing the actual index input.
184-
} }) {
172+
}
173+
}) {
185174
bytes.writeTo(indexOutputOutputStream);
186175
}
187176
CodecUtil.writeFooter(indexOutput);
188177
}
189-
BytesArray bytesArray = new BytesArray(byteArrayOutputStream.toByteArray());
190-
try (InputStream stream = bytesArray.streamInput()) {
191-
blobContainer.writeBlob(blobName, stream, bytesArray.length());
192-
}
178+
consumer.accept(new BytesArray(outputStream.toByteArray()));
193179
}
194180
}
195181

@@ -222,10 +208,4 @@ protected void write(T obj, StreamOutput streamOutput) throws IOException {
222208
builder.endObject();
223209
}
224210
}
225-
226-
227-
protected String tempBlobName(String name) {
228-
return TEMP_FILE_PREFIX + String.format(Locale.ROOT, blobNameFormat, name);
229-
}
230-
231211
}

server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public void waitForBlock(String node, String repository, TimeValue timeout) thro
9494
}
9595
Thread.sleep(100);
9696
}
97-
fail("Timeout!!!");
97+
fail("Timeout waiting for node [" + node + "] to be blocked");
9898
}
9999

100100
public SnapshotInfo waitForCompletion(String repository, String snapshotName, TimeValue timeout) throws InterruptedException {

server/src/test/java/org/elasticsearch/snapshots/BlobStoreFormatIT.java

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -224,52 +224,16 @@ public void testAtomicWriteFailures() throws Exception {
224224
IOException writeBlobException = expectThrows(IOException.class, () -> {
225225
BlobContainer wrapper = new BlobContainerWrapper(blobContainer) {
226226
@Override
227-
public void writeBlob(String blobName, InputStream inputStream, long blobSize) throws IOException {
228-
throw new IOException("Exception thrown in writeBlob() for " + blobName);
227+
public void writeBlobAtomic(String blobName, InputStream inputStream, long blobSize) throws IOException {
228+
throw new IOException("Exception thrown in writeBlobAtomic() for " + blobName);
229229
}
230230
};
231231
checksumFormat.writeAtomic(blobObj, wrapper, name);
232232
});
233233

234-
assertEquals("Exception thrown in writeBlob() for pending-" + name, writeBlobException.getMessage());
234+
assertEquals("Exception thrown in writeBlobAtomic() for " + name, writeBlobException.getMessage());
235235
assertEquals(0, writeBlobException.getSuppressed().length);
236236
}
237-
{
238-
IOException moveException = expectThrows(IOException.class, () -> {
239-
BlobContainer wrapper = new BlobContainerWrapper(blobContainer) {
240-
@Override
241-
public void move(String sourceBlobName, String targetBlobName) throws IOException {
242-
throw new IOException("Exception thrown in move() for " + sourceBlobName);
243-
}
244-
};
245-
checksumFormat.writeAtomic(blobObj, wrapper, name);
246-
});
247-
assertEquals("Exception thrown in move() for pending-" + name, moveException.getMessage());
248-
assertEquals(0, moveException.getSuppressed().length);
249-
}
250-
{
251-
IOException moveThenDeleteException = expectThrows(IOException.class, () -> {
252-
BlobContainer wrapper = new BlobContainerWrapper(blobContainer) {
253-
@Override
254-
public void move(String sourceBlobName, String targetBlobName) throws IOException {
255-
throw new IOException("Exception thrown in move() for " + sourceBlobName);
256-
}
257-
258-
@Override
259-
public void deleteBlob(String blobName) throws IOException {
260-
throw new IOException("Exception thrown in deleteBlob() for " + blobName);
261-
}
262-
};
263-
checksumFormat.writeAtomic(blobObj, wrapper, name);
264-
});
265-
266-
assertEquals("Exception thrown in move() for pending-" + name, moveThenDeleteException.getMessage());
267-
assertEquals(1, moveThenDeleteException.getSuppressed().length);
268-
269-
final Throwable suppressedThrowable = moveThenDeleteException.getSuppressed()[0];
270-
assertTrue(suppressedThrowable instanceof IOException);
271-
assertEquals("Exception thrown in deleteBlob() for pending-" + name, suppressedThrowable.getMessage());
272-
}
273237
}
274238

275239
protected BlobStore createTestBlobStore() throws IOException {

server/src/test/java/org/elasticsearch/snapshots/mockstore/BlobContainerWrapper.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,21 @@ public void writeBlob(String blobName, InputStream inputStream, long blobSize) t
5353
delegate.writeBlob(blobName, inputStream, blobSize);
5454
}
5555

56+
@Override
57+
public void writeBlobAtomic(final String blobName, final InputStream inputStream, final long blobSize) throws IOException {
58+
delegate.writeBlobAtomic(blobName, inputStream, blobSize);
59+
}
60+
5661
@Override
5762
public void deleteBlob(String blobName) throws IOException {
5863
delegate.deleteBlob(blobName);
5964
}
6065

66+
@Override
67+
public void deleteBlobIgnoringIfNotExists(final String blobName) throws IOException {
68+
delegate.deleteBlobIgnoringIfNotExists(blobName);
69+
}
70+
6171
@Override
6272
public Map<String, BlobMetaData> listBlobs() throws IOException {
6373
return delegate.listBlobs();

server/src/test/java/org/elasticsearch/snapshots/mockstore/MockRepository.java

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,6 @@
1919

2020
package org.elasticsearch.snapshots.mockstore;
2121

22-
import java.io.IOException;
23-
import java.io.InputStream;
24-
import java.io.UnsupportedEncodingException;
25-
import java.nio.file.Path;
26-
import java.security.MessageDigest;
27-
import java.security.NoSuchAlgorithmException;
28-
import java.util.Arrays;
29-
import java.util.Collections;
30-
import java.util.List;
31-
import java.util.Map;
32-
import java.util.concurrent.ConcurrentHashMap;
33-
import java.util.concurrent.ConcurrentMap;
34-
import java.util.concurrent.atomic.AtomicLong;
35-
3622
import com.carrotsearch.randomizedtesting.RandomizedContext;
3723
import org.apache.lucene.index.CorruptIndexException;
3824
import org.elasticsearch.ElasticsearchException;
@@ -49,11 +35,25 @@
4935
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
5036
import org.elasticsearch.env.Environment;
5137
import org.elasticsearch.plugins.RepositoryPlugin;
52-
import org.elasticsearch.repositories.Repository;
5338
import org.elasticsearch.repositories.IndexId;
39+
import org.elasticsearch.repositories.Repository;
5440
import org.elasticsearch.repositories.fs.FsRepository;
5541
import org.elasticsearch.snapshots.SnapshotId;
5642

43+
import java.io.IOException;
44+
import java.io.InputStream;
45+
import java.io.UnsupportedEncodingException;
46+
import java.nio.file.Path;
47+
import java.security.MessageDigest;
48+
import java.security.NoSuchAlgorithmException;
49+
import java.util.Arrays;
50+
import java.util.Collections;
51+
import java.util.List;
52+
import java.util.Map;
53+
import java.util.concurrent.ConcurrentHashMap;
54+
import java.util.concurrent.ConcurrentMap;
55+
import java.util.concurrent.atomic.AtomicLong;
56+
5757
public class MockRepository extends FsRepository {
5858

5959
public static class Plugin extends org.elasticsearch.plugins.Plugin implements RepositoryPlugin {
@@ -325,6 +325,12 @@ public void deleteBlob(String blobName) throws IOException {
325325
super.deleteBlob(blobName);
326326
}
327327

328+
@Override
329+
public void deleteBlobIgnoringIfNotExists(String blobName) throws IOException {
330+
maybeIOExceptionOrBlock(blobName);
331+
super.deleteBlobIgnoringIfNotExists(blobName);
332+
}
333+
328334
@Override
329335
public Map<String, BlobMetaData> listBlobs() throws IOException {
330336
maybeIOExceptionOrBlock("");
@@ -365,6 +371,12 @@ public void writeBlob(String blobName, InputStream inputStream, long blobSize) t
365371
maybeIOExceptionOrBlock(blobName);
366372
}
367373
}
374+
375+
@Override
376+
public void writeBlobAtomic(final String blobName, final InputStream inputStream, final long blobSize) throws IOException {
377+
maybeIOExceptionOrBlock(blobName);
378+
super.writeBlobAtomic(blobName, inputStream, blobSize);
379+
}
368380
}
369381
}
370382
}

test/framework/src/main/java/org/elasticsearch/repositories/ESBlobStoreContainerTestCase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ public void testVerifyOverwriteFails() throws IOException {
158158

159159
protected void writeBlob(final BlobContainer container, final String blobName, final BytesArray bytesArray) throws IOException {
160160
try (InputStream stream = bytesArray.streamInput()) {
161-
container.writeBlob(blobName, stream, bytesArray.length());
161+
if (randomBoolean()) {
162+
container.writeBlob(blobName, stream, bytesArray.length());
163+
} else {
164+
container.writeBlobAtomic(blobName, stream, bytesArray.length());
165+
}
162166
}
163167
}
164168

0 commit comments

Comments
 (0)