Skip to content

Commit 7f8803f

Browse files
committed
Address comments pt 1
1 parent 54457df commit 7f8803f

File tree

5 files changed

+42
-39
lines changed

5 files changed

+42
-39
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 MultipartS3AsyncClient GetObject retryable errors"
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."
66
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/SplittingTransformer.java

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.nio.ByteBuffer;
1919
import java.util.concurrent.CompletableFuture;
2020
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.concurrent.atomic.AtomicInteger;
2122
import java.util.concurrent.atomic.AtomicLong;
2223
import org.reactivestreams.Subscriber;
2324
import org.reactivestreams.Subscription;
@@ -57,7 +58,7 @@ public class SplittingTransformer<ResponseT, ResultT> implements SdkPublisher<As
5758
/**
5859
* Set to true once {@code .onStream()} is called on the upstreamResponseTransformer
5960
*/
60-
private final AtomicBoolean onStreamCalled = new AtomicBoolean(false);
61+
private boolean onStreamCalled;
6162

6263
/**
6364
* Set to true once {@code .cancel()} is called in the subscription of the downstream subscriber, or if the
@@ -108,16 +109,9 @@ public class SplittingTransformer<ResponseT, ResultT> implements SdkPublisher<As
108109
private volatile CompletableFuture<ResultT> upstreamFuture;
109110

110111
/**
111-
* Tracks whether an {@code IndividualTransformer} has been created for the first part yet. Errors will only be retried for
112-
* the first part.
112+
* Tracks the part number. Errors will only be retried for the first part.
113113
*/
114-
private final AtomicBoolean isFirstIndividualTransformer = new AtomicBoolean(true);
115-
116-
/**
117-
* Tracks whether an {@code IndividualPartSubscriber} has been created for the first part yet. Errors will only be retried for
118-
* the first part.
119-
*/
120-
private final AtomicBoolean isFirstIndividualSubscriber = new AtomicBoolean(true);
114+
private final AtomicInteger partNumber = new AtomicInteger(0);
121115

122116
private SplittingTransformer(AsyncResponseTransformer<ResponseT, ResultT> upstreamResponseTransformer,
123117
Long maximumBufferSizeInBytes,
@@ -206,7 +200,7 @@ private boolean doEmit() {
206200
}
207201
if (outstandingDemand.get() > 0) {
208202
demand = outstandingDemand.decrementAndGet();
209-
downstreamSubscriber.onNext(new IndividualTransformer(isFirstIndividualTransformer.compareAndSet(true, false)));
203+
downstreamSubscriber.onNext(new IndividualTransformer(partNumber.incrementAndGet()));
210204
}
211205
}
212206
return false;
@@ -224,7 +218,7 @@ private void handleSubscriptionCancel() {
224218
log.trace(() -> "downstreamSubscriber already null, skipping downstreamSubscriber.onComplete()");
225219
return;
226220
}
227-
if (!onStreamCalled.get()) {
221+
if (!onStreamCalled) {
228222
// we never subscribe publisherToUpstream to the upstream, it would not complete
229223
downstreamSubscriber = null;
230224
return;
@@ -268,19 +262,19 @@ private void handleFutureCancel(Throwable e) {
268262
* body publisher.
269263
*/
270264
private class IndividualTransformer implements AsyncResponseTransformer<ResponseT, ResponseT> {
271-
private final boolean isFirstPart;
265+
private final int partNumber;
272266
private ResponseT response;
273267
private CompletableFuture<ResponseT> individualFuture;
274268

275-
IndividualTransformer(boolean isFirstPart) {
276-
this.isFirstPart = isFirstPart;
269+
IndividualTransformer(int partNumber) {
270+
this.partNumber = partNumber;
277271
}
278272

279273
@Override
280274
public CompletableFuture<ResponseT> prepare() {
281275
this.individualFuture = new CompletableFuture<>();
282276

283-
if (isFirstPart) {
277+
if (partNumber == 1) {
284278
if (isCancelled.get()) {
285279
return individualFuture;
286280
}
@@ -299,7 +293,7 @@ public CompletableFuture<ResponseT> prepare() {
299293

300294
@Override
301295
public void onResponse(ResponseT response) {
302-
if (isFirstPart) {
296+
if (partNumber == 1) {
303297
log.trace(() -> "calling onResponse on the upstream transformer");
304298
upstreamResponseTransformer.onResponse(response);
305299
}
@@ -312,8 +306,8 @@ public void onStream(SdkPublisher<ByteBuffer> publisher) {
312306
return;
313307
}
314308
synchronized (cancelLock) {
315-
if (isFirstPart) {
316-
onStreamCalled.set(true);
309+
if (partNumber == 1) {
310+
onStreamCalled = true;
317311
log.trace(() -> "calling onStream on the upstream transformer");
318312
upstreamResponseTransformer.onStream(upstreamSubscriber -> publisherToUpstream.subscribe(
319313
DelegatingBufferingSubscriber.builder()
@@ -324,22 +318,25 @@ public void onStream(SdkPublisher<ByteBuffer> publisher) {
324318
}
325319
}
326320

327-
if (!resultFuture.isDone()) {
328-
CompletableFutureUtils.forwardResultTo(upstreamFuture, resultFuture);
329-
}
330-
331-
publisher.subscribe(new IndividualPartSubscriber<>(this.individualFuture, response,
332-
isFirstIndividualSubscriber.compareAndSet(true, false)));
321+
CompletableFutureUtils.forwardResultTo(upstreamFuture, resultFuture);
322+
publisher.subscribe(new IndividualPartSubscriber<>(this.individualFuture, response, partNumber));
333323
}
334324

335325
@Override
336326
public void exceptionOccurred(Throwable error) {
337327
log.trace(() -> "calling exceptionOccurred on the upstream transformer");
338-
upstreamResponseTransformer.exceptionOccurred(error);
339328

340-
if (!isFirstPart || onStreamCalled.get()) {
341-
publisherToUpstream.error(error);
329+
if (partNumber == 1) {
330+
upstreamResponseTransformer.exceptionOccurred(error);
342331
}
332+
333+
// TODO - add comments explaining
334+
synchronized (cancelLock) {
335+
if (partNumber > 1 || onStreamCalled) {
336+
publisherToUpstream.error(error);
337+
}
338+
}
339+
343340
}
344341
}
345342

@@ -350,13 +347,13 @@ class IndividualPartSubscriber<T> implements Subscriber<ByteBuffer> {
350347

351348
private final CompletableFuture<T> future;
352349
private final T response;
353-
private final boolean isFirstPart;
350+
private final int partNumber;
354351
private Subscription subscription;
355352

356-
IndividualPartSubscriber(CompletableFuture<T> future, T response, boolean isFirstPart) {
353+
IndividualPartSubscriber(CompletableFuture<T> future, T response, int partNumber) {
357354
this.future = future;
358355
this.response = response;
359-
this.isFirstPart = isFirstPart;
356+
this.partNumber = partNumber;
360357
}
361358

362359
@Override
@@ -396,7 +393,7 @@ public void onComplete() {
396393
}
397394

398395
private void handleError(Throwable t) {
399-
if (!isFirstPart) {
396+
if (partNumber > 1) {
400397
publisherToUpstream.error(t);
401398
}
402399
future.completeExceptionally(t);

core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/IndividualPartSubscriberTckTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public Subscriber<ByteBuffer> createSubscriber(WhiteboxSubscriberProbe<ByteBuffe
4444
.maximumBufferSizeInBytes(32L)
4545
.resultFuture(new CompletableFuture<>())
4646
.build();
47-
return transformer.new IndividualPartSubscriber<ByteBuffer>(future, ByteBuffer.wrap(new byte[0]), true) {
47+
return transformer.new IndividualPartSubscriber<ByteBuffer>(future, ByteBuffer.wrap(new byte[0]), 1) {
4848
@Override
4949
public void onSubscribe(Subscription s) {
5050
super.onSubscribe(s);

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/multipart/MultipartDownloaderSubscriber.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
package software.amazon.awssdk.services.s3.internal.multipart;
1717

18-
import java.util.ArrayList;
19-
import java.util.List;
18+
import java.util.Queue;
2019
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.ConcurrentLinkedQueue;
2121
import java.util.concurrent.atomic.AtomicInteger;
2222
import org.reactivestreams.Subscriber;
2323
import org.reactivestreams.Subscription;
@@ -81,7 +81,7 @@ public class MultipartDownloaderSubscriber implements Subscriber<AsyncResponseTr
8181
*/
8282
private final Object lock = new Object();
8383

84-
private final List<CompletableFuture<GetObjectResponse>> getObjectFutures = new ArrayList<>();
84+
private final Queue<CompletableFuture<GetObjectResponse>> getObjectFutures = new ConcurrentLinkedQueue<>();
8585

8686
public MultipartDownloaderSubscriber(S3AsyncClient s3, GetObjectRequest getObjectRequest) {
8787
this(s3, getObjectRequest, 0);
@@ -125,6 +125,7 @@ public void onNext(AsyncResponseTransformer<GetObjectResponse, GetObjectResponse
125125
CompletableFuture<GetObjectResponse> getObjectFuture = s3.getObject(actualRequest, asyncResponseTransformer);
126126
getObjectFutures.add(getObjectFuture);
127127
getObjectFuture.whenComplete((response, error) -> {
128+
getObjectFutures.remove(getObjectFuture);
128129
if (error != null) {
129130
log.debug(() -> "Error encountered during GetObjectRequest with partNumber=" + nextPartToGet);
130131
onError(error);
@@ -171,7 +172,10 @@ private void requestMoreIfNeeded(GetObjectResponse response) {
171172

172173
@Override
173174
public void onError(Throwable t) {
174-
getObjectFutures.forEach(future -> future.cancel(true));
175+
CompletableFuture<GetObjectResponse> partFuture;
176+
while ((partFuture = getObjectFutures.poll()) != null) {
177+
partFuture.cancel(true);
178+
}
175179
future.completeExceptionally(t);
176180
}
177181

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public void setup(WireMockRuntimeInfo wm) {
9292
.build();
9393
}
9494

95+
// TODO - 1) update test names 2) add test for I/O error
96+
9597
@Test
9698
public void stub_200_only() {
9799
List<CompletableFuture<ResponseBytes<GetObjectResponse>>> futures = new ArrayList<>();
@@ -305,7 +307,7 @@ public void multipleParts_503OnFirstPart_then_200s() {
305307
}
306308

307309
private CompletableFuture<ResponseBytes<GetObjectResponse>> mock200Response(S3AsyncClient s3Client, int runNumber) {
308-
String runId = runNumber + " sucess";
310+
String runId = runNumber + " success";
309311

310312
stubFor(any(anyUrl())
311313
.withHeader("RunNum", matching(runId))

0 commit comments

Comments
 (0)