Skip to content

Commit e041ca2

Browse files
authored
Fix for threads hanging with forwardTransformedResultTo (#5117)
* Fix forwardTransformedResultTo util method to catch any exception thrown by the transforming method. * catch Throwable
1 parent 330ab9f commit e041ca2

File tree

3 files changed

+49
-6
lines changed

3 files changed

+49
-6
lines changed

services-custom/s3-transfer-manager/src/test/java/software/amazon/awssdk/transfer/s3/internal/S3TransferManagerTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525

2626
import java.nio.file.Paths;
2727
import java.util.concurrent.CompletableFuture;
28+
import java.util.concurrent.TimeUnit;
2829
import org.junit.jupiter.api.AfterEach;
2930
import org.junit.jupiter.api.BeforeEach;
3031
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.Timeout;
3133
import software.amazon.awssdk.core.ResponseBytes;
3234
import software.amazon.awssdk.core.async.AsyncRequestBody;
3335
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
@@ -217,6 +219,25 @@ void download_cancel_shouldForwardCancellation() {
217219
assertThat(s3CrtFuture).isCancelled();
218220
}
219221

222+
@Test
223+
@Timeout(value = 5, unit = TimeUnit.SECONDS)
224+
void download_futureReturnsNull_doesNotHang() {
225+
AsyncResponseTransformer<GetObjectResponse, Void> mockTr = mock(AsyncResponseTransformer.class);
226+
CompletableFuture<Void> returnMockFuture = new CompletableFuture<>();
227+
when(mockS3Crt.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class)))
228+
.thenReturn(returnMockFuture);
229+
DownloadRequest<Void> downloadRequest =
230+
DownloadRequest.builder()
231+
.getObjectRequest(g -> g.bucket("bucket").key("key"))
232+
.responseTransformer(mockTr).build();
233+
234+
CompletableFuture<CompletedDownload<Void>> future = tm.download(downloadRequest).completionFuture();
235+
returnMockFuture.complete(null);
236+
assertThatThrownBy(future::join)
237+
.hasCauseInstanceOf(NullPointerException.class)
238+
.hasMessageContaining("result must not be null");
239+
}
240+
220241
@Test
221242
void objectLambdaArnBucketProvided_shouldThrowException() {
222243
String objectLambdaArn = "arn:xxx:s3-object-lambda";

utils/src/main/java/software/amazon/awssdk/utils/CompletableFutureUtils.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static CompletionException errorAsCompletionException(Throwable t) {
6767

6868
/**
6969
* Forward the {@code Throwable} from {@code src} to {@code dst}.
70-
70+
*
7171
* @param src The source of the {@code Throwable}.
7272
* @param dst The destination where the {@code Throwable} will be forwarded to.
7373
*
@@ -148,8 +148,9 @@ public static <T> CompletableFuture<T> forwardResultTo(CompletableFuture<T> src,
148148
}
149149

150150
/**
151-
* Completes the {@code dst} future based on the result of the {@code src} future, synchronously,
152-
* after applying the provided transformation {@link Function} if successful.
151+
* Completes the {@code dst} future based on the result of the {@code src} future, synchronously, after applying the provided
152+
* transformation {@link Function} if successful. If the function threw an exception, the destination
153+
* future will be completed exceptionally with that exception.
153154
*
154155
* @param src The source {@link CompletableFuture}
155156
* @param dst The destination where the {@code Throwable} or transformed result will be forwarded to.
@@ -161,9 +162,16 @@ public static <SourceT, DestT> CompletableFuture<SourceT> forwardTransformedResu
161162
src.whenComplete((r, e) -> {
162163
if (e != null) {
163164
dst.completeExceptionally(e);
164-
} else {
165-
dst.complete(function.apply(r));
165+
return;
166+
}
167+
DestT result = null;
168+
try {
169+
result = function.apply(r);
170+
} catch (Throwable functionException) {
171+
dst.completeExceptionally(functionException);
172+
return;
166173
}
174+
dst.complete(result);
167175
});
168176

169177
return src;

utils/src/test/java/software/amazon/awssdk/utils/CompletableFutureUtilsTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ public void forwardTransformedResultTo_srcCompletesExceptionally_shouldCompleteD
108108
assertThatThrownBy(dst::join).hasCause(exception);
109109
}
110110

111+
@Test(timeout = 1000)
112+
public void forwardTransformedResultTo_functionThrowsException_shouldCompleteExceptionally() {
113+
CompletableFuture<Integer> src = new CompletableFuture<>();
114+
CompletableFuture<String> dst = new CompletableFuture<>();
115+
116+
CompletableFutureUtils.forwardTransformedResultTo(src, dst, x -> { throw new RuntimeException("foobar"); });
117+
src.complete(0);
118+
assertThatThrownBy(dst::join)
119+
.hasMessageContaining("foobar")
120+
.hasCauseInstanceOf(RuntimeException.class);
121+
}
122+
111123
@Test(timeout = 1000)
112124
public void anyFail_shouldCompleteWhenAnyFutureFails() {
113125
RuntimeException exception = new RuntimeException("blah");
@@ -206,4 +218,6 @@ public void joinLikeSync_canceled_throwsCancellationException() {
206218
.hasNoSuppressedExceptions()
207219
.hasNoCause()
208220
.isInstanceOf(CancellationException.class);
209-
}}
221+
}
222+
223+
}

0 commit comments

Comments
 (0)