Skip to content

Commit 9f53186

Browse files
authored
Fix the StackOverflowException in WaiterExecutor in case of large retries count. (#3956)
1 parent 94fbbc7 commit 9f53186

File tree

3 files changed

+83
-30
lines changed

3 files changed

+83
-30
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "flittev",
5+
"description": "`WaiterExecutor` recursive implementation changed to iterative"
6+
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/WaiterExecutor.java

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@
1616
package software.amazon.awssdk.core.internal.waiters;
1717

1818
import java.util.List;
19-
import java.util.Optional;
2019
import java.util.function.Supplier;
2120
import software.amazon.awssdk.annotations.SdkInternalApi;
2221
import software.amazon.awssdk.annotations.ThreadSafe;
2322
import software.amazon.awssdk.core.exception.SdkClientException;
2423
import software.amazon.awssdk.core.waiters.WaiterAcceptor;
2524
import software.amazon.awssdk.core.waiters.WaiterResponse;
26-
import software.amazon.awssdk.core.waiters.WaiterState;
2725
import software.amazon.awssdk.utils.Either;
2826
import software.amazon.awssdk.utils.Validate;
2927

@@ -45,45 +43,42 @@ public WaiterExecutor(WaiterConfiguration configuration,
4543
}
4644

4745
WaiterResponse<T> execute(Supplier<T> pollingFunction) {
48-
return doExecute(pollingFunction, 0, System.currentTimeMillis());
49-
}
50-
51-
WaiterResponse<T> doExecute(Supplier<T> pollingFunction, int attemptNumber, long startTime) {
52-
attemptNumber++;
53-
T response;
54-
try {
55-
response = pollingFunction.get();
56-
} catch (Exception exception) {
57-
return evaluate(pollingFunction, Either.right(exception), attemptNumber, startTime);
58-
}
59-
60-
return evaluate(pollingFunction, Either.left(response), attemptNumber, startTime);
61-
}
46+
int attemptNumber = 0;
47+
long startTime = System.currentTimeMillis();
6248

63-
private WaiterResponse<T> evaluate(Supplier<T> pollingFunction,
64-
Either<T, Throwable> responseOrException,
65-
int attemptNumber,
66-
long startTime) {
67-
Optional<WaiterAcceptor<? super T>> waiterAcceptor = executorHelper.firstWaiterAcceptorIfMatched(responseOrException);
49+
while (true) {
50+
attemptNumber++;
6851

69-
if (waiterAcceptor.isPresent()) {
70-
WaiterState state = waiterAcceptor.get().waiterState();
71-
switch (state) {
52+
Either<T, Throwable> polledResponse = pollResponse(pollingFunction);
53+
WaiterAcceptor<? super T> waiterAcceptor = firstWaiterAcceptor(polledResponse);
54+
switch (waiterAcceptor.waiterState()) {
7255
case SUCCESS:
73-
return executorHelper.createWaiterResponse(responseOrException, attemptNumber);
56+
return executorHelper.createWaiterResponse(polledResponse, attemptNumber);
7457
case RETRY:
75-
return maybeRetry(pollingFunction, attemptNumber, startTime);
58+
waitToRetry(attemptNumber, startTime);
59+
break;
7660
case FAILURE:
77-
throw executorHelper.waiterFailureException(waiterAcceptor.get());
61+
throw executorHelper.waiterFailureException(waiterAcceptor);
7862
default:
7963
throw new UnsupportedOperationException();
8064
}
8165
}
66+
}
67+
68+
private Either<T, Throwable> pollResponse(Supplier<T> pollingFunction) {
69+
try {
70+
return Either.left(pollingFunction.get());
71+
} catch (Exception exception) {
72+
return Either.right(exception);
73+
}
74+
}
8275

83-
throw executorHelper.noneMatchException(responseOrException);
76+
private WaiterAcceptor<? super T> firstWaiterAcceptor(Either<T, Throwable> responseOrException) {
77+
return executorHelper.firstWaiterAcceptorIfMatched(responseOrException)
78+
.orElseThrow(() -> executorHelper.noneMatchException(responseOrException));
8479
}
8580

86-
private WaiterResponse<T> maybeRetry(Supplier<T> pollingFunction, int attemptNumber, long startTime) {
81+
private void waitToRetry(int attemptNumber, long startTime) {
8782
Either<Long, SdkClientException> nextDelayOrUnretryableException =
8883
executorHelper.nextDelayOrUnretryableException(attemptNumber, startTime);
8984

@@ -97,6 +92,5 @@ private WaiterResponse<T> maybeRetry(Supplier<T> pollingFunction, int attemptNum
9792
Thread.currentThread().interrupt();
9893
throw SdkClientException.create("The thread got interrupted", e);
9994
}
100-
return doExecute(pollingFunction, attemptNumber, startTime);
10195
}
10296
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.internal.waiters;
17+
18+
import java.util.Arrays;
19+
import java.util.concurrent.atomic.LongAdder;
20+
import org.junit.jupiter.api.Test;
21+
import org.testng.Assert;
22+
import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
23+
import software.amazon.awssdk.core.waiters.WaiterAcceptor;
24+
import software.amazon.awssdk.core.waiters.WaiterOverrideConfiguration;
25+
26+
class WaiterExecutorTest {
27+
@Test
28+
void largeMaxAttempts() {
29+
30+
int expectedAttempts = 10_000;
31+
32+
WaiterOverrideConfiguration conf =
33+
WaiterOverrideConfiguration.builder()
34+
.maxAttempts(expectedAttempts)
35+
.backoffStrategy(BackoffStrategy.none())
36+
.build();
37+
38+
WaiterExecutor<Integer> sut =
39+
new WaiterExecutor<>(new WaiterConfiguration(conf),
40+
Arrays.asList(
41+
WaiterAcceptor.retryOnResponseAcceptor(c -> c < expectedAttempts),
42+
WaiterAcceptor.successOnResponseAcceptor(c -> c == expectedAttempts)
43+
));
44+
45+
LongAdder attemptCounter = new LongAdder();
46+
sut.execute(() -> {
47+
attemptCounter.increment();
48+
return attemptCounter.intValue();
49+
});
50+
51+
Assert.assertEquals(attemptCounter.intValue(), expectedAttempts);
52+
}
53+
}

0 commit comments

Comments
 (0)