Skip to content

Commit 1f7c2ab

Browse files
authored
Add throttling backoff to all retry strategies (#5348)
* Add throttling backoff to all retry strategies * Update javadoc as per suggestion
1 parent 181e197 commit 1f7c2ab

File tree

14 files changed

+354
-46
lines changed

14 files changed

+354
-46
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "The retry strategies implementation was not backwards compatible with the retry policies in regards of throttled exceptions, for these the retry policies had a different backoff strategy that is much more slower. This change retrofits the retry strategies to have also a different backoff strategy for throttling errors that has the same base and max delay values as the legacy retry policy."
6+
}

core/retries-spi/src/main/java/software/amazon/awssdk/retries/api/RetryStrategy.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ default B retryOnRootCauseInstanceOf(Class<? extends Throwable> throwable) {
188188
}
189189

190190
/**
191-
* Configure the maximum number of attempts used by this executor.
191+
* Configure the maximum number of attempts used by the retry strategy.
192192
*
193193
* <p>The actual number of attempts made may be less, depending on the retry strategy implementation. For example, the
194194
* standard and adaptive retry modes both employ short-circuiting which reduces the maximum attempts during outages.
@@ -198,12 +198,26 @@ default B retryOnRootCauseInstanceOf(Class<? extends Throwable> throwable) {
198198
B maxAttempts(int maxAttempts);
199199

200200
/**
201-
* Configure the backoff strategy used by this executor.
201+
* Configure the backoff strategy used by the retry strategy.
202202
*
203203
* <p>By default, this uses jittered exponential backoff.
204204
*/
205205
B backoffStrategy(BackoffStrategy backoffStrategy);
206206

207+
/**
208+
* Configure the backoff strategy used for throttling exceptions by this strategy.
209+
*
210+
* <p>By default, this uses jittered exponential backoff.
211+
*/
212+
B throttlingBackoffStrategy(BackoffStrategy throttlingBackoffStrategy);
213+
214+
/**
215+
* Configure a predicate that determines whether a retryable exception is a throttling exception. When this predicate
216+
* returns true, the retry strategy will use the {@link #throttlingBackoffStrategy}. If it returns false, the
217+
* {@link #backoffStrategy} will be used. This predicate will not be called for non-retryable exceptions.
218+
*/
219+
B treatAsThrottling(Predicate<Throwable> treatAsThrottling);
220+
207221
/**
208222
* Build a new {@link RetryStrategy} with the current configuration on this builder.
209223
*/

core/retries-spi/src/test/java/software/amazon/awssdk/retries/api/RetryStrategyBuilderTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ public BuilderToTestDefaults backoffStrategy(BackoffStrategy backoffStrategy) {
158158
return this;
159159
}
160160

161+
@Override
162+
public BuilderToTestDefaults throttlingBackoffStrategy(BackoffStrategy backoffStrategy) {
163+
return this;
164+
}
165+
166+
@Override
167+
public BuilderToTestDefaults treatAsThrottling(Predicate<Throwable> treatAsThrottling) {
168+
return this;
169+
}
170+
161171
@Override
162172
public DummyRetryStrategy build() {
163173
return null;

core/retries/src/it/java/software/amazon/awssdk/retries/internal/AdaptiveRetryStrategyResourceConstrainedTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ void seemsToBeCorrectAndThreadSafe() {
7171
.tokenBucketStore(TokenBucketStore.builder().tokenBucketMaxCapacity(10_000).build())
7272
// Just wait for the rate limiter delays.
7373
.backoffStrategy(BackoffStrategy.retryImmediately())
74+
.throttlingBackoffStrategy(BackoffStrategy.retryImmediately())
7475
.rateLimiterTokenBucketStore(RateLimiterTokenBucketStore.builder().build())
7576
.retryOnExceptionInstanceOf(ThrottlingException.class)
7677
.treatAsThrottling(x -> x instanceof ThrottlingException)

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import software.amazon.awssdk.annotations.SdkPublicApi;
2020
import software.amazon.awssdk.annotations.ThreadSafe;
2121
import software.amazon.awssdk.retries.api.AcquireInitialTokenRequest;
22-
import software.amazon.awssdk.retries.api.BackoffStrategy;
2322
import software.amazon.awssdk.retries.api.RetryStrategy;
2423
import software.amazon.awssdk.retries.internal.DefaultAdaptiveRetryStrategy;
2524
import software.amazon.awssdk.retries.internal.circuitbreaker.TokenBucketStore;
@@ -30,10 +29,11 @@
3029
* <p>
3130
* Unlike {@link StandardRetryStrategy}, care should be taken when using this strategy. Specifically, it should be used:
3231
* <ol>
33-
* <li>When the availability of downstream resources are mostly affected by callers that are also using
34-
* the {@link AdaptiveRetryStrategy}.
35-
* <li>The scope (either the whole strategy or the {@link AcquireInitialTokenRequest#scope}) of the strategy is constrained
36-
* to target "resource", so that availability issues in one resource cannot delay other, unrelated resource's availability.
32+
* <li>When the availability of downstream resources are mostly affected by callers that are also using
33+
* the {@link AdaptiveRetryStrategy}.
34+
* <li>The scope (either the whole strategy or the {@link AcquireInitialTokenRequest#scope}) of the strategy is constrained
35+
* to target "resource", so that availability issues in one resource cannot delay other, unrelated resource's availability.
36+
* </ol>
3737
* <p>
3838
* The adaptive retry strategy by default:
3939
* <ol>
@@ -70,8 +70,6 @@ static AdaptiveRetryStrategy.Builder builder() {
7070
.tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE)
7171
.build())
7272
.tokenBucketExceptionCost(DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST)
73-
.backoffStrategy(BackoffStrategy.exponentialDelay(DefaultRetryStrategy.Standard.BASE_DELAY,
74-
DefaultRetryStrategy.Standard.MAX_BACKOFF))
7573
.rateLimiterTokenBucketStore(RateLimiterTokenBucketStore.builder().build());
7674
}
7775

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ public static StandardRetryStrategy doNotRetry() {
5353
public static StandardRetryStrategy.Builder standardStrategyBuilder() {
5454
return StandardRetryStrategy.builder()
5555
.maxAttempts(Standard.MAX_ATTEMPTS)
56-
.backoffStrategy(BackoffStrategy.exponentialDelay(Standard.BASE_DELAY, Standard.MAX_BACKOFF));
56+
.backoffStrategy(BackoffStrategy.exponentialDelay(Standard.BASE_DELAY, Standard.MAX_BACKOFF))
57+
.throttlingBackoffStrategy(BackoffStrategy.exponentialDelay(
58+
Standard.THROTTLED_BASE_DELAY,
59+
Standard.MAX_BACKOFF));
5760
}
5861

5962
/**
@@ -91,12 +94,18 @@ public static LegacyRetryStrategy.Builder legacyStrategyBuilder() {
9194
*/
9295
public static AdaptiveRetryStrategy.Builder adaptiveStrategyBuilder() {
9396
return AdaptiveRetryStrategy.builder()
94-
.maxAttempts(Adaptive.MAX_ATTEMPTS);
97+
.maxAttempts(Adaptive.MAX_ATTEMPTS)
98+
.backoffStrategy(BackoffStrategy.exponentialDelay(Standard.BASE_DELAY,
99+
Standard.MAX_BACKOFF))
100+
.throttlingBackoffStrategy(BackoffStrategy.exponentialDelay(
101+
Standard.THROTTLED_BASE_DELAY,
102+
Standard.MAX_BACKOFF));
95103
}
96104

97105
static final class Standard {
98106
static final int MAX_ATTEMPTS = 3;
99107
static final Duration BASE_DELAY = Duration.ofMillis(100);
108+
static final Duration THROTTLED_BASE_DELAY = Duration.ofSeconds(1);
100109
static final Duration MAX_BACKOFF = Duration.ofSeconds(20);
101110
static final int TOKEN_BUCKET_SIZE = 500;
102111
static final int DEFAULT_EXCEPTION_TOKEN_COST = 5;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ static Builder builder() {
7171

7272
interface Builder extends RetryStrategy.Builder<Builder, StandardRetryStrategy> {
7373
/**
74-
* Whether circuit breaking is enabled for this executor.
74+
* Whether circuit breaking is enabled for this strategy.
7575
*
7676
* <p>The circuit breaker will prevent attempts (even below the {@link #maxAttempts(int)}) if a large number of
7777
* failures are observed by this executor.

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public abstract class BaseRetryStrategy implements RetryStrategy {
5151
protected final int maxAttempts;
5252
protected final boolean circuitBreakerEnabled;
5353
protected final BackoffStrategy backoffStrategy;
54+
protected final BackoffStrategy throttlingBackoffStrategy;
55+
protected final Predicate<Throwable> treatAsThrottling;
5456
protected final int exceptionCost;
5557
protected final TokenBucketStore tokenBucketStore;
5658

@@ -60,6 +62,8 @@ public abstract class BaseRetryStrategy implements RetryStrategy {
6062
this.maxAttempts = Validate.isPositive(builder.maxAttempts, "maxAttempts");
6163
this.circuitBreakerEnabled = builder.circuitBreakerEnabled == null || builder.circuitBreakerEnabled;
6264
this.backoffStrategy = Validate.paramNotNull(builder.backoffStrategy, "backoffStrategy");
65+
this.throttlingBackoffStrategy = Validate.paramNotNull(builder.throttlingBackoffStrategy, "throttlingBackoffStrategy");
66+
this.treatAsThrottling = Validate.paramNotNull(builder.treatAsThrottling, "treatAsThrottling");
6367
this.exceptionCost = Validate.paramNotNull(builder.exceptionCost, "exceptionCost");
6468
this.tokenBucketStore = Validate.paramNotNull(builder.tokenBucketStore, "tokenBucketStore");
6569
}
@@ -149,7 +153,12 @@ protected Duration computeInitialBackoff(AcquireInitialTokenRequest request) {
149153
* compute different a different depending on their logic.
150154
*/
151155
protected Duration computeBackoff(RefreshRetryTokenRequest request, DefaultRetryToken token) {
152-
Duration backoff = backoffStrategy.computeDelay(token.attempt());
156+
Duration backoff;
157+
if (treatAsThrottling.test(request.failure())) {
158+
backoff = throttlingBackoffStrategy.computeDelay(token.attempt());
159+
} else {
160+
backoff = backoffStrategy.computeDelay(token.attempt());
161+
}
153162
Duration suggested = request.suggestedDelay().orElse(Duration.ZERO);
154163
return maxOf(suggested, backoff);
155164
}
@@ -340,6 +349,8 @@ static class Builder {
340349
private Boolean circuitBreakerEnabled;
341350
private Integer exceptionCost;
342351
private BackoffStrategy backoffStrategy;
352+
private BackoffStrategy throttlingBackoffStrategy;
353+
private Predicate<Throwable> treatAsThrottling = throwable -> false;
343354
private TokenBucketStore tokenBucketStore;
344355

345356
Builder() {
@@ -352,6 +363,8 @@ static class Builder {
352363
this.circuitBreakerEnabled = strategy.circuitBreakerEnabled;
353364
this.exceptionCost = strategy.exceptionCost;
354365
this.backoffStrategy = strategy.backoffStrategy;
366+
this.throttlingBackoffStrategy = strategy.throttlingBackoffStrategy;
367+
this.treatAsThrottling = strategy.treatAsThrottling;
355368
this.tokenBucketStore = strategy.tokenBucketStore;
356369
}
357370

@@ -375,6 +388,14 @@ void setBackoffStrategy(BackoffStrategy backoffStrategy) {
375388
this.backoffStrategy = backoffStrategy;
376389
}
377390

391+
void setThrottlingBackoffStrategy(BackoffStrategy throttlingBackoffStrategy) {
392+
this.throttlingBackoffStrategy = throttlingBackoffStrategy;
393+
}
394+
395+
void setTreatAsThrottling(Predicate<Throwable> treatAsThrottling) {
396+
this.treatAsThrottling = treatAsThrottling;
397+
}
398+
378399
void setTokenBucketExceptionCost(int exceptionCost) {
379400
this.exceptionCost = exceptionCost;
380401
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,10 @@ public final class DefaultAdaptiveRetryStrategy
3333
extends BaseRetryStrategy implements AdaptiveRetryStrategy {
3434

3535
private static final Logger LOG = Logger.loggerFor(DefaultAdaptiveRetryStrategy.class);
36-
private final Predicate<Throwable> treatAsThrottling;
3736
private final RateLimiterTokenBucketStore rateLimiterTokenBucketStore;
3837

3938
DefaultAdaptiveRetryStrategy(Builder builder) {
4039
super(LOG, builder);
41-
this.treatAsThrottling = Validate.paramNotNull(builder.treatAsThrottling, "treatAsThrottling");
4240
this.rateLimiterTokenBucketStore = Validate.paramNotNull(builder.rateLimiterTokenBucketStore,
4341
"rateLimiterTokenBucketStore");
4442
}
@@ -81,15 +79,13 @@ public static Builder builder() {
8179
}
8280

8381
public static class Builder extends BaseRetryStrategy.Builder implements AdaptiveRetryStrategy.Builder {
84-
private Predicate<Throwable> treatAsThrottling;
8582
private RateLimiterTokenBucketStore rateLimiterTokenBucketStore;
8683

8784
Builder() {
8885
}
8986

9087
Builder(DefaultAdaptiveRetryStrategy strategy) {
9188
super(strategy);
92-
this.treatAsThrottling = strategy.treatAsThrottling;
9389
this.rateLimiterTokenBucketStore = strategy.rateLimiterTokenBucketStore;
9490
}
9591

@@ -107,7 +103,7 @@ public Builder maxAttempts(int maxAttempts) {
107103

108104
@Override
109105
public Builder treatAsThrottling(Predicate<Throwable> treatAsThrottling) {
110-
this.treatAsThrottling = treatAsThrottling;
106+
setTreatAsThrottling(treatAsThrottling);
111107
return this;
112108
}
113109

@@ -117,6 +113,12 @@ public Builder backoffStrategy(BackoffStrategy backoffStrategy) {
117113
return this;
118114
}
119115

116+
@Override
117+
public Builder throttlingBackoffStrategy(BackoffStrategy backoffStrategy) {
118+
setThrottlingBackoffStrategy(backoffStrategy);
119+
return this;
120+
}
121+
120122
public Builder circuitBreakerEnabled(Boolean circuitBreakerEnabled) {
121123
setCircuitBreakerEnabled(circuitBreakerEnabled);
122124
return this;

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

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

1616
package software.amazon.awssdk.retries.internal;
1717

18-
import java.time.Duration;
1918
import java.util.function.Predicate;
2019
import software.amazon.awssdk.annotations.SdkInternalApi;
2120
import software.amazon.awssdk.retries.LegacyRetryStrategy;
@@ -29,28 +28,11 @@
2928
public final class DefaultLegacyRetryStrategy
3029
extends BaseRetryStrategy implements LegacyRetryStrategy {
3130
private static final Logger LOG = Logger.loggerFor(LegacyRetryStrategy.class);
32-
private final BackoffStrategy throttlingBackoffStrategy;
3331
private final int throttlingExceptionCost;
34-
private final Predicate<Throwable> treatAsThrottling;
3532

3633
DefaultLegacyRetryStrategy(Builder builder) {
3734
super(LOG, builder);
3835
this.throttlingExceptionCost = Validate.paramNotNull(builder.throttlingExceptionCost, "throttlingExceptionCost");
39-
this.throttlingBackoffStrategy = Validate.paramNotNull(builder.throttlingBackoffStrategy, "throttlingBackoffStrategy");
40-
this.treatAsThrottling = Validate.paramNotNull(builder.treatAsThrottling, "treatAsThrottling");
41-
}
42-
43-
@Override
44-
protected Duration computeBackoff(RefreshRetryTokenRequest request, DefaultRetryToken token) {
45-
Duration backoff;
46-
if (treatAsThrottling.test(request.failure())) {
47-
backoff = throttlingBackoffStrategy.computeDelay(token.attempt());
48-
} else {
49-
backoff = backoffStrategy.computeDelay(token.attempt());
50-
}
51-
// Take the max delay between the suggested delay and the backoff delay.
52-
Duration suggested = request.suggestedDelay().orElse(Duration.ZERO);
53-
return maxOf(suggested, backoff);
5436
}
5537

5638
@Override
@@ -74,17 +56,13 @@ public static Builder builder() {
7456
}
7557

7658
public static class Builder extends BaseRetryStrategy.Builder implements LegacyRetryStrategy.Builder {
77-
private BackoffStrategy throttlingBackoffStrategy;
7859
private Integer throttlingExceptionCost;
79-
private Predicate<Throwable> treatAsThrottling;
8060

8161
Builder() {
8262
}
8363

8464
Builder(DefaultLegacyRetryStrategy strategy) {
8565
super(strategy);
86-
this.throttlingBackoffStrategy = strategy.throttlingBackoffStrategy;
87-
this.treatAsThrottling = strategy.treatAsThrottling;
8866
this.throttlingExceptionCost = strategy.throttlingExceptionCost;
8967
}
9068

@@ -108,7 +86,7 @@ public Builder backoffStrategy(BackoffStrategy backoffStrategy) {
10886

10987
@Override
11088
public Builder throttlingBackoffStrategy(BackoffStrategy throttlingBackoffStrategy) {
111-
this.throttlingBackoffStrategy = throttlingBackoffStrategy;
89+
setThrottlingBackoffStrategy(throttlingBackoffStrategy);
11290
return this;
11391
}
11492

@@ -120,7 +98,7 @@ public Builder circuitBreakerEnabled(Boolean circuitBreakerEnabled) {
12098

12199
@Override
122100
public Builder treatAsThrottling(Predicate<Throwable> treatAsThrottling) {
123-
this.treatAsThrottling = treatAsThrottling;
101+
setTreatAsThrottling(treatAsThrottling);
124102
return this;
125103
}
126104

0 commit comments

Comments
 (0)