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 @@ -437,7 +437,13 @@ private void configureRetryPolicy(SdkClientConfiguration.Builder config) {
private void configureRetryStrategy(SdkClientConfiguration.Builder config) {
RetryStrategy strategy = config.option(SdkClientOption.RETRY_STRATEGY);
if (strategy == null) {
config.lazyOption(SdkClientOption.RETRY_STRATEGY, this::resolveAwsRetryStrategy);
Boolean defaultNewRetries2026 = config.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026);

config.lazyOption(SdkClientOption.RETRY_STRATEGY, src -> resolveAwsRetryStrategy(src, defaultNewRetries2026));

config.option(SdkClientOption.NEW_RETRIES_2026_ENABLED,
new NewRetries2026Resolver().defaultNewRetries2026(defaultNewRetries2026).resolve());

return;
}

Expand All @@ -457,9 +463,7 @@ private void configureRetryStrategy(SdkClientConfiguration.Builder config) {

}

private RetryStrategy resolveAwsRetryStrategy(LazyValueSource config) {
Boolean defaultNewRetries2026 = config.get(SdkClientOption.DEFAULT_NEW_RETRIES_2026);

private RetryStrategy resolveAwsRetryStrategy(LazyValueSource config, Boolean defaultNewRetries2026) {
RetryMode retryMode = RetryMode.resolver()
.profileFile(config.get(SdkClientOption.PROFILE_FILE_SUPPLIER))
.profileName(config.get(SdkClientOption.PROFILE_NAME))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static software.amazon.awssdk.auth.signer.internal.util.SignerMethodResolver.resolveSigningMethodUsed;
import static software.amazon.awssdk.awscore.internal.AwsServiceProtocol.SMITHY_RPC_V2_CBOR;
import static software.amazon.awssdk.core.client.config.SdkClientOption.NEW_RETRIES_2026_ENABLED;
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY;
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY;
import static software.amazon.awssdk.core.interceptor.SdkExecutionAttribute.RESOLVED_CHECKSUM_SPECS;
Expand Down Expand Up @@ -104,6 +105,7 @@
.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, clientConfig.option(AwsClientOption.SIGNING_REGION))
.putAttribute(SdkInternalExecutionAttribute.IS_FULL_DUPLEX, executionParams.isFullDuplex())
.putAttribute(SdkInternalExecutionAttribute.IS_LONG_POLLING, executionParams.isLongPolling())
.putAttribute(SdkInternalExecutionAttribute.NEW_RETRIES_2026_ENABLED, clientConfig.option(NEW_RETRIES_2026_ENABLED))
.putAttribute(SdkInternalExecutionAttribute.HAS_INITIAL_REQUEST_EVENT, executionParams.hasInitialRequestEvent())
.putAttribute(SdkExecutionAttribute.CLIENT_TYPE, clientConfig.option(SdkClientOption.CLIENT_TYPE))
.putAttribute(SdkExecutionAttribute.SERVICE_NAME, clientConfig.option(SdkClientOption.SERVICE_NAME))
Expand Down Expand Up @@ -270,7 +272,7 @@
SdkClientConfiguration clientConfig,
SdkRequest originalRequest) {

// TODO(request-override auth scheme feature): When request-level auth scheme provider is added, use the request-level

Check warning on line 275 in core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ3VWMLnBIKHU6rmS9R5&open=AZ3VWMLnBIKHU6rmS9R5&pullRequest=6914
// auth scheme provider if the customer specified an override, otherwise fall back to the one on the client.
AuthSchemeProvider authSchemeProvider = clientConfig.option(SdkClientOption.AUTH_SCHEME_PROVIDER);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static software.amazon.awssdk.core.client.config.SdkClientOption.NEW_RETRIES_2026_ENABLED;
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY;

import java.util.stream.Stream;
Expand All @@ -35,7 +36,7 @@
import software.amazon.awssdk.retries.StandardRetryStrategy;
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;

public class InternalDefaultsTest {

Check warning on line 39 in core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/InternalDefaultsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ3VWMMDBIKHU6rmS9R6&open=AZ3VWMMDBIKHU6rmS9R6&pullRequest=6914
private static String newRetries2026Save;

@BeforeAll
Expand All @@ -60,8 +61,8 @@
@ParameterizedTest(name = "system prop = {0}, env var = {1}, default cfg = {2}, expected = {3}")
@MethodSource("newRetries2026Settings")
void buildClient_precedenceIsCorrect(String systemProperty, String environmentVariable, Boolean defaultConfig,
Class<?> retryStrategyClass) {
Class<?> retryStrategyClass, boolean newRetries2026Enabled) {
EnvironmentVariableHelper.run((env) -> {

Check warning on line 65 in core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/InternalDefaultsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the parentheses around the "env" parameter

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ3VWMMDBIKHU6rmS9R7&open=AZ3VWMMDBIKHU6rmS9R7&pullRequest=6914
if (environmentVariable != null) {
env.set(SdkSystemSetting.AWS_NEW_RETRIES_2026.environmentVariable(), environmentVariable);
}
Expand All @@ -80,23 +81,26 @@

assertThat(sync.clientConfiguration.option(RETRY_STRATEGY)).isInstanceOf(retryStrategyClass);
assertThat(async.clientConfiguration.option(RETRY_STRATEGY)).isInstanceOf(retryStrategyClass);

assertThat(sync.clientConfiguration.option(NEW_RETRIES_2026_ENABLED)).isEqualTo(newRetries2026Enabled);
assertThat(async.clientConfiguration.option(NEW_RETRIES_2026_ENABLED)).isEqualTo(newRetries2026Enabled);
});
}

// system property, environment variable, default config, expected retry strategy
static Stream<Arguments> newRetries2026Settings() {
return Stream.of(
Arguments.of(null, null, null, LegacyRetryStrategy.class),
Arguments.of(null, null, null, LegacyRetryStrategy.class, false),

Arguments.of("true", null, null, StandardRetryStrategy.class),
Arguments.of("false", null, null, LegacyRetryStrategy.class),
Arguments.of(null, "true", null, StandardRetryStrategy.class),
Arguments.of(null, "false", null, LegacyRetryStrategy.class),
Arguments.of(null, null, true, StandardRetryStrategy.class),
Arguments.of(null, null, false, LegacyRetryStrategy.class),
Arguments.of("true", null, null, StandardRetryStrategy.class, true),
Arguments.of("false", null, null, LegacyRetryStrategy.class, false),
Arguments.of(null, "true", null, StandardRetryStrategy.class, true),
Arguments.of(null, "false", null, LegacyRetryStrategy.class, false),
Arguments.of(null, null, true, StandardRetryStrategy.class, true),
Arguments.of(null, null, false, LegacyRetryStrategy.class, false),

Arguments.of("true", null, false, StandardRetryStrategy.class),
Arguments.of(null, "true", false, StandardRetryStrategy.class)
Arguments.of("true", null, false, StandardRetryStrategy.class, true),
Arguments.of(null, "true", false, StandardRetryStrategy.class, true)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,19 @@ public void invokeInterceptorsAndCreateExecutionContext_withLongPollingOperation
assertThat(executionContext.executionAttributes().getAttribute(SdkInternalExecutionAttribute.IS_LONG_POLLING)).isTrue();
}

@Test
public void invokeInterceptorsAndCreateExecutionContext_newRetries2026EnabledConfig_setsCorrectAttributeValue() {
SdkClientConfiguration clientConfig = testClientConfiguration()
.option(SdkClientOption.NEW_RETRIES_2026_ENABLED, true)
.build();
ClientExecutionParams<SdkRequest, SdkResponse> executionParams = clientExecutionParams();
ExecutionContext executionContext =
AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig);

assertThat(executionContext.executionAttributes()
.getAttribute(SdkInternalExecutionAttribute.NEW_RETRIES_2026_ENABLED)).isTrue();
}

private ClientExecutionParams<SdkRequest, SdkResponse> clientExecutionParams() {
return clientExecutionParams(sdkRequest);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@ public final class SdkClientOption<T> extends ClientOption<T> {
*/
public static final SdkClientOption<Boolean> DEFAULT_NEW_RETRIES_2026 = new SdkClientOption<>(Boolean.class);

/**
* Whether retries 2.1 behavior is enabled.
*/
public static final SdkClientOption<Boolean> NEW_RETRIES_2026_ENABLED = new SdkClientOption<>(Boolean.class);

/**
* The {@link EndpointProvider} configured on the client.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ public final class SdkInternalExecutionAttribute extends SdkExecutionAttribute {
*/
public static final ExecutionAttribute<Boolean> IS_LONG_POLLING = new ExecutionAttribute<>("IsLongPolling");

/**
* Indicates whether retries v2.1 is enabled.
*/
public static final ExecutionAttribute<Boolean> NEW_RETRIES_2026_ENABLED = new ExecutionAttribute<>("NewRetries2026Enabled");

/**
* The backing attribute for RESOLVED_CHECKSUM_SPECS.
* This holds the real ChecksumSpecs value, and is used to map to the ChecksumAlgorithm signer property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,26 @@
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.client.config.SdkClientOption;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
import software.amazon.awssdk.core.internal.http.HttpClientDependencies;
import software.amazon.awssdk.core.internal.http.RequestExecutionContext;
import software.amazon.awssdk.core.internal.http.TransformingAsyncResponseHandler;
import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline;
import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.RetryableStageHelper;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.utils.CompletableFutureUtils;
import software.amazon.awssdk.utils.Either;
import software.amazon.awssdk.utils.Logger;

/**
* Wrapper around the pipeline for a single request to provide retry, clockskew and request throttling functionality.
*/
@SdkInternalApi
public final class AsyncRetryableStage<OutputT> implements RequestPipeline<SdkHttpFullRequest,
CompletableFuture<Response<OutputT>>> {
private static final String X_AMZ_RETRY_AFTER_HEADER = "x-amz-retry-after";
private static final Logger LOG = Logger.loggerFor(AsyncRetryableStage.class);

private final TransformingAsyncResponseHandler<Response<OutputT>> responseHandler;
private final RequestPipeline<SdkHttpFullRequest, CompletableFuture<Response<OutputT>>> requestPipeline;
Expand Down Expand Up @@ -135,7 +140,7 @@
}

public void maybeAttemptExecute(CompletableFuture<Response<OutputT>> future) {
Either<Duration, Duration> backoffDelay = retryableStageHelper.tryRefreshToken(Duration.ZERO);
Either<Duration, Duration> backoffDelay = retryableStageHelper.tryRefreshToken(suggestedDelay());

Optional<Duration> acquireFailureDelay = backoffDelay.right();
if (acquireFailureDelay.isPresent()) {
Expand All @@ -158,7 +163,7 @@
context.requestProvider(originalRequestBody);

// get() is safe, Either requires left OR right to be present
Duration successDelay = backoffDelay.left().get();

Check warning on line 166 in core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Call "Optional#isPresent()" before accessing the value.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ3VWMC_BIKHU6rmS9Rj&open=AZ3VWMC_BIKHU6rmS9Rj&pullRequest=6914
retryableStageHelper.logBackingOff(successDelay);
long totalDelayMillis = successDelay.toMillis();
scheduledExecutor.schedule(() -> attemptExecute(future), totalDelayMillis, MILLISECONDS);
Expand All @@ -172,5 +177,37 @@
future.completeExceptionally(t);
}
}

private Duration suggestedDelay() {
if (newRetries2026Enabled(context)) {
return xAmzRetryAfter(retryableStageHelper.getLastResponse()).orElse(Duration.ZERO);
}
// Unlike in the sync RetryableStage, we never used 'Retry-After' for suggested delay in async
// https://github.com/aws/aws-sdk-java-v2/blob/1483d30d071716ead3dc1fa6571441658013d5c1/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java#L137
return Duration.ZERO;
}
}

/**
* Returns the suggested backoff delay based on the 'x-amz-retry-after' header value in the response.
*/
private Optional<Duration> xAmzRetryAfter(SdkHttpResponse response) {

Check warning on line 194 in core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this method into "RetryingExecutor".

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ3VWMC_BIKHU6rmS9Rl&open=AZ3VWMC_BIKHU6rmS9Rl&pullRequest=6914
Optional<String> optionalXAmzRetryAfter = response.firstMatchingHeader(X_AMZ_RETRY_AFTER_HEADER);
return optionalXAmzRetryAfter.map(xAmzRetryAfter -> {
try {
return Duration.ofMillis(Integer.parseInt(xAmzRetryAfter));
Comment thread
dagnir marked this conversation as resolved.
} catch (NumberFormatException e) {
// Ignore and fallback to returning empty.
LOG.debug(() -> String.format("Unable to parse header '%s' value '%s' as integer",
X_AMZ_RETRY_AFTER_HEADER, xAmzRetryAfter), e);
return null;
Comment thread
dagnir marked this conversation as resolved.
}
});
}

private boolean newRetries2026Enabled(RequestExecutionContext executionContext) {

Check warning on line 208 in core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this method into "RetryingExecutor".

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZ3VWMC_BIKHU6rmS9Rk&open=AZ3VWMC_BIKHU6rmS9Rk&pullRequest=6914
return executionContext.executionAttributes()
.getOptionalAttribute(SdkInternalExecutionAttribute.NEW_RETRIES_2026_ENABLED)
.orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.Response;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
import software.amazon.awssdk.core.internal.http.HttpClientDependencies;
import software.amazon.awssdk.core.internal.http.RequestExecutionContext;
import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline;
Expand All @@ -30,13 +31,17 @@
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpFullResponse;
import software.amazon.awssdk.utils.Either;
import software.amazon.awssdk.utils.Logger;

/**
* Wrapper around the pipeline for a single request to provide retry, clock-skew and request throttling functionality.
*/
@SdkInternalApi
public final class RetryableStage<OutputT> implements RequestToResponsePipeline<OutputT> {
private static final String RETRY_AFTER_HEADER = "Retry-After";
private static final String X_AMZ_RETRY_AFTER_HEADER = "x-amz-retry-after";
private static final Logger LOG = Logger.loggerFor(RetryableStage.class);

private final RequestPipeline<SdkHttpFullRequest, Response<OutputT>> requestPipeline;
private final HttpClientDependencies dependencies;

Expand Down Expand Up @@ -87,7 +92,7 @@ public Response<OutputT> execute(SdkHttpFullRequest request, RequestExecutionCon
private Duration suggestedDelay(Exception e) {
if (e instanceof SdkExceptionWithRetryAfterHint) {
SdkExceptionWithRetryAfterHint except = (SdkExceptionWithRetryAfterHint) e;
return Duration.ofSeconds(except.retryAfter());
return except.retryAfter();
}
return Duration.ZERO;
}
Expand All @@ -102,52 +107,83 @@ private Response<OutputT> executeRequest(RetryableStageHelper retryableStageHelp
retryableStageHelper.setLastResponse(response.httpResponse());
if (!response.isSuccess()) {
retryableStageHelper.adjustClockIfClockSkew(response);
throw responseException(response);
throw responseException(response, context);
}
return response;
}

private RuntimeException responseException(Response<OutputT> response) {
Optional<Integer> optionalRetryAfter = retryAfter(response.httpResponse());
private RuntimeException responseException(Response<OutputT> response, RequestExecutionContext context) {
Optional<Duration> optionalRetryAfter;
if (newRetries2026Enabled(context)) {
optionalRetryAfter = xAmzRetryAfter(response.httpResponse());
} else {
optionalRetryAfter = retryAfter(response.httpResponse());
}

if (optionalRetryAfter.isPresent()) {
return new SdkExceptionWithRetryAfterHint(optionalRetryAfter.get(), response.exception());
}
return response.exception();
}

private Optional<Integer> retryAfter(SdkHttpFullResponse response) {
/**
* Returns the suggested backoff delay based on the 'x-amz-retry-after' header value in the response.
*/
private Optional<Duration> xAmzRetryAfter(SdkHttpFullResponse response) {
Optional<String> optionalXAmzRetryAfter = response.firstMatchingHeader(X_AMZ_RETRY_AFTER_HEADER);
return optionalXAmzRetryAfter.map(xAmzRetryAfter -> {
try {
return Duration.ofMillis(Integer.parseInt(xAmzRetryAfter));
} catch (NumberFormatException e) {
// Ignore and fallback to returning empty.
logIntParseException(X_AMZ_RETRY_AFTER_HEADER, xAmzRetryAfter, e);
return null;
}
});
}

/**
* Returns the suggested backoff delay based on the 'Retry-After' header value in the response.
*/
private Optional<Duration> retryAfter(SdkHttpFullResponse response) {
Optional<String> optionalRetryAfterHeader = response.firstMatchingHeader(RETRY_AFTER_HEADER);
if (optionalRetryAfterHeader.isPresent()) {
String retryAfterHeader = optionalRetryAfterHeader.get();
return optionalRetryAfterHeader.map(retryAfterHeader -> {
try {
return Optional.of(Integer.parseInt(retryAfterHeader));
return Duration.ofSeconds(Integer.parseInt(retryAfterHeader));
Comment thread
dagnir marked this conversation as resolved.
} catch (NumberFormatException e) {
// Ignore and fallback to returning empty.
logIntParseException(RETRY_AFTER_HEADER, retryAfterHeader, e);
return null;
}
}
return Optional.empty();
});
}

private boolean newRetries2026Enabled(RequestExecutionContext executionContext) {
return executionContext.executionAttributes()
.getOptionalAttribute(SdkInternalExecutionAttribute.NEW_RETRIES_2026_ENABLED)
.orElse(false);
}

private static void logIntParseException(String headerName, String headerValue, Throwable t) {
LOG.debug(() -> String.format("Unable to parse header '%s' value '%s' as integer", headerName, headerValue), t);
}

// This probably should go directly into SdkException
static class SdkExceptionWithRetryAfterHint extends RuntimeException {
private final SdkException cause;
private final int seconds;
private final Duration delay;

SdkExceptionWithRetryAfterHint(int seconds, SdkException cause) {
this.seconds = seconds;
SdkExceptionWithRetryAfterHint(Duration delay, SdkException cause) {
this.delay = delay;
this.cause = cause;
}

public int retryAfter() {
return seconds;
public Duration retryAfter() {
return delay;
}

public SdkException cause() {
return cause;
}

public int seconds() {
return seconds;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ public void setLastResponse(SdkHttpResponse lastResponse) {
this.lastResponse = lastResponse;
}

public SdkHttpResponse getLastResponse() {
return lastResponse;
}

/**
* Returns true if this is the first attempt.
*/
Expand Down
Loading
Loading