Skip to content

Add legacy retry strategy #3988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import static org.assertj.core.api.Assertions.withinPercentage;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
Expand Down Expand Up @@ -63,7 +62,7 @@ void seemsToBeCorrectAndThreadSafe() {
ExecutorService executor = Executors.newFixedThreadPool(parallelism);
Server server = new Server(serverWorkers, executor);
RateLimiterTokenBucketStore store = RateLimiterTokenBucketStore.builder().build();
AdaptiveRetryStrategy strategy = AdaptiveRetryStrategyImpl
AdaptiveRetryStrategy strategy = DefaultAdaptiveRetryStrategy
.builder()
// We don't care about how many attempts we allow to, that logic is tested somewhere else.
// so we give the strategy plenty of room for retries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import software.amazon.awssdk.retries.api.AcquireInitialTokenRequest;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.retries.api.RetryStrategy;
import software.amazon.awssdk.retries.internal.AdaptiveRetryStrategyImpl;
import software.amazon.awssdk.retries.internal.DefaultAdaptiveRetryStrategy;
import software.amazon.awssdk.retries.internal.circuitbreaker.TokenBucketStore;
import software.amazon.awssdk.retries.internal.ratelimiter.RateLimiterTokenBucketStore;

Expand Down Expand Up @@ -63,9 +63,9 @@ public interface AdaptiveRetryStrategy extends RetryStrategy<AdaptiveRetryStrate
* </pre>
*/
static AdaptiveRetryStrategy.Builder builder() {
return AdaptiveRetryStrategyImpl
return DefaultAdaptiveRetryStrategy
.builder()
.maxAttempts(DefaultRetryStrategy.Standard.MAX_ATTEMPTS)
.maxAttempts(DefaultRetryStrategy.Adaptive.MAX_ATTEMPTS)
.tokenBucketStore(TokenBucketStore.builder()
.tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE)
.build())
Expand All @@ -84,7 +84,6 @@ interface Builder extends RetryStrategy.Builder<Builder, AdaptiveRetryStrategy>
*/
Builder treatAsThrottling(Predicate<Throwable> treatAsThrottling);


@Override
AdaptiveRetryStrategy build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ private DefaultRetryStrategy() {
}

/**
* Create a new builder for a {@code StandardRetryStrategy}.
* Create a new builder for a {@link StandardRetryStrategy}.
*
* <p>Example Usage
* <pre>
* {@snippet
* StandardRetryStrategy retryStrategy =
* RetryStrategies.adaptiveStrategyBuilder()
* DefaultRetryStrategy.standardStrategyBuilder()
* .retryOnExceptionInstanceOf(IllegalArgumentException.class)
* .retryOnExceptionInstanceOf(IllegalStateException.class)
* .build();
* </pre>
* }
*/
public static StandardRetryStrategy.Builder standardStrategyBuilder() {
return StandardRetryStrategy.builder()
Expand All @@ -48,18 +48,38 @@ public static StandardRetryStrategy.Builder standardStrategyBuilder() {
}

/**
* Create a new builder for a {@code AdaptiveRetryStrategy}.
* Create a new builder for a {@link LegacyRetryStrategy}.
*
* <p>Example Usage
* <pre>
* {@snippet
* LegacyRetryStrategy retryStrategy =
* DefaultRetryStrategy.legacyStrategyBuilder()
* .retryOnExceptionInstanceOf(IllegalArgumentException.class)
* .retryOnExceptionInstanceOf(IllegalStateException.class)
* .build();
* }
*/
public static LegacyRetryStrategy.Builder legacyStrategyBuilder() {
return LegacyRetryStrategy.builder()
.maxAttempts(Legacy.MAX_ATTEMPTS)
.backoffStrategy(BackoffStrategy.exponentialDelay(Legacy.BASE_DELAY, Legacy.MAX_BACKOFF))
.throttlingBackoffStrategy(BackoffStrategy.exponentialDelay(Legacy.THROTTLED_BASE_DELAY,
Legacy.MAX_BACKOFF));
}

/**
* Create a new builder for a {@link AdaptiveRetryStrategy}.
*
* <p>Example Usage
* {@snippet
* AdaptiveRetryStrategy retryStrategy =
* RetryStrategies.adaptiveStrategyBuilder()
* DefaultRetryStrategy.adaptiveStrategyBuilder()
* .retryOnExceptionInstanceOf(IllegalArgumentException.class)
* .retryOnExceptionInstanceOf(IllegalStateException.class)
* .build();
* </pre>
* }
*/
public static AdaptiveRetryStrategy.Builder adaptiveRetryStrategyBuilder() {
public static AdaptiveRetryStrategy.Builder adaptiveStrategyBuilder() {
return AdaptiveRetryStrategy.builder()
.maxAttempts(Adaptive.MAX_ATTEMPTS);
}
Expand All @@ -70,9 +90,29 @@ static final class Standard {
static final Duration MAX_BACKOFF = Duration.ofSeconds(20);
static final int TOKEN_BUCKET_SIZE = 500;
static final int DEFAULT_EXCEPTION_TOKEN_COST = 5;

private Standard() {
}
}

static final class Adaptive {
static final int MAX_ATTEMPTS = 3;

private Adaptive() {
}
}

static final class Legacy {
static final int MAX_ATTEMPTS = 4;
static final Duration BASE_DELAY = Duration.ofMillis(100);
static final Duration THROTTLED_BASE_DELAY = Duration.ofMillis(500);

static final Duration MAX_BACKOFF = Duration.ofSeconds(20);
static final int TOKEN_BUCKET_SIZE = 500;
static final int DEFAULT_EXCEPTION_TOKEN_COST = 5;
static final int THROTTLE_EXCEPTION_TOKEN_COST = 0;

private Legacy() {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.retries;

import java.util.function.Predicate;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.retries.api.RetryStrategy;
import software.amazon.awssdk.retries.internal.DefaultLegacyRetryStrategy;
import software.amazon.awssdk.retries.internal.circuitbreaker.TokenBucketStore;

/**
* The legacy retry strategy is a {@link RetryStrategy} for normal use-cases.
* <p>
* The legacy retry strategy by default:
* <ol>
* <li>Retries on the conditions configured in the {@link Builder}.
* <li>Retries 3 times (4 total attempts). Adjust with {@link Builder#maxAttempts(int)}
* <li>For non-throttling exceptions uses the {@link BackoffStrategy#exponentialDelay} backoff strategy, with a base delay
* of 100 milliseconds and max delay of 20 seconds. Adjust with {@link Builder#backoffStrategy}
* <li>For throttling exceptions uses the {@link BackoffStrategy#exponentialDelay} backoff strategy, with a base delay of
* 500 milliseconds and max delay of 20 seconds. Adjust with {@link LegacyRetryStrategy.Builder#throttlingBackoffStrategy}
* <li>Circuit breaking (disabling retries) in the event of high downstream failures across the scope of
* the strategy. The circuit breaking will never prevent a successful first attempt. Adjust with
* {@link Builder#circuitBreakerEnabled}
* <li>The state of the circuit breaker is not affected by throttling exceptions
* </ol>
*
* @see StandardRetryStrategy
* @see AdaptiveRetryStrategy
*/
@SdkPublicApi
@ThreadSafe
public interface LegacyRetryStrategy extends RetryStrategy<LegacyRetryStrategy.Builder, LegacyRetryStrategy> {
/**
* Create a new {@link LegacyRetryStrategy.Builder}.
*
* <p>Example Usage
* <pre>
* LegacyRetryStrategy retryStrategy =
* LegacyRetryStrategy.builder()
* .retryOnExceptionInstanceOf(IllegalArgumentException.class)
* .retryOnExceptionInstanceOf(IllegalStateException.class)
* .build();
* </pre>
*/
static Builder builder() {
return DefaultLegacyRetryStrategy
.builder()
.maxAttempts(DefaultRetryStrategy.Legacy.MAX_ATTEMPTS)
.tokenBucketStore(TokenBucketStore
.builder()
.tokenBucketMaxCapacity(DefaultRetryStrategy.Legacy.TOKEN_BUCKET_SIZE)
.build())
.tokenBucketExceptionCost(DefaultRetryStrategy.Legacy.DEFAULT_EXCEPTION_TOKEN_COST)
.tokenBucketThrottlingExceptionCost(DefaultRetryStrategy.Legacy.THROTTLE_EXCEPTION_TOKEN_COST);
}

@Override
Builder toBuilder();

interface Builder extends RetryStrategy.Builder<Builder, LegacyRetryStrategy> {
/**
* Configure the backoff strategy used by this strategy.
*
* <p>By default, this uses jittered exponential backoff.
*/
Builder backoffStrategy(BackoffStrategy backoffStrategy);

/**
* Configure the backoff strategy used for throttling exceptions by this strategy.
*
* <p>By default, this uses jittered exponential backoff.
*/
Builder throttlingBackoffStrategy(BackoffStrategy throttlingBackoffStrategy);

/**
* Whether circuit breaking is enabled for this strategy.
*
* <p>The circuit breaker will prevent attempts (even below the {@link #maxAttempts(int)}) if a large number of
* failures are observed by this executor.
*
* <p>Note: The circuit breaker scope is local to the created {@link RetryStrategy},
* and will therefore not be effective unless the {@link RetryStrategy} is used for more than one call. It's recommended
* that a {@link RetryStrategy} be reused for all calls to a single unreliable resource. It's also recommended that
* separate {@link RetryStrategy}s be used for calls to unrelated resources.
*
* <p>By default, this is {@code true}.
*/
Builder circuitBreakerEnabled(Boolean circuitBreakerEnabled);

/**
* Configure the predicate to allow the strategy categorize a Throwable as throttling exception.
*/
Builder treatAsThrottling(Predicate<Throwable> treatAsThrottling);

@Override
LegacyRetryStrategy build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.retries.api.RetryStrategy;
import software.amazon.awssdk.retries.internal.StandardRetryStrategyImpl;
import software.amazon.awssdk.retries.internal.DefaultStandardRetryStrategy;
import software.amazon.awssdk.retries.internal.circuitbreaker.TokenBucketStore;

/**
Expand Down Expand Up @@ -56,14 +56,14 @@ public interface StandardRetryStrategy extends RetryStrategy<StandardRetryStrate
* </pre>
*/
static Builder builder() {
return StandardRetryStrategyImpl
return DefaultStandardRetryStrategy
.builder()
.maxAttempts(DefaultRetryStrategy.Standard.MAX_ATTEMPTS)
.tokenBucketStore(TokenBucketStore
.builder()
.tokenBucketMaxCapacity(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE)
.build())
.tokenBucketExceptionCost(DefaultRetryStrategy.Standard.TOKEN_BUCKET_SIZE);
.tokenBucketExceptionCost(DefaultRetryStrategy.Standard.DEFAULT_EXCEPTION_TOKEN_COST);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
* Implementation of the {@link AdaptiveRetryStrategy} interface.
*/
@SdkInternalApi
public final class AdaptiveRetryStrategyImpl implements AdaptiveRetryStrategy {
private static final Logger LOG = Logger.loggerFor(AdaptiveRetryStrategyImpl.class);
public final class DefaultAdaptiveRetryStrategy implements AdaptiveRetryStrategy {
private static final Logger LOG = Logger.loggerFor(DefaultAdaptiveRetryStrategy.class);
private final List<Predicate<Throwable>> retryPredicates;
private final int maxAttempts;
private final boolean circuitBreakerEnabled;
Expand All @@ -59,10 +59,10 @@ public final class AdaptiveRetryStrategyImpl implements AdaptiveRetryStrategy {
private final TokenBucketStore tokenBucketStore;
private final RateLimiterTokenBucketStore rateLimiterTokenBucketStore;

private AdaptiveRetryStrategyImpl(Builder builder) {
private DefaultAdaptiveRetryStrategy(Builder builder) {
this.retryPredicates = Collections.unmodifiableList(Validate.paramNotNull(builder.retryPredicates, "retryPredicates"));
this.maxAttempts = Validate.isPositive(builder.maxAttempts, "maxAttempts");
this.circuitBreakerEnabled = builder.circuitBreakerEnabled;
this.circuitBreakerEnabled = builder.circuitBreakerEnabled == null || builder.circuitBreakerEnabled;
this.backoffStrategy = Validate.paramNotNull(builder.backoffStrategy, "backoffStrategy");
this.exceptionCost = builder.exceptionCost;
this.tokenBucketMaxCapacity = builder.tokenBucketMaxCapacity;
Expand Down Expand Up @@ -293,6 +293,9 @@ static DefaultRetryToken asStandardRetryToken(RetryToken token) {

private AcquireResponse requestAcquireCapacity(DefaultRetryToken token) {
TokenBucket tokenBucket = tokenBucketStore.tokenBucketForScope(token.scope());
if (!circuitBreakerEnabled) {
return tokenBucket.tryAcquire(0);
}
return tokenBucket.tryAcquire(exceptionCost);
}

Expand All @@ -310,7 +313,7 @@ private DefaultRetryToken refreshToken(RefreshRetryTokenRequest request, Acquire
public static class Builder implements AdaptiveRetryStrategy.Builder {
private List<Predicate<Throwable>> retryPredicates;
private int maxAttempts;
private boolean circuitBreakerEnabled;
private Boolean circuitBreakerEnabled;
private int tokenBucketMaxCapacity;
private int exceptionCost;
private Predicate<Throwable> treatAsThrottling;
Expand All @@ -322,7 +325,7 @@ public static class Builder implements AdaptiveRetryStrategy.Builder {
retryPredicates = new ArrayList<>();
}

Builder(AdaptiveRetryStrategyImpl strategy) {
Builder(DefaultAdaptiveRetryStrategy strategy) {
this.retryPredicates = new ArrayList<>(strategy.retryPredicates);
this.maxAttempts = strategy.maxAttempts;
this.circuitBreakerEnabled = strategy.circuitBreakerEnabled;
Expand Down Expand Up @@ -372,9 +375,14 @@ public Builder tokenBucketExceptionCost(int exceptionCost) {
return this;
}

public Builder circuitBreakerEnabled(Boolean circuitBreakerEnabled) {
this.circuitBreakerEnabled = circuitBreakerEnabled;
return this;
}

@Override
public AdaptiveRetryStrategyImpl build() {
return new AdaptiveRetryStrategyImpl(this);
public DefaultAdaptiveRetryStrategy build() {
return new DefaultAdaptiveRetryStrategy(this);
}
}
}
Loading