Skip to content

Commit e6eb8ef

Browse files
authored
[Test] Add test for AWS SDKv1 swallowing exception at IndexputStream close time (#123505)
This change adds a unit test to demonstrate a specific behavior of the AWS SDKv1, which closes the InputStream used to upload a blob only after the HTTP request has been sent (this is to accomodate for retries). The SDK then swallows any exception thrown when closing the InputStream which has the effect to hide any potential CorruptIndexException that could have been detected at that time. Relates ES-10931
1 parent a9e27a9 commit e6eb8ef

File tree

1 file changed

+54
-0
lines changed

1 file changed

+54
-0
lines changed

modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobContainerRetriesTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import com.sun.net.httpserver.HttpHandler;
2222

2323
import org.apache.http.HttpStatus;
24+
import org.apache.lucene.index.CorruptIndexException;
25+
import org.apache.lucene.store.AlreadyClosedException;
2426
import org.elasticsearch.ExceptionsHelper;
2527
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
2628
import org.elasticsearch.common.BackoffPolicy;
@@ -78,6 +80,7 @@
7880
import java.util.concurrent.atomic.AtomicBoolean;
7981
import java.util.concurrent.atomic.AtomicInteger;
8082
import java.util.concurrent.atomic.AtomicLong;
83+
import java.util.concurrent.atomic.AtomicReference;
8184
import java.util.function.IntConsumer;
8285

8386
import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomNonDataPurpose;
@@ -332,6 +335,57 @@ public void testWriteBlobWithReadTimeouts() {
332335
assertThat(exception.getCause().getCause().getMessage().toLowerCase(Locale.ROOT), containsString("read timed out"));
333336
}
334337

338+
/**
339+
* This test shows that the AWS SDKv1 defers the closing of the InputStream used to upload a blob after the HTTP request has been sent
340+
* to S3, swallowing any exception thrown at closing time.
341+
*/
342+
public void testWriteBlobWithExceptionThrownAtClosingTime() throws Exception {
343+
var maxRetries = randomInt(3);
344+
var blobLength = randomIntBetween(1, 4096 * 3);
345+
var blobName = getTestName().toLowerCase(Locale.ROOT);
346+
var blobContainer = createBlobContainer(maxRetries, null, true, null);
347+
348+
var uploadedBytes = new AtomicReference<BytesReference>();
349+
httpServer.createContext(downloadStorageEndpoint(blobContainer, blobName), exchange -> {
350+
var requestComponents = S3HttpHandler.parseRequestComponents(S3HttpHandler.getRawRequestString(exchange));
351+
if ("PUT".equals(requestComponents.method()) && requestComponents.query().isEmpty()) {
352+
var body = Streams.readFully(exchange.getRequestBody());
353+
if (uploadedBytes.compareAndSet(null, body)) {
354+
exchange.sendResponseHeaders(HttpStatus.SC_OK, -1);
355+
exchange.close();
356+
return;
357+
}
358+
}
359+
exchange.sendResponseHeaders(HttpStatus.SC_BAD_REQUEST, -1);
360+
exchange.close();
361+
});
362+
363+
final byte[] bytes = randomByteArrayOfLength(blobLength);
364+
365+
var exceptionThrown = new AtomicBoolean();
366+
blobContainer.writeBlobAtomic(randomPurpose(), blobName, new FilterInputStream(new ByteArrayInputStream(bytes)) {
367+
@Override
368+
public void close() throws IOException {
369+
if (exceptionThrown.compareAndSet(false, true)) {
370+
switch (randomInt(3)) {
371+
case 0:
372+
throw new CorruptIndexException("simulated", blobName);
373+
case 1:
374+
throw new AlreadyClosedException("simulated");
375+
case 2:
376+
throw new RuntimeException("simulated");
377+
case 3:
378+
default:
379+
throw new IOException("simulated");
380+
}
381+
}
382+
}
383+
}, blobLength, true);
384+
385+
assertThat(exceptionThrown.get(), is(true));
386+
assertArrayEquals(bytes, BytesReference.toBytes(uploadedBytes.get()));
387+
}
388+
335389
public void testWriteLargeBlob() throws Exception {
336390
final boolean useTimeout = rarely();
337391
final TimeValue readTimeout = useTimeout ? TimeValue.timeValueMillis(randomIntBetween(100, 500)) : null;

0 commit comments

Comments
 (0)