Skip to content

Commit 67b7844

Browse files
authored
Add ADAPTIVE_V2 retry mode to support the legacy behavior (#5123)
* Add a new ADAPTIVE2 mode to support the legacy behavior * Fix dynamodb test to use adaptive2 mode * Fixes and tests for the expected behaviors * Rename the new adaptive mode to ADAPTIVE_V2 * More fixes related to the rename from adaptive2 to adaptive_v2 * Fix dynamodb retry resolver logic for adaptive mode * Properly clean up the test state * Address PR comments
1 parent c21eeed commit 67b7844

File tree

11 files changed

+866
-37
lines changed

11 files changed

+866
-37
lines changed

core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryStrategy.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import software.amazon.awssdk.annotations.SdkPublicApi;
1919
import software.amazon.awssdk.awscore.exception.AwsServiceException;
2020
import software.amazon.awssdk.awscore.internal.AwsErrorCode;
21+
import software.amazon.awssdk.core.internal.retry.RetryPolicyAdapter;
2122
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
2223
import software.amazon.awssdk.core.retry.RetryMode;
2324
import software.amazon.awssdk.retries.AdaptiveRetryStrategy;
@@ -54,10 +55,12 @@ private AwsRetryStrategy() {
5455
switch (mode) {
5556
case STANDARD:
5657
return standardRetryStrategy();
57-
case ADAPTIVE:
58+
case ADAPTIVE_V2:
5859
return adaptiveRetryStrategy();
5960
case LEGACY:
6061
return legacyRetryStrategy();
62+
case ADAPTIVE:
63+
return legacyAdaptiveRetryStrategy();
6164
default:
6265
throw new IllegalArgumentException("unknown retry mode: " + mode);
6366
}
@@ -84,7 +87,6 @@ private AwsRetryStrategy() {
8487
return DefaultRetryStrategy.none();
8588
}
8689

87-
8890
/**
8991
* Returns a {@link StandardRetryStrategy} with AWS-specific conditions added.
9092
*
@@ -121,8 +123,8 @@ public static AdaptiveRetryStrategy adaptiveRetryStrategy() {
121123
* Configures a retry strategy using its builder to add AWS-specific retry exceptions.
122124
*
123125
* @param builder The builder to add the AWS-specific retry exceptions
126+
* @param <T> The type of the builder extending {@link RetryStrategy.Builder}
124127
* @return The given builder
125-
* @param <T> The type of the builder extending {@link RetryStrategy.Builder}
126128
*/
127129
public static <T extends RetryStrategy.Builder<T, ?>> T configure(T builder) {
128130
return builder.retryOnException(AwsRetryStrategy::retryOnAwsRetryableErrors);
@@ -135,6 +137,9 @@ public static AdaptiveRetryStrategy adaptiveRetryStrategy() {
135137
* @return The given builder
136138
*/
137139
public static RetryStrategy.Builder<?, ?> configureStrategy(RetryStrategy.Builder<?, ?> builder) {
140+
if (builder instanceof RetryPolicyAdapter.Builder) {
141+
return builder;
142+
}
138143
return builder.retryOnException(AwsRetryStrategy::retryOnAwsRetryableErrors);
139144
}
140145

@@ -145,4 +150,16 @@ private static boolean retryOnAwsRetryableErrors(Throwable ex) {
145150
}
146151
return false;
147152
}
153+
154+
/**
155+
* Returns a {@link RetryStrategy<?, ?>} that implements the legacy {@link RetryMode#ADAPTIVE} mode.
156+
*
157+
* @return a {@link RetryStrategy<?, ?>} that implements the legacy {@link RetryMode#ADAPTIVE} mode.
158+
*/
159+
private static RetryStrategy<?, ?> legacyAdaptiveRetryStrategy() {
160+
return RetryPolicyAdapter.builder()
161+
.retryPolicy(AwsRetryPolicy.forRetryMode(RetryMode.ADAPTIVE))
162+
.build();
163+
}
164+
148165
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper2.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public final class RetryableStageHelper2 {
6060
public static final String SDK_RETRY_INFO_HEADER = "amz-sdk-request";
6161
private final SdkHttpFullRequest request;
6262
private final RequestExecutionContext context;
63-
private final RetryPolicy retryPolicy;
6463
private RetryPolicyAdapter retryPolicyAdapter;
6564
private final RetryStrategy<?, ?> retryStrategy;
6665
private final HttpClientDependencies dependencies;
@@ -74,8 +73,16 @@ public RetryableStageHelper2(SdkHttpFullRequest request,
7473
HttpClientDependencies dependencies) {
7574
this.request = request;
7675
this.context = context;
77-
this.retryPolicy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_POLICY);
78-
this.retryStrategy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_STRATEGY);
76+
RetryPolicy retryPolicy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_POLICY);
77+
RetryStrategy<?, ?> retryStrategy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_STRATEGY);
78+
if (retryPolicy != null) {
79+
retryPolicyAdapter = RetryPolicyAdapter.builder()
80+
.retryPolicy(retryPolicy)
81+
.build();
82+
} else if (retryStrategy instanceof RetryPolicyAdapter) {
83+
retryPolicyAdapter = (RetryPolicyAdapter) retryStrategy;
84+
}
85+
this.retryStrategy = retryStrategy;
7986
this.dependencies = dependencies;
8087
}
8188

@@ -256,15 +263,14 @@ private int retriesAttemptedSoFar() {
256263
* calling code.
257264
*/
258265
private RetryStrategy<?, ?> retryStrategy() {
259-
if (retryPolicy != null) {
260-
if (retryPolicyAdapter == null) {
261-
retryPolicyAdapter = RetryPolicyAdapter.builder()
262-
.retryPolicy(this.retryPolicy)
266+
if (retryPolicyAdapter != null) {
267+
if (retryPolicyAdapter.isInitialized()) {
268+
retryPolicyAdapter = retryPolicyAdapter.toBuilder()
263269
.retryPolicyContext(retryPolicyContext())
264270
.build();
265271
} else {
266272
retryPolicyAdapter = retryPolicyAdapter.toBuilder()
267-
.retryPolicyContext(retryPolicyContext())
273+
.initialize(retryPolicyContext())
268274
.build();
269275
}
270276
return retryPolicyAdapter;

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/RetryPolicyAdapter.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,26 @@
4242
*/
4343
@SdkInternalApi
4444
public final class RetryPolicyAdapter implements RetryStrategy<RetryPolicyAdapter.Builder, RetryPolicyAdapter> {
45-
4645
private final RetryPolicy retryPolicy;
4746
private final RetryPolicyContext retryPolicyContext;
4847
private final RateLimitingTokenBucket rateLimitingTokenBucket;
4948

5049
private RetryPolicyAdapter(Builder builder) {
5150
this.retryPolicy = Validate.paramNotNull(builder.retryPolicy, "retryPolicy");
52-
this.retryPolicyContext = Validate.paramNotNull(builder.retryPolicyContext, "retryPolicyContext");
51+
this.retryPolicyContext = builder.retryPolicyContext;
5352
this.rateLimitingTokenBucket = builder.rateLimitingTokenBucket;
5453
}
5554

5655
@Override
5756
public AcquireInitialTokenResponse acquireInitialToken(AcquireInitialTokenRequest request) {
57+
validateState();
5858
RetryPolicyAdapterToken token = new RetryPolicyAdapterToken(request.scope());
5959
return AcquireInitialTokenResponse.create(token, rateLimitingTokenAcquire());
6060
}
6161

6262
@Override
6363
public RefreshRetryTokenResponse refreshRetryToken(RefreshRetryTokenRequest request) {
64+
validateState();
6465
RetryPolicyAdapterToken token = getToken(request.token());
6566
boolean willRetry = retryPolicy.aggregateRetryCondition().shouldRetry(retryPolicyContext);
6667
if (!willRetry) {
@@ -73,6 +74,7 @@ public RefreshRetryTokenResponse refreshRetryToken(RefreshRetryTokenRequest requ
7374

7475
@Override
7576
public RecordSuccessResponse recordSuccess(RecordSuccessRequest request) {
77+
validateState();
7678
RetryPolicyAdapterToken token = getToken(request.token());
7779
retryPolicy.aggregateRetryCondition().requestSucceeded(retryPolicyContext);
7880
return RecordSuccessResponse.create(token);
@@ -88,6 +90,16 @@ public Builder toBuilder() {
8890
return new Builder(this);
8991
}
9092

93+
public boolean isInitialized() {
94+
return retryPolicyContext != null;
95+
}
96+
97+
void validateState() {
98+
if (retryPolicyContext == null) {
99+
throw new IllegalStateException("This RetryPolicyAdapter instance has not been initialized.");
100+
}
101+
}
102+
91103
RetryPolicyAdapterToken getToken(RetryToken token) {
92104
return Validate.isInstanceOf(RetryPolicyAdapterToken.class, token, "Object of class %s was not created by this retry "
93105
+ "strategy", token.getClass().getName());
@@ -146,7 +158,6 @@ public static class Builder implements RetryStrategy.Builder<RetryPolicyAdapter.
146158
private RateLimitingTokenBucket rateLimitingTokenBucket;
147159

148160
private Builder() {
149-
rateLimitingTokenBucket = new RateLimitingTokenBucket();
150161
}
151162

152163
private Builder(RetryPolicyAdapter adapter) {
@@ -162,7 +173,7 @@ public Builder retryOnException(Predicate<Throwable> shouldRetry) {
162173

163174
@Override
164175
public Builder maxAttempts(int maxAttempts) {
165-
throw new UnsupportedOperationException("RetryPolicyAdapter does not support calling retryOnException");
176+
throw new UnsupportedOperationException("RetryPolicyAdapter does not support calling maxAttempts");
166177
}
167178

168179
@Override
@@ -175,13 +186,14 @@ public Builder retryPolicy(RetryPolicy retryPolicy) {
175186
return this;
176187
}
177188

178-
public Builder rateLimitingTokenBucket(RateLimitingTokenBucket rateLimitingTokenBucket) {
179-
this.rateLimitingTokenBucket = rateLimitingTokenBucket;
189+
public Builder retryPolicyContext(RetryPolicyContext retryPolicyContext) {
190+
this.retryPolicyContext = retryPolicyContext;
180191
return this;
181192
}
182193

183-
public Builder retryPolicyContext(RetryPolicyContext retryPolicyContext) {
194+
public Builder initialize(RetryPolicyContext retryPolicyContext) {
184195
this.retryPolicyContext = retryPolicyContext;
196+
this.rateLimitingTokenBucket = new RateLimitingTokenBucket();
185197
return this;
186198
}
187199

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetryStrategy.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import software.amazon.awssdk.core.exception.SdkException;
2020
import software.amazon.awssdk.core.exception.SdkServiceException;
2121
import software.amazon.awssdk.core.retry.RetryMode;
22+
import software.amazon.awssdk.core.retry.RetryPolicy;
2223
import software.amazon.awssdk.core.retry.RetryUtils;
2324
import software.amazon.awssdk.retries.AdaptiveRetryStrategy;
2425
import software.amazon.awssdk.retries.DefaultRetryStrategy;
@@ -55,6 +56,8 @@ private SdkDefaultRetryStrategy() {
5556
case STANDARD:
5657
return standardRetryStrategy();
5758
case ADAPTIVE:
59+
return legacyAdaptiveRetryStrategy();
60+
case ADAPTIVE_V2:
5861
return adaptiveRetryStrategy();
5962
case LEGACY:
6063
return legacyRetryStrategy();
@@ -74,11 +77,14 @@ public static RetryMode retryMode(RetryStrategy<?, ?> retryStrategy) {
7477
return RetryMode.STANDARD;
7578
}
7679
if (retryStrategy instanceof AdaptiveRetryStrategy) {
77-
return RetryMode.ADAPTIVE;
80+
return RetryMode.ADAPTIVE_V2;
7881
}
7982
if (retryStrategy instanceof LegacyRetryStrategy) {
8083
return RetryMode.LEGACY;
8184
}
85+
if (retryStrategy instanceof RetryPolicyAdapter) {
86+
return RetryMode.ADAPTIVE;
87+
}
8288
throw new IllegalArgumentException("unknown retry strategy class: " + retryStrategy.getClass().getName());
8389
}
8490

@@ -193,4 +199,16 @@ private static boolean retryOnThrottlingCondition(Throwable ex) {
193199
}
194200
return false;
195201
}
202+
203+
/**
204+
* Returns a {@link RetryStrategy<?, ?>} that implements the legacy {@link RetryMode#ADAPTIVE} mode.
205+
*
206+
* @return a {@link RetryStrategy<?, ?>} that implements the legacy {@link RetryMode#ADAPTIVE} mode.
207+
*/
208+
private static RetryStrategy<?, ?> legacyAdaptiveRetryStrategy() {
209+
return RetryPolicyAdapter.builder()
210+
.retryPolicy(RetryPolicy.forRetryMode(RetryMode.ADAPTIVE))
211+
.build();
212+
}
196213
}
214+

core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryMode.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public enum RetryMode {
7373
STANDARD,
7474

7575
/**
76-
* Adaptive retry mode builds on {@code STANDARD} mode.
76+
* Adaptive retry mode builds on {@link #STANDARD} mode.
7777
* <p>
7878
* Adaptive retry mode dynamically limits the rate of AWS requests to maximize success rate. This may be at the
7979
* expense of request latency. Adaptive retry mode is not recommended when predictable latency is important.
@@ -84,9 +84,31 @@ public enum RetryMode {
8484
* the same client. When using adaptive retry mode, we recommend using a single client per resource.
8585
*
8686
* @see RetryPolicy#isFastFailRateLimiting()
87+
* @deprecated As of 2.25.xx, replaced by {@link #ADAPTIVE_V2}. The ADAPTIVE implementation has a bug that prevents it
88+
* from remembering its state across requests which is needed to correctly estimate its sending rate. Given that
89+
* this bug has been present since its introduction and that correct version might change the traffic patterns of the SDK we
90+
* deemed too risky to fix this implementation.
8791
*/
92+
@Deprecated
8893
ADAPTIVE,
8994

95+
/**
96+
* Adaptive V2 retry mode builds on {@link #STANDARD} mode.
97+
* <p>
98+
* Adaptive retry mode qdynamically limits the rate of AWS requests to maximize success rate. This may be at the
99+
* expense of request latency. Adaptive V2 retry mode is not recommended when predictable latency is important.
100+
* <p>
101+
* {@code ADAPTIVE_V2} mode differs from {@link #ADAPTIVE} mode in the computed delays between calls, including the first
102+
* attempt
103+
* that might be delayed if the algorithm considers that it's needed to increase the odds of a successful response.
104+
* <p>
105+
* <b>Warning:</b> Adaptive V2 retry mode assumes that the client is working against a single resource (e.g. one
106+
* DynamoDB Table or one S3 Bucket). If you use a single client for multiple resources, throttling or outages
107+
* associated with one resource will result in increased latency and failures when accessing all other resources via
108+
* the same client. When using adaptive retry mode, we recommend using a single client per resource.
109+
*/
110+
ADAPTIVE_V2,
111+
90112
;
91113

92114
/**
@@ -176,6 +198,8 @@ private static Optional<RetryMode> fromString(String string) {
176198
return Optional.of(STANDARD);
177199
case "adaptive":
178200
return Optional.of(ADAPTIVE);
201+
case "adaptive_v2":
202+
return Optional.of(ADAPTIVE_V2);
179203
default:
180204
throw new IllegalStateException("Unsupported retry policy mode configured: " + string);
181205
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicy.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ private static final class BuilderImpl implements Builder {
372372
private Boolean fastFailRateLimiting;
373373

374374
private BuilderImpl(RetryMode retryMode) {
375+
if (retryMode == RetryMode.ADAPTIVE_V2) {
376+
throw new UnsupportedOperationException("ADAPTIVE_V2 is not supported by retry policies, use a RetryStrategy "
377+
+ "instead");
378+
}
375379
this.retryMode = retryMode;
376380
this.numRetries = SdkDefaultRetrySetting.maxAttempts(retryMode) - 1;
377381
this.additionalRetryConditionsAllowed = true;

services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
2424
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
2525
import software.amazon.awssdk.core.client.config.SdkClientOption;
26+
import software.amazon.awssdk.core.internal.retry.RetryPolicyAdapter;
2627
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting;
2728
import software.amazon.awssdk.core.retry.RetryMode;
2829
import software.amazon.awssdk.core.retry.RetryPolicy;
@@ -76,18 +77,8 @@ public static RetryPolicy resolveRetryPolicy(SdkClientConfiguration config) {
7677
return configuredRetryPolicy;
7778
}
7879

79-
RetryMode retryMode = RetryMode.resolver()
80-
.profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
81-
.profileName(config.option(SdkClientOption.PROFILE_NAME))
82-
.defaultRetryMode(config.option(SdkClientOption.DEFAULT_RETRY_MODE))
83-
.resolve();
84-
85-
return AwsRetryPolicy.forRetryMode(retryMode)
86-
.toBuilder()
87-
.additionalRetryConditionsAllowed(false)
88-
.numRetries(MAX_ERROR_RETRY)
89-
.backoffStrategy(BACKOFF_STRATEGY)
90-
.build();
80+
RetryMode retryMode = resolveRetryMode(config);
81+
return retryPolicyFor(retryMode);
9182
}
9283

9384
public static RetryStrategy<?, ?> resolveRetryStrategy(SdkClientConfiguration config) {
@@ -96,16 +87,35 @@ public static RetryPolicy resolveRetryPolicy(SdkClientConfiguration config) {
9687
return configuredRetryStrategy;
9788
}
9889

99-
RetryMode retryMode = RetryMode.resolver()
100-
.profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
101-
.profileName(config.option(SdkClientOption.PROFILE_NAME))
102-
.defaultRetryMode(config.option(SdkClientOption.DEFAULT_RETRY_MODE))
103-
.resolve();
90+
RetryMode retryMode = resolveRetryMode(config);
91+
92+
if (retryMode == RetryMode.ADAPTIVE) {
93+
return RetryPolicyAdapter.builder()
94+
.retryPolicy(retryPolicyFor(retryMode))
95+
.build();
96+
}
10497

10598
return AwsRetryStrategy.forRetryMode(retryMode)
10699
.toBuilder()
107100
.maxAttempts(MAX_ATTEMPTS)
108101
.backoffStrategy(exponentialDelay(BASE_DELAY, SdkDefaultRetrySetting.MAX_BACKOFF))
109102
.build();
110103
}
104+
105+
private static RetryPolicy retryPolicyFor(RetryMode retryMode) {
106+
return AwsRetryPolicy.forRetryMode(retryMode)
107+
.toBuilder()
108+
.additionalRetryConditionsAllowed(false)
109+
.numRetries(MAX_ERROR_RETRY)
110+
.backoffStrategy(BACKOFF_STRATEGY)
111+
.build();
112+
}
113+
114+
private static RetryMode resolveRetryMode(SdkClientConfiguration config) {
115+
return RetryMode.resolver()
116+
.profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
117+
.profileName(config.option(SdkClientOption.PROFILE_NAME))
118+
.defaultRetryMode(config.option(SdkClientOption.DEFAULT_RETRY_MODE))
119+
.resolve();
120+
}
111121
}

0 commit comments

Comments
 (0)