Skip to content

Commit b18babd

Browse files
authored
2.1 behavior in standard, adaptive strats (#6871)
* 2.1 behavior in standard, adaptive strats Support the 2.1 behavior changes in adaptive and standard strategies. This includes the change in constant values based on 2.0 and 2.1, and the application of a different cost for throttling retries in 2.1. The 2.1 behavior is implemented as an overload of the builder() method that accepts a boolean to select between 2.0 and 2.1. The no-arg version defaults to false, i.e. 2.0. * Review comments
1 parent f387f6f commit b18babd

8 files changed

Lines changed: 353 additions & 9 deletions

File tree

core/retries/src/main/java/software/amazon/awssdk/retries/AdaptiveRetryStrategy.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.retries;
1717

18+
import java.time.Duration;
1819
import java.util.function.Predicate;
1920
import software.amazon.awssdk.annotations.SdkPublicApi;
2021
import software.amazon.awssdk.annotations.ThreadSafe;
@@ -64,15 +65,33 @@ public interface AdaptiveRetryStrategy extends RetryStrategy {
6465
* </pre>
6566
*/
6667
static AdaptiveRetryStrategy.Builder builder() {
68+
return builder(false);
69+
}
70+
71+
/**
72+
* Create a new {@link AdaptiveRetryStrategy.Builder} with v2.0 or v2.1 retry constants.
73+
*
74+
* @param retries2026Enabled when {@code true}, uses v2.1 constants (50ms base delay, differentiated token costs);
75+
* when {@code false}, uses v2.0 constants (100ms base delay, uniform token costs)
76+
*/
77+
static AdaptiveRetryStrategy.Builder builder(boolean retries2026Enabled) {
78+
Duration baseDelay = retries2026Enabled ? DefaultRetryStrategy.Standard.BASE_DELAY_V21
79+
: DefaultRetryStrategy.Standard.BASE_DELAY_V20;
80+
int exceptionCost = retries2026Enabled ? DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V21
81+
: DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V20;
82+
// v2.0 does not treat throttling exceptions differently from others
83+
int throttlingCost = retries2026Enabled ? DefaultRetryStrategy.Standard.THROTTLING_EXCEPTION_TOKEN_COST_V21
84+
: exceptionCost;
6785
return DefaultAdaptiveRetryStrategy
6886
.builder()
6987
.maxAttempts(DefaultRetryStrategy.Adaptive.MAX_ATTEMPTS)
7088
.tokenBucketStore(TokenBucketStore.builder()
7189
.tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE)
7290
.build())
73-
.tokenBucketExceptionCost(DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST)
91+
.tokenBucketExceptionCost(exceptionCost)
92+
.throttlingTokenBucketExceptionCost(throttlingCost)
7493
.rateLimiterTokenBucketStore(RateLimiterTokenBucketStore.builder().build())
75-
.backoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.BASE_DELAY,
94+
.backoffStrategy(BackoffStrategy.exponentialDelay(baseDelay,
7695
DefaultRetryStrategy.Standard.MAX_BACKOFF))
7796
.throttlingBackoffStrategy(BackoffStrategy.exponentialDelay(
7897
DefaultRetryStrategy.Standard.THROTTLED_BASE_DELAY,

core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,19 @@ public static AdaptiveRetryStrategy.Builder adaptiveStrategyBuilder() {
8787

8888
static final class Standard {
8989
static final int MAX_ATTEMPTS = 3;
90-
static final Duration BASE_DELAY = Duration.ofMillis(100);
90+
91+
// v2.1 constants
92+
static final Duration BASE_DELAY_V21 = Duration.ofMillis(50);
93+
static final int DEFAULT_EXCEPTION_TOKEN_COST_V21 = 14;
94+
static final int THROTTLING_EXCEPTION_TOKEN_COST_V21 = 5;
95+
96+
// v2.0 constants
97+
static final Duration BASE_DELAY_V20 = Duration.ofMillis(100);
98+
static final int DEFAULT_EXCEPTION_TOKEN_COST_V20 = 5;
99+
91100
static final Duration THROTTLED_BASE_DELAY = Duration.ofSeconds(1);
92101
static final Duration MAX_BACKOFF = Duration.ofSeconds(20);
93102
static final int TOKEN_BUCKET_SIZE = 500;
94-
static final int DEFAULT_EXCEPTION_TOKEN_COST = 5;
95103

96104
private Standard() {
97105
}

core/retries/src/main/java/software/amazon/awssdk/retries/StandardRetryStrategy.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.retries;
1717

18+
import java.time.Duration;
1819
import software.amazon.awssdk.annotations.SdkPublicApi;
1920
import software.amazon.awssdk.annotations.ThreadSafe;
2021
import software.amazon.awssdk.retries.api.BackoffStrategy;
@@ -56,15 +57,33 @@ public interface StandardRetryStrategy extends RetryStrategy {
5657
* </pre>
5758
*/
5859
static Builder builder() {
60+
return builder(false);
61+
}
62+
63+
/**
64+
* Create a new {@link StandardRetryStrategy.Builder} with v2.0 or v2.1 retry constants.
65+
*
66+
* @param retries2026Enabled when {@code true}, uses v2.1 constants (50ms base delay, differentiated token costs);
67+
* when {@code false}, uses v2.0 constants (100ms base delay, uniform token costs)
68+
*/
69+
static Builder builder(boolean retries2026Enabled) {
70+
Duration baseDelay = retries2026Enabled ? DefaultRetryStrategy.Standard.BASE_DELAY_V21
71+
: DefaultRetryStrategy.Standard.BASE_DELAY_V20;
72+
int exceptionCost = retries2026Enabled ? DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V21
73+
: DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST_V20;
74+
// v2.0 does not treat throttling exceptions differently from others
75+
int throttlingCost = retries2026Enabled ? DefaultRetryStrategy.Standard.THROTTLING_EXCEPTION_TOKEN_COST_V21
76+
: exceptionCost;
5977
return DefaultStandardRetryStrategy
6078
.builder()
6179
.maxAttempts(DefaultRetryStrategy.Standard.MAX_ATTEMPTS)
6280
.tokenBucketStore(TokenBucketStore
6381
.builder()
6482
.tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE)
6583
.build())
66-
.tokenBucketExceptionCost(DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST)
67-
.backoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.BASE_DELAY,
84+
.tokenBucketExceptionCost(exceptionCost)
85+
.throttlingTokenBucketExceptionCost(throttlingCost)
86+
.backoffStrategy(BackoffStrategy.exponentialDelay(baseDelay,
6887
DefaultRetryStrategy.Standard.MAX_BACKOFF))
6988
.throttlingBackoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.THROTTLED_BASE_DELAY,
7089
DefaultRetryStrategy.Standard.MAX_BACKOFF));

core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public abstract class BaseRetryStrategy implements DefaultAwareRetryStrategy {
5757
protected final BackoffStrategy throttlingBackoffStrategy;
5858
protected final Predicate<Throwable> treatAsThrottling;
5959
protected final int exceptionCost;
60+
protected final int throttlingExceptionCost;
6061
protected final TokenBucketStore tokenBucketStore;
6162
protected final Set<String> defaultsAdded;
6263
protected final boolean useClientDefaults;
@@ -71,6 +72,8 @@ public abstract class BaseRetryStrategy implements DefaultAwareRetryStrategy {
7172
this.throttlingBackoffStrategy = Validate.paramNotNull(builder.throttlingBackoffStrategy, "throttlingBackoffStrategy");
7273
this.treatAsThrottling = Validate.paramNotNull(builder.treatAsThrottling, "treatAsThrottling");
7374
this.exceptionCost = Validate.paramNotNull(builder.exceptionCost, "exceptionCost");
75+
this.throttlingExceptionCost = builder.throttlingExceptionCost != null
76+
? builder.throttlingExceptionCost : this.exceptionCost;
7477
this.tokenBucketStore = Validate.paramNotNull(builder.tokenBucketStore, "tokenBucketStore");
7578
this.defaultsAdded = Collections.unmodifiableSet(
7679
Validate.paramNotNull(new HashSet<>(builder.defaultsAdded), "defaultsAdded"));
@@ -194,10 +197,13 @@ protected void updateStateForRetry(RefreshRetryTokenRequest request) {
194197
* amount for the specific kind of failure.
195198
*/
196199
protected int exceptionCost(RefreshRetryTokenRequest request) {
197-
if (circuitBreakerEnabled) {
198-
return exceptionCost;
200+
if (!circuitBreakerEnabled) {
201+
return 0;
199202
}
200-
return 0;
203+
if (treatAsThrottling.test(request.failure())) {
204+
return throttlingExceptionCost;
205+
}
206+
return exceptionCost;
201207
}
202208

203209
/**
@@ -397,6 +403,7 @@ public String toString() {
397403
.add("tokenBucketStore", tokenBucketStore)
398404
.add("defaultsAdded", defaultsAdded)
399405
.add("useClientDefaults", useClientDefaults)
406+
.add("throttlingExceptionCost", throttlingExceptionCost)
400407
.build();
401408
}
402409

@@ -408,6 +415,7 @@ public abstract static class Builder implements DefaultAwareRetryStrategy.Builde
408415
private Boolean circuitBreakerEnabled;
409416
private Boolean useClientDefaults;
410417
private Integer exceptionCost;
418+
private Integer throttlingExceptionCost;
411419
private BackoffStrategy backoffStrategy;
412420
private BackoffStrategy throttlingBackoffStrategy;
413421
private Predicate<Throwable> treatAsThrottling = throwable -> false;
@@ -423,6 +431,7 @@ public abstract static class Builder implements DefaultAwareRetryStrategy.Builde
423431
this.maxAttempts = strategy.maxAttempts;
424432
this.circuitBreakerEnabled = strategy.circuitBreakerEnabled;
425433
this.exceptionCost = strategy.exceptionCost;
434+
this.throttlingExceptionCost = strategy.throttlingExceptionCost;
426435
this.backoffStrategy = strategy.backoffStrategy;
427436
this.throttlingBackoffStrategy = strategy.throttlingBackoffStrategy;
428437
this.treatAsThrottling = strategy.treatAsThrottling;
@@ -463,6 +472,10 @@ void setTokenBucketExceptionCost(int exceptionCost) {
463472
this.exceptionCost = exceptionCost;
464473
}
465474

475+
void setThrottlingTokenBucketExceptionCost(int throttlingExceptionCost) {
476+
this.throttlingExceptionCost = throttlingExceptionCost;
477+
}
478+
466479
void setUseClientDefaults(Boolean useClientDefaults) {
467480
this.useClientDefaults = useClientDefaults;
468481
}

core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultAdaptiveRetryStrategy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ public Builder tokenBucketExceptionCost(int exceptionCost) {
129129
return this;
130130
}
131131

132+
public Builder throttlingTokenBucketExceptionCost(int throttlingExceptionCost) {
133+
setThrottlingTokenBucketExceptionCost(throttlingExceptionCost);
134+
return this;
135+
}
136+
132137
public Builder rateLimiterTokenBucketStore(RateLimiterTokenBucketStore rateLimiterTokenBucketStore) {
133138
this.rateLimiterTokenBucketStore = rateLimiterTokenBucketStore;
134139
return this;

core/retries/src/main/java/software/amazon/awssdk/retries/internal/DefaultStandardRetryStrategy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ public Builder tokenBucketExceptionCost(int exceptionCost) {
9090
return this;
9191
}
9292

93+
public Builder throttlingTokenBucketExceptionCost(int throttlingExceptionCost) {
94+
setThrottlingTokenBucketExceptionCost(throttlingExceptionCost);
95+
return this;
96+
}
97+
9398
public Builder tokenBucketStore(TokenBucketStore tokenBucketStore) {
9499
setTokenBucketStore(tokenBucketStore);
95100
return this;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.retries;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.time.Duration;
21+
import org.junit.jupiter.api.Test;
22+
import software.amazon.awssdk.retries.api.AcquireInitialTokenResponse;
23+
import software.amazon.awssdk.retries.api.RefreshRetryTokenRequest;
24+
import software.amazon.awssdk.retries.api.RefreshRetryTokenResponse;
25+
import software.amazon.awssdk.retries.api.RetryStrategy;
26+
import software.amazon.awssdk.retries.api.RetryToken;
27+
import software.amazon.awssdk.retries.api.internal.AcquireInitialTokenRequestImpl;
28+
import software.amazon.awssdk.retries.internal.DefaultRetryToken;
29+
30+
/**
31+
* Tests that {@code AdaptiveRetryStrategy.builder(boolean retries2026Enabled)} selects the correct
32+
* v2.0 or v2.1 constants for base delay, exception token cost, and throttling token cost.
33+
*/
34+
class AdaptiveRetryStrategyV21ConstantsTest {
35+
36+
private static final int BUCKET_CAPACITY = 500;
37+
38+
@Test
39+
void v21Enabled_nonThrottlingRetry_deducts14Tokens() {
40+
RetryStrategy strategy = AdaptiveRetryStrategy.builder(true)
41+
.retryOnException(t -> true)
42+
.treatAsThrottling(t -> false)
43+
.build();
44+
45+
DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient"));
46+
assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 14);
47+
}
48+
49+
@Test
50+
void v21Enabled_throttlingRetry_deducts5Tokens() {
51+
RetryStrategy strategy = AdaptiveRetryStrategy.builder(true)
52+
.retryOnException(t -> true)
53+
.treatAsThrottling(t -> true)
54+
.build();
55+
56+
DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("throttled"));
57+
assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5);
58+
}
59+
60+
@Test
61+
void v20_nonThrottlingRetry_deducts5Tokens() {
62+
RetryStrategy strategy = AdaptiveRetryStrategy.builder(false)
63+
.retryOnException(t -> true)
64+
.treatAsThrottling(t -> false)
65+
.build();
66+
67+
DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient"));
68+
assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5);
69+
}
70+
71+
@Test
72+
void v20_throttlingRetry_deducts5Tokens() {
73+
RetryStrategy strategy = AdaptiveRetryStrategy.builder(false)
74+
.retryOnException(t -> true)
75+
.treatAsThrottling(t -> true)
76+
.build();
77+
78+
DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("throttled"));
79+
assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5);
80+
}
81+
82+
@Test
83+
void v21Enabled_backoffUses50msBaseDelay() {
84+
RetryStrategy strategy = AdaptiveRetryStrategy.builder(true)
85+
.retryOnException(t -> true)
86+
.build();
87+
88+
RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err"));
89+
// First retry delay should include exponential backoff component in [0, 50ms]
90+
assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(50));
91+
}
92+
93+
@Test
94+
void v20_backoffUses100msBaseDelay() {
95+
RetryStrategy strategy = AdaptiveRetryStrategy.builder(false)
96+
.retryOnException(t -> true)
97+
.build();
98+
99+
RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err"));
100+
// First retry delay should include exponential backoff component in [0, 100ms]
101+
assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(100));
102+
}
103+
104+
@Test
105+
void noArgBuilder_usesV20Constants() {
106+
RetryStrategy strategy = AdaptiveRetryStrategy.builder()
107+
.retryOnException(t -> true)
108+
.treatAsThrottling(t -> false)
109+
.build();
110+
111+
DefaultRetryToken token = retryOnceBeforeSuccess(strategy, new RuntimeException("transient"));
112+
// v2.0: exception cost is 5
113+
assertThat(token.capacityRemaining()).isEqualTo(BUCKET_CAPACITY - 5);
114+
}
115+
116+
/**
117+
* Acquires an initial token, triggers one retry. Returns the token after the retry (before success).
118+
*/
119+
private DefaultRetryToken retryOnceBeforeSuccess(RetryStrategy strategy, Exception failure) {
120+
AcquireInitialTokenResponse initial = strategy.acquireInitialToken(AcquireInitialTokenRequestImpl.create("test"));
121+
RetryToken token = initial.token();
122+
123+
RefreshRetryTokenResponse refreshResponse = strategy.refreshRetryToken(
124+
RefreshRetryTokenRequest.builder().token(token).failure(failure).build());
125+
126+
return (DefaultRetryToken) refreshResponse.token();
127+
}
128+
129+
/**
130+
* Acquires an initial token and triggers one refresh to get the backoff delay.
131+
*/
132+
private RefreshRetryTokenResponse refreshToken(RetryStrategy strategy, Exception failure) {
133+
AcquireInitialTokenResponse initial = strategy.acquireInitialToken(AcquireInitialTokenRequestImpl.create("test"));
134+
return strategy.refreshRetryToken(
135+
RefreshRetryTokenRequest.builder().token(initial.token()).failure(failure).build());
136+
}
137+
}

0 commit comments

Comments
 (0)