Skip to content

Commit 05ea027

Browse files
committed
Add test case and update changelog
1 parent 22b0a0b commit 05ea027

File tree

2 files changed

+90
-4
lines changed

2 files changed

+90
-4
lines changed

.changes/next-release/bugfix-AmazonS3-263fed5.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"type": "bugfix",
33
"category": "Amazon S3",
44
"contributor": "",
5-
"description": "Fix bug in MultipartS3AsyncClient GET where retryable errors may not retried, and if retried, successful responses are incorrectly processed with the initial error."
5+
"description": "Fix bug in MultipartS3AsyncClient GET where retryable errors may not be retried, and if retried, successful responses are incorrectly processed with the initial error."
66
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/multipart/S3MultipartClientGetObjectWiremockTest.java

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929
import static org.junit.jupiter.api.Assertions.assertNotEquals;
3030
import static org.junit.jupiter.api.Assertions.assertNotNull;
3131
import static org.junit.jupiter.api.Assertions.assertThrows;
32+
import static org.junit.jupiter.api.Assertions.assertTrue;
3233

3334
import com.github.tomakehurst.wiremock.http.Fault;
3435
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
3536
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
3637
import com.github.tomakehurst.wiremock.stubbing.Scenario;
3738
import java.net.URI;
39+
import java.nio.ByteBuffer;
3840
import java.nio.charset.StandardCharsets;
3941
import java.time.Duration;
4042
import java.util.ArrayList;
@@ -43,13 +45,17 @@
4345
import java.util.UUID;
4446
import java.util.concurrent.CompletableFuture;
4547
import java.util.concurrent.CompletionException;
48+
import java.util.concurrent.atomic.AtomicBoolean;
4649
import org.junit.jupiter.api.BeforeEach;
4750
import org.junit.jupiter.api.Test;
51+
import org.reactivestreams.Subscriber;
52+
import org.reactivestreams.Subscription;
4853
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
4954
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
5055
import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
5156
import software.amazon.awssdk.core.ResponseBytes;
5257
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
58+
import software.amazon.awssdk.core.async.SdkPublisher;
5359
import software.amazon.awssdk.core.interceptor.Context;
5460
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
5561
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
@@ -163,9 +169,8 @@ public void getObject_503Response_shouldNotReuseInitialRequestId() {
163169
.withHeader("x-amz-request-id", secondRequestId)
164170
.withStatus(503)));
165171

166-
assertThrows(CompletionException.class, () -> {
167-
multipartClient.getObject(b -> b.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join();
168-
});
172+
assertThrows(CompletionException.class, () ->
173+
multipartClient.getObject(b -> b.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join());
169174

170175
List<SdkHttpResponse> responses = capturingInterceptor.getResponses();
171176
assertEquals(MAX_ATTEMPTS, responses.size(), () -> String.format("Expected exactly %s responses", MAX_ATTEMPTS));
@@ -343,6 +348,87 @@ public void getObject_iOError_shouldRetrySuccessfully() {
343348
assertEquals(requestId, finalRequestId);
344349
}
345350

351+
@Test
352+
public void multipartDownload_errorDuringFirstPartAfterOnStream_shouldFailAndNotRetry() {
353+
stubFor(get(urlEqualTo(String.format("/%s/%s?partNumber=1", BUCKET, KEY)))
354+
.willReturn(aResponse()
355+
.withHeader("x-amz-mp-parts-count", String.valueOf(2))
356+
.withStatus(200)
357+
.withBody("Hello ")));
358+
359+
stubFor(get(urlEqualTo(String.format("/%s/%s?partNumber=2", BUCKET, KEY)))
360+
.willReturn(aResponse()
361+
.withStatus(200)
362+
.withHeader("x-amz-mp-parts-count", "2")
363+
.withBody("World")));
364+
365+
StreamingErrorTransformer failingTransformer = new StreamingErrorTransformer();
366+
assertThrows(CompletionException.class, () ->
367+
multipartClient.getObject(b -> b.bucket(BUCKET).key(KEY), failingTransformer).join());
368+
369+
assertTrue(failingTransformer.onStreamCalled.get());
370+
// Verify that the first part was requested only once and not retried
371+
verify(1, getRequestedFor(urlEqualTo(String.format("/%s/%s?partNumber=1", BUCKET, KEY))));
372+
}
373+
374+
/**
375+
* Custom AsyncResponseTransformer that simulates an error occurring after onStream() has been called
376+
*/
377+
private static final class StreamingErrorTransformer
378+
implements AsyncResponseTransformer<GetObjectResponse, ResponseBytes<GetObjectResponse>> {
379+
380+
private final CompletableFuture<ResponseBytes<GetObjectResponse>> future = new CompletableFuture<>();
381+
private final AtomicBoolean errorThrown = new AtomicBoolean();
382+
private final AtomicBoolean onStreamCalled = new AtomicBoolean();
383+
384+
@Override
385+
public CompletableFuture<ResponseBytes<GetObjectResponse>> prepare() {
386+
return future;
387+
}
388+
389+
@Override
390+
public void onResponse(GetObjectResponse response) {
391+
//
392+
}
393+
394+
@Override
395+
public void onStream(SdkPublisher<ByteBuffer> publisher) {
396+
onStreamCalled.set(true);
397+
publisher.subscribe(new Subscriber<ByteBuffer>() {
398+
private Subscription subscription;
399+
400+
@Override
401+
public void onSubscribe(Subscription s) {
402+
this.subscription = s;
403+
s.request(1);
404+
}
405+
406+
@Override
407+
public void onNext(ByteBuffer byteBuffer) {
408+
if (errorThrown.compareAndSet(false, true)) {
409+
future.completeExceptionally(new RuntimeException());
410+
subscription.cancel();
411+
}
412+
}
413+
414+
@Override
415+
public void onError(Throwable t) {
416+
future.completeExceptionally(t);
417+
}
418+
419+
@Override
420+
public void onComplete() {
421+
//
422+
}
423+
});
424+
}
425+
426+
@Override
427+
public void exceptionOccurred(Throwable throwable) {
428+
future.completeExceptionally(throwable);
429+
}
430+
}
431+
346432
private CompletableFuture<ResponseBytes<GetObjectResponse>> mock200Response(S3AsyncClient s3Client, int runNumber) {
347433
String runId = runNumber + " success";
348434

0 commit comments

Comments
 (0)