Skip to content

Commit

Permalink
Add default backoff strategies (#3906)
Browse files Browse the repository at this point in the history
* Add default backoff strategies

* Moved the backoff strategires to the SPI package

* Use AssertJ instead of Hamcrest
  • Loading branch information
sugmanue committed Apr 19, 2023
1 parent 7ef8830 commit f40dd27
Show file tree
Hide file tree
Showing 12 changed files with 794 additions and 6 deletions.
6 changes: 3 additions & 3 deletions core/retries-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<parent>
<artifactId>core</artifactId>
<groupId>software.amazon.awssdk</groupId>
<version>2.20.4-SNAPSHOT</version>
<version>2.20.7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

Expand Down Expand Up @@ -60,8 +60,8 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@
package software.amazon.awssdk.retries.api;

import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.retries.api.internal.backoff.ExponentialDelayWithJitter;
import software.amazon.awssdk.retries.api.internal.backoff.ExponentialDelayWithoutJitter;
import software.amazon.awssdk.retries.api.internal.backoff.FixedDelayWithJitter;
import software.amazon.awssdk.retries.api.internal.backoff.FixedDelayWithoutJitter;
import software.amazon.awssdk.retries.api.internal.backoff.Immediately;

/**
* Determines how long to wait before each execution attempt.
Expand All @@ -33,4 +39,47 @@ public interface BackoffStrategy {
* @throws IllegalArgumentException If the given attempt is less or equal to zero.
*/
Duration computeDelay(int attempt);

/**
* Do not back off: retry immediately.
*/
static BackoffStrategy retryImmediately() {
return new Immediately();
}

/**
* Wait for a random period of time between 0ms and the provided delay.
*/
static BackoffStrategy fixedDelay(Duration delay) {
return new FixedDelayWithJitter(ThreadLocalRandom::current, delay);
}

/**
* Wait for a period of time equal to the provided delay.
*/
static BackoffStrategy fixedDelayWithoutJitter(Duration delay) {
return new FixedDelayWithoutJitter(delay);
}

/**
* Wait for a random period of time between 0ms and an exponentially increasing amount of time between each subsequent attempt
* of the same call.
*
* <p>Specifically, the first attempt waits 0ms, and each subsequent attempt waits between
* 0ms and {@code min(maxDelay, baseDelay * (1 << (attempt - 2)))}.
*/
static BackoffStrategy exponentialDelay(Duration baseDelay, Duration maxDelay) {
return new ExponentialDelayWithJitter(ThreadLocalRandom::current, baseDelay, maxDelay);
}

/**
* Wait for an exponentially increasing amount of time between each subsequent attempt of the same call.
*
* <p>Specifically, the first attempt waits 0ms, and each subsequent attempt waits for
* {@code min(maxDelay, baseDelay * (1 << (attempt - 2)))}.
*/
static BackoffStrategy exponentialDelayWithoutJitter(Duration baseDelay, Duration maxDelay) {
return new ExponentialDelayWithoutJitter(baseDelay, maxDelay);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.api.internal.backoff;

import java.time.Duration;
import software.amazon.awssdk.annotations.SdkInternalApi;

/**
* Constants and utility functions shared by the BackoffStrategy implementations.
*/
@SdkInternalApi
class BackoffStrategiesConstants {
static final Duration BASE_DELAY_CEILING = Duration.ofMillis(Integer.MAX_VALUE); // Around ~24.8 days
static final Duration MAX_BACKOFF_CEILING = Duration.ofMillis(Integer.MAX_VALUE); // Around ~24.8 days
/**
* Max permitted retry times. To prevent exponentialDelay from overflow, there must be 2 ^ retriesAttempted &lt;= 2 ^ 31 - 1,
* which means retriesAttempted &lt;= 30, so that is the ceil for retriesAttempted.
*/
static final int RETRIES_ATTEMPTED_CEILING = (int) Math.floor(Math.log(Integer.MAX_VALUE) / Math.log(2));

private BackoffStrategiesConstants() {
}

/**
* Returns the computed exponential delay in milliseconds given the retries attempted, the base delay and the max backoff
* time.
*
* <p>Specifically it returns {@code min(maxDelay, baseDelay * (1 << (attempt - 2)))}. To prevent overflowing the attempts
* get capped to 30.
*/
static int calculateExponentialDelay(int retriesAttempted, Duration baseDelay, Duration maxBackoffTime) {
int cappedRetries = Math.min(retriesAttempted, BackoffStrategiesConstants.RETRIES_ATTEMPTED_CEILING);
return (int) Math.min(baseDelay.multipliedBy(1L << (cappedRetries - 2)).toMillis(), maxBackoffTime.toMillis());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.api.internal.backoff;

import static software.amazon.awssdk.retries.api.internal.backoff.BackoffStrategiesConstants.calculateExponentialDelay;

import java.time.Duration;
import java.util.Random;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.utils.NumericUtils;
import software.amazon.awssdk.utils.ToString;
import software.amazon.awssdk.utils.Validate;

/**
* Strategy that waits for a random period of time between 0ms and an exponentially increasing amount of time between each
* subsequent attempt of the same call.
*
* <p>Specifically, the first attempt waits 0ms, and each subsequent attempt waits between
* 0ms and {@code min(maxDelay, baseDelay * (1 << (attempt - 2)))}.
*/
@SdkInternalApi
public final class ExponentialDelayWithJitter implements BackoffStrategy {
private final Supplier<Random> randomSupplier;
private final Duration baseDelay;
private final Duration maxDelay;

public ExponentialDelayWithJitter(Supplier<Random> randomSupplier, Duration baseDelay, Duration maxDelay) {
this.randomSupplier = Validate.paramNotNull(randomSupplier, "random");
this.baseDelay = NumericUtils.min(Validate.isPositive(baseDelay, "baseDelay"),
BackoffStrategiesConstants.BASE_DELAY_CEILING);
this.maxDelay = NumericUtils.min(Validate.isPositive(maxDelay, "maxDelay"),
BackoffStrategiesConstants.MAX_BACKOFF_CEILING);
}

@Override
public Duration computeDelay(int attempt) {
Validate.isPositive(attempt, "attempt");
if (attempt == 1) {
return Duration.ZERO;
}
int delay = calculateExponentialDelay(attempt, baseDelay, maxDelay);
int randInt = randomSupplier.get().nextInt(delay);
return Duration.ofMillis(randInt);
}

@Override
public String toString() {
return ToString.builder("ExponentialDelayWithJitter")
.add("baseDelay", baseDelay)
.add("maxDelay", maxDelay)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.api.internal.backoff;

import static software.amazon.awssdk.retries.api.internal.backoff.BackoffStrategiesConstants.calculateExponentialDelay;

import java.time.Duration;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.utils.NumericUtils;
import software.amazon.awssdk.utils.ToString;
import software.amazon.awssdk.utils.Validate;

/**
* Strategy that waits for an exponentially increasing amount of time between each subsequent attempt of the same call.
*
* <p>Specifically, the first attempt waits 0ms, and each subsequent attempt waits for
* {@code min(maxDelay, baseDelay * (1 << (attempt - 2)))}.
*/
@SdkInternalApi
public final class ExponentialDelayWithoutJitter implements BackoffStrategy {
private final Duration baseDelay;
private final Duration maxDelay;

public ExponentialDelayWithoutJitter(Duration baseDelay, Duration maxDelay) {
this.baseDelay = NumericUtils.min(Validate.isPositive(baseDelay, "baseDelay"),
BackoffStrategiesConstants.BASE_DELAY_CEILING);
this.maxDelay = NumericUtils.min(Validate.isPositive(maxDelay, "maxDelay"),
BackoffStrategiesConstants.MAX_BACKOFF_CEILING);
}

@Override
public Duration computeDelay(int attempt) {
Validate.isPositive(attempt, "attempt");
if (attempt == 1) {
return Duration.ZERO;
}
int delay = calculateExponentialDelay(attempt, baseDelay, maxDelay);
return Duration.ofMillis(delay);
}

@Override
public String toString() {
return ToString.builder("ExponentialDelayWithoutJitter")
.add("baseDelay", baseDelay)
.add("maxDelay", maxDelay)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.api.internal.backoff;

import java.time.Duration;
import java.util.Random;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.utils.NumericUtils;
import software.amazon.awssdk.utils.ToString;
import software.amazon.awssdk.utils.Validate;

/**
* Strategy that waits for a random period of time between 0ms and the provided delay.
*/
@SdkInternalApi
public final class FixedDelayWithJitter implements BackoffStrategy {
private final Supplier<Random> randomSupplier;
private final Duration delay;

public FixedDelayWithJitter(Supplier<Random> randomSupplier, Duration delay) {
this.randomSupplier = Validate.paramNotNull(randomSupplier, "random");
this.delay = NumericUtils.min(Validate.isPositive(delay, "delay"), BackoffStrategiesConstants.BASE_DELAY_CEILING);
}

@Override
public Duration computeDelay(int attempt) {
Validate.isPositive(attempt, "attempt");
return Duration.ofMillis(randomSupplier.get().nextInt((int) delay.toMillis()));
}

@Override
public String toString() {
return ToString.builder("FixedDelayWithJitter")
.add("delay", delay)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.api.internal.backoff;

import java.time.Duration;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.retries.api.BackoffStrategy;
import software.amazon.awssdk.utils.ToString;
import software.amazon.awssdk.utils.Validate;

/**
* Strategy that waits for a period of time equal to the provided delay.
*/
@SdkInternalApi
public final class FixedDelayWithoutJitter implements BackoffStrategy {
private final Duration delay;

public FixedDelayWithoutJitter(Duration delay) {
this.delay = Validate.isPositive(delay, "delay");
}

@Override
public Duration computeDelay(int attempt) {
Validate.isPositive(attempt, "attempt");
return delay;
}

@Override
public String toString() {
return ToString.builder("FixedDelayWithoutJitter")
.add("delay", delay)
.build();
}
}
Loading

0 comments on commit f40dd27

Please sign in to comment.