diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java index c17a83c25030..d3cc24a8eeae 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java @@ -225,6 +225,11 @@ public class CustomizationConfig { private RetryMode defaultRetryMode; + /** + * Whether the client will use retry 2.1 behavior by default. + */ + private Boolean defaultNewRetries2026; + /** * Whether to generate an abstract decorator class that delegates to the async service client */ @@ -714,6 +719,14 @@ public void setDefaultRetryMode(RetryMode defaultRetryMode) { this.defaultRetryMode = defaultRetryMode; } + public Boolean getDefaultNewRetries2026() { + return defaultNewRetries2026; + } + + public void setDefaultNewRetries2026(Boolean defaultNewRetries2026) { + this.defaultNewRetries2026 = defaultNewRetries2026; + } + public ServiceConfig getServiceConfig() { return serviceConfig; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java index 44d8ce154e72..d3c37f92a367 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java @@ -328,9 +328,10 @@ private void configureEnvironmentBearerToken(MethodSpec.Builder builder) { private Optional mergeInternalDefaultsMethod() { String userAgent = model.getCustomizationConfig().getUserAgent(); RetryMode defaultRetryMode = model.getCustomizationConfig().getDefaultRetryMode(); + Boolean defaultNewRetries2026 = model.getCustomizationConfig().getDefaultNewRetries2026(); // If none of the options are customized, then we do not need to bother overriding the method - if (userAgent == null && defaultRetryMode == null) { + if (userAgent == null && defaultRetryMode == null && defaultNewRetries2026 == null) { return Optional.empty(); } @@ -348,6 +349,10 @@ private Optional mergeInternalDefaultsMethod() { builder.addCode("c.option($T.DEFAULT_RETRY_MODE, $T.$L);\n", SdkClientOption.class, RetryMode.class, defaultRetryMode.name()); } + if (defaultNewRetries2026 != null) { + builder.addCode("c.option($T.DEFAULT_NEW_RETRIES_2026, $L);\n", + SdkClientOption.class, defaultNewRetries2026); + } builder.addCode("});\n"); return Optional.of(builder.build()); } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java index ac11be3c716d..52b5677b2d5f 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java @@ -69,7 +69,7 @@ void baseClientBuilderClassWithNoAuthService_sra() { } @Test - void baseClientBuilderClassWithInternalUserAgent_sra() { + void baseClientBuilderClassWithInternalDefaults_sra() { validateBaseClientBuilderClassGeneration(internalConfigModels(), "test-client-builder-internal-defaults-class.java"); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java index 9b143b9ccd69..5ecb09b82593 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-internal-defaults-class.java @@ -69,6 +69,7 @@ protected final SdkClientConfiguration mergeInternalDefaults(SdkClientConfigurat return config.merge(c -> { c.option(SdkClientOption.INTERNAL_USER_AGENT, "md/foobar"); c.option(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD); + c.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026, true); }); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config index b4e783add53d..594a4aceb54d 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/internalconfig/customization.config @@ -3,5 +3,6 @@ "skip" : true }, "userAgent": "md/foobar", - "defaultRetryMode": "STANDARD" + "defaultRetryMode": "STANDARD", + "defaultNewRetries2026": "true" } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 6f392f6f7324..d97261c3c44d 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -54,6 +54,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.internal.SdkInternalTestAdvancedClientOption; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; +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.http.SdkHttpClient; @@ -457,12 +458,18 @@ private void configureRetryStrategy(SdkClientConfiguration.Builder config) { } private RetryStrategy resolveAwsRetryStrategy(LazyValueSource config) { + Boolean defaultNewRetries2026 = config.get(SdkClientOption.DEFAULT_NEW_RETRIES_2026); + RetryMode retryMode = RetryMode.resolver() .profileFile(config.get(SdkClientOption.PROFILE_FILE_SUPPLIER)) .profileName(config.get(SdkClientOption.PROFILE_NAME)) .defaultRetryMode(config.get(SdkClientOption.DEFAULT_RETRY_MODE)) + .defaultNewRetries2026(defaultNewRetries2026) .resolve(); - return AwsRetryStrategy.forRetryMode(retryMode); + + NewRetries2026Resolver newRetries2026Resolver = new NewRetries2026Resolver().defaultNewRetries2026(defaultNewRetries2026); + + return AwsRetryStrategy.forRetryMode(retryMode, newRetries2026Resolver.resolve()); } @Override diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryStrategy.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryStrategy.java index 17cd00b58e08..a68b95e503ac 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryStrategy.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryStrategy.java @@ -63,17 +63,29 @@ public static RetryStrategy defaultRetryStrategy() { } /** - * Retrieve the appropriate retry strategy for the retry mode with AWS-specific conditions added. + * Retrieve the appropriate retry strategy for the retry mode with AWS-specific conditions added. This is equivalent to + * {@code forRetryMode(mode, false)}. * * @param mode The retry mode for which we want to create a retry strategy. * @return A retry strategy for the given retry mode. */ public static RetryStrategy forRetryMode(RetryMode mode) { + return forRetryMode(mode, false); + } + + /** + * Retrieve the appropriate retry strategy for the retry mode with AWS-specific conditions added. + * + * @param mode The retry mode for which we want to create a retry strategy. + * @param newRetries2026Enabled Whether retries 2.1 behavior is enabled. + * @return A retry strategy for the given retry mode. + */ + public static RetryStrategy forRetryMode(RetryMode mode, boolean newRetries2026Enabled) { switch (mode) { case STANDARD: - return standardRetryStrategy(); + return standardRetryStrategy(newRetries2026Enabled); case ADAPTIVE_V2: - return adaptiveRetryStrategy(); + return adaptiveRetryStrategy(newRetries2026Enabled); case LEGACY: return legacyRetryStrategy(); case ADAPTIVE: @@ -83,6 +95,7 @@ public static RetryStrategy forRetryMode(RetryMode mode) { } } + /** * Update the provided {@link RetryStrategy} to add AWS-specific conditions. * @@ -105,12 +118,23 @@ public static RetryStrategy doNotRetry() { } /** - * Returns a {@link StandardRetryStrategy} with AWS-specific conditions added. + * Returns a {@link StandardRetryStrategy} with AWS-specific conditions added. This is equivalent to {@code + * standardRetryStrategy(false)}. * * @return A {@link StandardRetryStrategy} with AWS-specific conditions added. */ public static StandardRetryStrategy standardRetryStrategy() { - StandardRetryStrategy.Builder builder = SdkDefaultRetryStrategy.standardRetryStrategyBuilder(); + return standardRetryStrategy(false); + } + + /** + * Returns a {@link StandardRetryStrategy} with AWS-specific conditions added. + * + * @param newRetries2026Enabled Whether retries 2.1 behavior is enabled. + * @return A {@link StandardRetryStrategy} with AWS-specific conditions added. + */ + public static StandardRetryStrategy standardRetryStrategy(boolean newRetries2026Enabled) { + StandardRetryStrategy.Builder builder = SdkDefaultRetryStrategy.standardRetryStrategyBuilder(newRetries2026Enabled); return configure(builder).build(); } @@ -126,12 +150,23 @@ public static LegacyRetryStrategy legacyRetryStrategy() { } /** - * Returns an {@link AdaptiveRetryStrategy} with AWS-specific conditions added. + * Returns an {@link AdaptiveRetryStrategy} with AWS-specific conditions added. This is equivalent to {@code + * adaptiveRetryStrategy(false)}. * * @return An {@link AdaptiveRetryStrategy} with AWS-specific conditions added. */ public static AdaptiveRetryStrategy adaptiveRetryStrategy() { - AdaptiveRetryStrategy.Builder builder = SdkDefaultRetryStrategy.adaptiveRetryStrategyBuilder(); + return adaptiveRetryStrategy(false); + } + + /** + * Returns an {@link AdaptiveRetryStrategy} with AWS-specific conditions added. + * + * @param newRetries2026Enabled Whether retries 2.1 behavior is enabled. + * @return An {@link AdaptiveRetryStrategy} with AWS-specific conditions added. + */ + public static AdaptiveRetryStrategy adaptiveRetryStrategy(boolean newRetries2026Enabled) { + AdaptiveRetryStrategy.Builder builder = SdkDefaultRetryStrategy.adaptiveRetryStrategyBuilder(newRetries2026Enabled); return configure(builder) .build(); } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/InternalDefaultsTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/InternalDefaultsTest.java new file mode 100644 index 000000000000..f397e0bc53a2 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/InternalDefaultsTest.java @@ -0,0 +1,159 @@ +/* + * 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.awscore.client.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY; + +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +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.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.retries.LegacyRetryStrategy; +import software.amazon.awssdk.retries.StandardRetryStrategy; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +public class InternalDefaultsTest { + private static String newRetries2026Save; + + @BeforeAll + static void setup() { + newRetries2026Save = System.getProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); + } + + @BeforeEach + void methodSetup() { + System.clearProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); + } + + @AfterAll + static void teardown() { + if (newRetries2026Save != null) { + System.setProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property(), newRetries2026Save); + } else { + System.clearProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); + } + } + + @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) { + EnvironmentVariableHelper.run((env) -> { + if (environmentVariable != null) { + env.set(SdkSystemSetting.AWS_NEW_RETRIES_2026.environmentVariable(), environmentVariable); + } + + if (systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property(), systemProperty); + } + + TestClient sync = new TestClientBuilder(true) + .newRetries2026Default(defaultConfig) + .buildClient(); + + TestClient async = new TestClientBuilder(false) + .newRetries2026Default(defaultConfig) + .buildClient(); + + assertThat(sync.clientConfiguration.option(RETRY_STRATEGY)).isInstanceOf(retryStrategyClass); + assertThat(async.clientConfiguration.option(RETRY_STRATEGY)).isInstanceOf(retryStrategyClass); + }); + } + + // system property, environment variable, default config, expected retry strategy + static Stream newRetries2026Settings() { + return Stream.of( + Arguments.of(null, null, null, LegacyRetryStrategy.class), + + 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, false, StandardRetryStrategy.class), + Arguments.of(null, "true", false, StandardRetryStrategy.class) + ); + } + + private static class TestClient { + private final SdkClientConfiguration clientConfiguration; + + public TestClient(SdkClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + } + + private static class TestClientBuilder extends AwsDefaultClientBuilder { + private final boolean sync; + private Boolean newRetries2026Default; + + protected TestClientBuilder(boolean sync) { + super(mock(SdkHttpClient.Builder.class), mock(SdkAsyncHttpClient.Builder.class), null); + this.sync = sync; + } + + public TestClientBuilder newRetries2026Default(Boolean newRetries2026Default) { + this.newRetries2026Default = newRetries2026Default; + return this; + } + + @Override + protected String serviceEndpointPrefix() { + return "test-client"; + } + + @Override + protected String signingName() { + return "test-client"; + } + + @Override + protected String serviceName() { + return "test-client"; + } + + @Override + protected final SdkClientConfiguration mergeInternalDefaults(SdkClientConfiguration config) { + return config.merge(c -> { + c.option(SdkClientOption.DEFAULT_NEW_RETRIES_2026, newRetries2026Default); + }); + } + + @Override + protected TestClient buildClient() { + SdkClientConfiguration config; + if (sync) { + config = syncClientConfiguration(); + } else { + config = asyncClientConfiguration(); + } + + return new TestClient(config); + } + } +} diff --git a/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java b/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java index 879ddd002d49..84540992b04c 100644 --- a/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java +++ b/core/retries/src/main/java/software/amazon/awssdk/retries/DefaultRetryStrategy.java @@ -38,7 +38,7 @@ public static StandardRetryStrategy doNotRetry() { } /** - * Create a new builder for a {@link StandardRetryStrategy}. + * Create a new builder for a {@link StandardRetryStrategy}. This is equivalent to {@code standardStrategyBuilder(false)}. * *

Example Usage * {@snippet @@ -50,7 +50,25 @@ public static StandardRetryStrategy doNotRetry() { * } */ public static StandardRetryStrategy.Builder standardStrategyBuilder() { - return StandardRetryStrategy.builder(); + return standardStrategyBuilder(false); + } + + /** + * Create a new builder for a {@link StandardRetryStrategy}. This is equivalent to {@code standardStrategyBuilder(false)}. + * + *

Example Usage + * {@snippet + * StandardRetryStrategy retryStrategy = + * DefaultRetryStrategy.standardStrategyBuilder(true) + * .retryOnExceptionInstanceOf(IllegalArgumentException.class) + * .retryOnExceptionInstanceOf(IllegalStateException.class) + * .build(); + * } + * + * @param retries2026Enabled Whether retries 2.1 behavior is used. + */ + public static StandardRetryStrategy.Builder standardStrategyBuilder(boolean retries2026Enabled) { + return StandardRetryStrategy.builder(retries2026Enabled); } /** @@ -70,7 +88,7 @@ public static LegacyRetryStrategy.Builder legacyStrategyBuilder() { } /** - * Create a new builder for a {@link AdaptiveRetryStrategy}. + * Create a new builder for a {@link AdaptiveRetryStrategy}. This is equivalent to {@code adaptiveStrategyBuilder(false)}. * *

Example Usage * {@snippet @@ -82,7 +100,25 @@ public static LegacyRetryStrategy.Builder legacyStrategyBuilder() { * } */ public static AdaptiveRetryStrategy.Builder adaptiveStrategyBuilder() { - return AdaptiveRetryStrategy.builder(); + return adaptiveStrategyBuilder(false); + } + + /** + * Create a new builder for a {@link AdaptiveRetryStrategy}. + * + *

Example Usage + * {@snippet + * AdaptiveRetryStrategy retryStrategy = + * DefaultRetryStrategy.adaptiveStrategyBuilder(true) + * .retryOnExceptionInstanceOf(IllegalArgumentException.class) + * .retryOnExceptionInstanceOf(IllegalStateException.class) + * .build(); + * } + * + * @param retries2026Enabled Whether retries 2.1 behavior is used. + */ + public static AdaptiveRetryStrategy.Builder adaptiveStrategyBuilder(boolean retries2026Enabled) { + return AdaptiveRetryStrategy.builder(retries2026Enabled); } static final class Standard { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index 759ee4ad4a59..db0ee4d67f8f 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -270,7 +270,7 @@ public enum SdkSystemSetting implements SystemSetting { * defaults including STANDARD as the default retry mode, reduced base backoff delays, differentiated token bucket * costs, and other v2.1 retry specification changes. When {@code false} (the default), the SDK uses v2.0 retry behavior. */ - AWS_NEW_RETRIES_2026("aws.newRetries2026", "false"); + AWS_NEW_RETRIES_2026("aws.newRetries2026", null); private final String systemProperty; private final String defaultValue; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 53dfcc52d6bb..0e20334e9ecd 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -306,6 +306,11 @@ public final class SdkClientOption extends ClientOption { */ public static final SdkClientOption DEFAULT_RETRY_MODE = new SdkClientOption<>(RetryMode.class); + /** + * Option to specify the default for the {@code AWS_NEW_RETRIES_2026} feature gate for the SDK client. + */ + public static final SdkClientOption DEFAULT_NEW_RETRIES_2026 = new SdkClientOption<>(Boolean.class); + /** * The {@link EndpointProvider} configured on the client. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetryStrategy.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetryStrategy.java index bc7685b6126c..ea4aae5e1869 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetryStrategy.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetryStrategy.java @@ -139,10 +139,20 @@ public static AdaptiveRetryStrategy adaptiveRetryStrategy() { * @return a {@link StandardRetryStrategy.Builder} with preconfigured generic SDK retry conditions. */ public static StandardRetryStrategy.Builder standardRetryStrategyBuilder() { - StandardRetryStrategy.Builder builder = DefaultRetryStrategy.standardStrategyBuilder(); + return standardRetryStrategyBuilder(false); + } + + /** + * Returns a {@link StandardRetryStrategy.Builder} with preconfigured generic SDK retry conditions. + * + * @return a {@link StandardRetryStrategy.Builder} with preconfigured generic SDK retry conditions. + */ + public static StandardRetryStrategy.Builder standardRetryStrategyBuilder(boolean newRetries2026Enabled) { + StandardRetryStrategy.Builder builder = DefaultRetryStrategy.standardStrategyBuilder(newRetries2026Enabled); return configure(builder); } + /** * Returns a {@link LegacyRetryStrategy.Builder} with preconfigured generic SDK retry conditions. * @@ -159,7 +169,16 @@ public static LegacyRetryStrategy.Builder legacyRetryStrategyBuilder() { * @return an {@link AdaptiveRetryStrategy.Builder} with preconfigured generic SDK retry conditions. */ public static AdaptiveRetryStrategy.Builder adaptiveRetryStrategyBuilder() { - AdaptiveRetryStrategy.Builder builder = DefaultRetryStrategy.adaptiveStrategyBuilder(); + return adaptiveRetryStrategyBuilder(false); + } + + /** + * Returns an {@link AdaptiveRetryStrategy.Builder} with preconfigured generic SDK retry conditions. + * + * @return an {@link AdaptiveRetryStrategy.Builder} with preconfigured generic SDK retry conditions. + */ + public static AdaptiveRetryStrategy.Builder adaptiveRetryStrategyBuilder(boolean newRetries2026Enabled) { + AdaptiveRetryStrategy.Builder builder = DefaultRetryStrategy.adaptiveStrategyBuilder(newRetries2026Enabled); return configure(builder); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/NewRetries2026Resolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/NewRetries2026Resolver.java new file mode 100644 index 000000000000..142583b15c2c --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/NewRetries2026Resolver.java @@ -0,0 +1,56 @@ +/* + * 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.core.retry; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.core.SdkSystemSetting; + +/** + * Resolver for the {@link SdkSystemSetting#AWS_NEW_RETRIES_2026} that supports setting a fallback value if not defined in the + * environment or system properties. + */ +@SdkProtectedApi +public final class NewRetries2026Resolver { + private Boolean defaultNewRetries2026; + + /** + * The default value for {@code AWS_NEW_RETRIES_2026} if not configured via {@link SdkSystemSetting#AWS_NEW_RETRIES_2026}. + * + * @return This resolver for method chaining. + */ + public NewRetries2026Resolver defaultNewRetries2026(Boolean defaultNewRetries2026) { + this.defaultNewRetries2026 = defaultNewRetries2026; + return this; + } + + /** + * Resolve whether retries v2.1 is used. + */ + public boolean resolve() { + Optional envConfig = SdkSystemSetting.AWS_NEW_RETRIES_2026.getBooleanValue(); + + if (envConfig.isPresent()) { + return envConfig.get(); + } + + if (defaultNewRetries2026 != null) { + return defaultNewRetries2026; + } + + return false; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryMode.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryMode.java index 0f233b46326b..3d616446eafb 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryMode.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryMode.java @@ -133,6 +133,7 @@ public static class Resolver { private Supplier profileFile; private String profileName; private RetryMode defaultRetryMode; + private Boolean defaultNewRetries2026; private Resolver() { } @@ -162,6 +163,15 @@ public Resolver defaultRetryMode(RetryMode defaultRetryMode) { return this; } + /** + * Configure whether retry 2.1 behavior is enabled by default if not specified anywhere else (i.e. via + * {@link SdkSystemSetting#AWS_NEW_RETRIES_2026}). + */ + public Resolver defaultNewRetries2026(Boolean defaultNewRetries2026) { + this.defaultNewRetries2026 = defaultNewRetries2026; + return this; + } + /** * Resolve which retry mode should be used, based on the configured values. */ @@ -208,8 +218,8 @@ private RetryMode fromDefaultMode() { /** * Resolves the SDK default retry mode dynamically based on the {@code AWS_NEW_RETRIES_2026} gate. */ - private static RetryMode sdkDefaultRetryMode() { - return SdkSystemSetting.AWS_NEW_RETRIES_2026.getBooleanValue().orElse(false) ? STANDARD : LEGACY; + private RetryMode sdkDefaultRetryMode() { + return new NewRetries2026Resolver().defaultNewRetries2026(defaultNewRetries2026).resolve() ? STANDARD : LEGACY; } } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/SdkSystemSettingNewRetriesTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/SdkSystemSettingNewRetriesTest.java index a55bcfc324ec..0a32d18f0129 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/SdkSystemSettingNewRetriesTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/SdkSystemSettingNewRetriesTest.java @@ -34,8 +34,8 @@ void cleanup() { } @Test - void defaultsToFalse_whenUnset() { - assertThat(SdkSystemSetting.AWS_NEW_RETRIES_2026.getBooleanValue()).hasValue(false); + void defaultsToEmpty_whenUnset() { + assertThat(SdkSystemSetting.AWS_NEW_RETRIES_2026.getBooleanValue()).isEmpty(); } @ParameterizedTest(name = "systemProperty=\"{0}\" -> {1}") @@ -82,8 +82,8 @@ void systemProperty_isCorrectName() { } @Test - void defaultValue_isFalse() { + void defaultValue_isNull() { assertThat(SdkSystemSetting.AWS_NEW_RETRIES_2026.defaultValue()) - .isEqualTo("false"); + .isNull(); } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/NewRetries2026ResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/NewRetries2026ResolverTest.java new file mode 100644 index 000000000000..573d0fff22f0 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/NewRetries2026ResolverTest.java @@ -0,0 +1,110 @@ +/* + * 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.core.retry; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +public class NewRetries2026ResolverTest { + private static String newRetries2026Save; + + @BeforeAll + static void setup() { + newRetries2026Save = System.getProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); + } + + @AfterAll + static void teardown() { + if (newRetries2026Save != null) { + System.setProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property(), newRetries2026Save); + } else { + System.clearProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); + } + } + + @BeforeEach + void methodSetup() { + System.clearProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); + } + + @ParameterizedTest + @MethodSource("params") + void resolve_behavesCorrectly(TestParams params) { + EnvironmentVariableHelper.run((env) -> { + if (params.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property(), params.systemProperty); + } + + if (params.envVar != null) { + env.set(SdkSystemSetting.AWS_NEW_RETRIES_2026.environmentVariable(), params.envVar); + } + + NewRetries2026Resolver resolver = new NewRetries2026Resolver().defaultNewRetries2026(params.defaultNewRetries2026); + + assertThat(resolver.resolve()).isEqualTo(params.expected); + }); + } + + private static Stream params() { + return Stream.of( + // default + new TestParams().expected(false), + + // precedence testing + new TestParams().systemProperty("true").defaultNewRetries2026(true).expected(true), + new TestParams().systemProperty("false").defaultNewRetries2026(true).expected(false), + new TestParams().envVar("true").defaultNewRetries2026(true).expected(true), + new TestParams().envVar("false").defaultNewRetries2026(true).expected(false), + new TestParams().defaultNewRetries2026(true).expected(true), + new TestParams().defaultNewRetries2026(false).expected(false) + ); + } + + private static class TestParams { + private String systemProperty; + private String envVar; + private Boolean defaultNewRetries2026; + private boolean expected; + + public TestParams systemProperty(String systemProperty) { + this.systemProperty = systemProperty; + return this; + } + + public TestParams envVar(String envVar) { + this.envVar = envVar; + return this; + } + + public TestParams defaultNewRetries2026(Boolean defaultNewRetries2026) { + this.defaultNewRetries2026 = defaultNewRetries2026; + return this; + } + + public TestParams expected(boolean expected) { + this.expected = expected; + return this; + } + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryModeTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryModeTest.java index b5fb23c37ed4..40fca37c82e3 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryModeTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryModeTest.java @@ -44,34 +44,57 @@ public class RetryModeTest { public static Collection data() { return Arrays.asList(new Object[] { // Test defaults - new TestData(null, null, null, null, RetryMode.LEGACY), - new TestData(null, null, "PropertyNotSet", null, RetryMode.LEGACY), + new TestData(null, null, null, null, null, null, null, RetryMode.LEGACY), + new TestData(null, null, "PropertyNotSet", null, null, null, null, RetryMode.LEGACY), + + // default + new retries 2026 + new TestData(null, null, null, null, "true", null, null, RetryMode.STANDARD), + new TestData(null, null, null, null, null, "true", null, RetryMode.STANDARD), + new TestData(null, null, null, null, null, null, true, RetryMode.STANDARD), + + // new retries 2026 precedence + new TestData(null, null, null, null, "false", null, true, RetryMode.LEGACY), + new TestData(null, null, null, null, null, "false", true, RetryMode.LEGACY), // Test resolution - new TestData("legacy", null, null, null, RetryMode.LEGACY), - new TestData("standard", null, null, null, RetryMode.STANDARD), - new TestData("adaptive", null, null, null, RetryMode.ADAPTIVE_V2), - new TestData("lEgAcY", null, null, null, RetryMode.LEGACY), - new TestData("sTanDaRd", null, null, null, RetryMode.STANDARD), - new TestData("aDaPtIvE", null, null, null, RetryMode.ADAPTIVE_V2), + new TestData("legacy", null, null, null, null, null, null, RetryMode.LEGACY), + new TestData("standard", null, null, null, null, null, null, RetryMode.STANDARD), + new TestData("adaptive", null, null, null, null, null, null, RetryMode.ADAPTIVE_V2), + new TestData("lEgAcY", null, null, null, null, null, null, RetryMode.LEGACY), + new TestData("sTanDaRd", null, null, null, null, null, null, RetryMode.STANDARD), + new TestData("aDaPtIvE", null, null, null, null, null, null, RetryMode.ADAPTIVE_V2), + + // new retries 2026 does not have an effect if retry mode set explicitly + new TestData("legacy", null, null, null, "true", null, null, RetryMode.LEGACY), + new TestData("standard", null, null, null, "true", null, null, RetryMode.STANDARD), + new TestData("adaptive", null, null, null, "true", null, null, RetryMode.ADAPTIVE_V2), + new TestData("lEgAcY", null, null, null, "true", null, null, RetryMode.LEGACY), + new TestData("sTanDaRd", null, null, null, "true", null, null, RetryMode.STANDARD), + new TestData("aDaPtIvE", null, null, null, "true", null, null, RetryMode.ADAPTIVE_V2), + new TestData("legacy", null, null, null, "false", null, null, RetryMode.LEGACY), + new TestData("standard", null, null, null, "false", null, null, RetryMode.STANDARD), + new TestData("adaptive", null, null, null, "false", null, null, RetryMode.ADAPTIVE_V2), + new TestData("lEgAcY", null, null, null, "false", null, null, RetryMode.LEGACY), + new TestData("sTanDaRd", null, null, null, "false", null, null, RetryMode.STANDARD), + new TestData("aDaPtIvE", null, null, null, "false", null, null, RetryMode.ADAPTIVE_V2), // Test precedence - new TestData("standard", "legacy", "PropertySetToLegacy", RetryMode.LEGACY, RetryMode.STANDARD), - new TestData("standard", null, null, RetryMode.LEGACY, RetryMode.STANDARD), - new TestData(null, "standard", "PropertySetToLegacy", RetryMode.LEGACY, RetryMode.STANDARD), - new TestData(null, "standard", null, RetryMode.LEGACY, RetryMode.STANDARD), - new TestData(null, null, "PropertySetToStandard", RetryMode.LEGACY, RetryMode.STANDARD), - new TestData(null, null, null, RetryMode.STANDARD, RetryMode.STANDARD), + new TestData("standard", "legacy", "PropertySetToLegacy", RetryMode.LEGACY, null, null, null, RetryMode.STANDARD), + new TestData("standard", null, null, RetryMode.LEGACY, null, null, null, RetryMode.STANDARD), + new TestData(null, "standard", "PropertySetToLegacy", RetryMode.LEGACY, null, null, null, RetryMode.STANDARD), + new TestData(null, "standard", null, RetryMode.LEGACY, null, null, null, RetryMode.STANDARD), + new TestData(null, null, "PropertySetToStandard", RetryMode.LEGACY, null, null, null, RetryMode.STANDARD), + new TestData(null, null, null, RetryMode.STANDARD, null, null, null, RetryMode.STANDARD), // Test invalid values - new TestData("wrongValue", null, null, null, IllegalStateException.class), - new TestData(null, "wrongValue", null, null, IllegalStateException.class), - new TestData(null, null, "PropertySetToUnsupportedValue", null, IllegalStateException.class), + new TestData("wrongValue", null, null, null, null, null, null, IllegalStateException.class), + new TestData(null, "wrongValue", null, null, null, null, null, IllegalStateException.class), + new TestData(null, null, "PropertySetToUnsupportedValue", null, null, null, null, IllegalStateException.class), // Test capitalization standardization - new TestData("sTaNdArD", null, null, null, RetryMode.STANDARD), - new TestData(null, "sTaNdArD", null, null, RetryMode.STANDARD), - new TestData(null, null, "PropertyMixedCase", null, RetryMode.STANDARD), + new TestData("sTaNdArD", null, null, null, null, null, null, RetryMode.STANDARD), + new TestData(null, "sTaNdArD", null, null, null, null, null, RetryMode.STANDARD), + new TestData(null, null, "PropertyMixedCase", null, null, null, null, RetryMode.STANDARD), }); } @@ -82,6 +105,8 @@ public void methodSetup() { System.clearProperty(SdkSystemSetting.AWS_RETRY_MODE.property()); System.clearProperty(ProfileFileSystemSetting.AWS_PROFILE.property()); System.clearProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property()); + + System.clearProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property()); } @Test @@ -101,7 +126,18 @@ public void differentCombinationOfConfigs_shouldResolveCorrectly() throws Except System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), diskLocationForFile); } - Callable result = RetryMode.resolver().defaultRetryMode(testData.defaultMode)::resolve; + if (testData.newRetries2026EnvVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_NEW_RETRIES_2026.environmentVariable(), + testData.newRetries2026EnvVarValue); + } + + if (testData.newRetries2026SystemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_NEW_RETRIES_2026.property(), testData.newRetries2026SystemProperty); + } + + Callable result = RetryMode.resolver() + .defaultRetryMode(testData.defaultMode) + .defaultNewRetries2026(testData.defaultNewRetries2026)::resolve; if (testData.expected instanceof Class) { Class expectedClassType = (Class) testData.expected; assertThatThrownBy(result::call).isInstanceOf(expectedClassType); @@ -120,14 +156,25 @@ private static class TestData { private final String systemProperty; private final String configFile; private final RetryMode defaultMode; + + private final String newRetries2026SystemProperty; + private final String newRetries2026EnvVarValue; + private final Boolean defaultNewRetries2026; + + private final Object expected; - TestData(String systemProperty, String envVarValue, String configFile, RetryMode defaultMode, Object expected) { + TestData(String systemProperty, String envVarValue, String configFile, RetryMode defaultMode, + String newRetries2026SystemProperty, String newRetries2026EnvVarValue, Boolean defaultNewRetries2026, + Object expected) { this.envVarValue = envVarValue; this.systemProperty = systemProperty; this.configFile = configFile; this.defaultMode = defaultMode; + this.newRetries2026SystemProperty = newRetries2026SystemProperty; + this.newRetries2026EnvVarValue = newRetries2026EnvVarValue; + this.defaultNewRetries2026 = defaultNewRetries2026; this.expected = expected; } } -} \ No newline at end of file +}