Skip to content
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 @@ -18,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.retries.api.AcquireInitialTokenResponse;
import software.amazon.awssdk.retries.api.BackoffStrategy;
Expand Down Expand Up @@ -82,24 +83,33 @@ void v20_throttlingRetry_deducts5Tokens() {

@Test
void v21Enabled_backoffUses50msBaseDelay() {
RetryStrategy strategy = StandardRetryStrategy.builder(true)
.retryOnException(t -> true)
.build();

RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err"));
// First retry delay should be in [0, 50ms] (jittered)
assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(50));
Supplier<RetryStrategy> stratSupplier = () -> StandardRetryStrategy.builder(true)
.retryOnException(t -> true)
.build();

probabilisticAssertDelayBetween(stratSupplier, new RuntimeException("err"), Duration.ZERO, Duration.ofMillis(50));
}

@Test
void v21Enabled_throttlingBackoffUses1000msBaseDelay() {

Supplier<RetryStrategy> stratSupplier = () -> StandardRetryStrategy.builder(true)
.retryOnException(t -> true)
.treatAsThrottling(t -> true)
.build();

probabilisticAssertDelayBetween(stratSupplier, new RuntimeException("err"), Duration.ZERO, Duration.ofMillis(1000));
}

@Test
void v20_backoffUses100msBaseDelay() {
RetryStrategy strategy = StandardRetryStrategy.builder(false)
.retryOnException(t -> true)
.build();
Supplier<RetryStrategy> stratSupplier = () -> StandardRetryStrategy.builder(false)
.retryOnException(t -> true)
.build();

probabilisticAssertDelayBetween(stratSupplier, new RuntimeException("err"), Duration.ZERO, Duration.ofMillis(100));

RefreshRetryTokenResponse response = refreshToken(strategy, new RuntimeException("err"));
// First retry delay should be in [0, 100ms] (jittered)
assertThat(response.delay()).isBetween(Duration.ZERO, Duration.ofMillis(100));
}

@Test
Expand Down Expand Up @@ -135,4 +145,15 @@ private RefreshRetryTokenResponse refreshToken(RetryStrategy strategy, Exception
return strategy.refreshRetryToken(
RefreshRetryTokenRequest.builder().token(initial.token()).failure(failure).build());
}

// Backoffs are jittered, so verify it by testing it many times
private void probabilisticAssertDelayBetween(Supplier<RetryStrategy> strategy,
Exception failure,
Duration min,
Duration max) {
for (int i = 0; i < 128; ++i) {
RefreshRetryTokenResponse response = refreshToken(strategy.get(), failure);
assertThat(response.delay()).isBetween(min, max);
}
}
}
6 changes: 6 additions & 0 deletions services/dynamodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,11 @@
<artifactId>http-auth-aws</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>retries</artifactId>
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import software.amazon.awssdk.core.client.config.SdkClientOption;
import software.amazon.awssdk.core.internal.retry.RetryPolicyAdapter;
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting;
import software.amazon.awssdk.core.retry.NewRetries2026Resolver;
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.retry.RetryPolicy;
import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
Expand All @@ -41,12 +42,17 @@ final class DynamoDbRetryPolicy {
/**
* Default max retry count for DynamoDB client, regardless of retry mode.
**/
private static final int MAX_ERROR_RETRY = 8;
private static final int MAX_ERROR_RETRY_V20 = 8;

/**
* Default attempts count for DynamoDB client, regardless of retry mode.
**/
private static final int MAX_ATTEMPTS = MAX_ERROR_RETRY + 1;
private static final int MAX_ATTEMPTS_V20 = MAX_ERROR_RETRY_V20 + 1;

/**
* Default attempts count for DynamoDB client, regardless of retry mode. (v2.1)
**/
private static final int MAX_ATTEMPTS_V21 = 4;

/**
* Default base sleep time for DynamoDB, regardless of retry mode.
Expand Down Expand Up @@ -95,9 +101,12 @@ public static RetryStrategy resolveRetryStrategy(SdkClientConfiguration config)
.build();
}

return AwsRetryStrategy.forRetryMode(retryMode)
boolean newRetries2026Enabled = isNewRetries2026Enabled(config);
int maxAttempts = newRetries2026Enabled ? MAX_ATTEMPTS_V21 : MAX_ATTEMPTS_V20;

return AwsRetryStrategy.forRetryMode(retryMode, newRetries2026Enabled)
.toBuilder()
.maxAttempts(MAX_ATTEMPTS)
.maxAttempts(maxAttempts)
.backoffStrategy(exponentialDelay(BASE_DELAY, SdkDefaultRetrySetting.MAX_BACKOFF))
.build();
}
Expand All @@ -106,7 +115,7 @@ private static RetryPolicy retryPolicyFor(RetryMode retryMode) {
return AwsRetryPolicy.forRetryMode(retryMode)
.toBuilder()
.additionalRetryConditionsAllowed(false)
.numRetries(MAX_ERROR_RETRY)
.numRetries(MAX_ERROR_RETRY_V20)
.backoffStrategy(BACKOFF_STRATEGY)
.build();
}
Expand All @@ -116,6 +125,12 @@ private static RetryMode resolveRetryMode(SdkClientConfiguration config) {
.profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
.profileName(config.option(SdkClientOption.PROFILE_NAME))
.defaultRetryMode(config.option(SdkClientOption.DEFAULT_RETRY_MODE))
.defaultNewRetries2026(config.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026))
.resolve();
}

private static boolean isNewRetries2026Enabled(SdkClientConfiguration config) {
Boolean defaultNewRetries2026 = config.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026);
return new NewRetries2026Resolver().defaultNewRetries2026(defaultNewRetries2026).resolve();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
import software.amazon.awssdk.core.client.config.SdkClientOption;
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.retries.LegacyRetryStrategy;
import software.amazon.awssdk.retries.StandardRetryStrategy;
import software.amazon.awssdk.retries.api.RetryStrategy;
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
import software.amazon.awssdk.utils.StringInputStream;
Expand Down Expand Up @@ -126,4 +130,20 @@ void resolve_retryModeNotSetWithEnvNorSupplier_resolvesFromSdkDefault() {
assertThat(retryMode).isEqualTo(RetryMode.LEGACY);
}

@ParameterizedTest(name = "V2.1 retries = {0}, max attempts = {1}")
@CsvSource({
"false,9",
"true,4"
})
void resolve_maxAttemptsAndStrategyCompliantWithRetriesVersion(boolean retries21, int expectedMaxAttempts) {
SdkClientConfiguration cfg = SdkClientConfiguration.builder()
.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026, retries21)
.build();
RetryStrategy strategy = DynamoDbRetryPolicy.resolveRetryStrategy(cfg);

Class<?> expectedStrategy = retries21 ? StandardRetryStrategy.class : LegacyRetryStrategy.class;

assertThat(strategy).isInstanceOf(expectedStrategy);
assertThat(strategy.maxAttempts()).isEqualTo(expectedMaxAttempts);
}
}
Loading