diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-11a7359.json b/.changes/next-release/bugfix-AWSSDKforJavav2-11a7359.json new file mode 100644 index 000000000000..fd7ac738174c --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-11a7359.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "description": "Fixed an issue where the retry condition returned by `RetryPolicy.retryCondition` differed from the one specified by `RetryPolicy.Builder.retryCondition`. The old value can be accessed via the new `RetryPolicy.aggregateRetryCondition`." +} diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-373ea2a.json b/.changes/next-release/bugfix-AWSSDKforJavav2-373ea2a.json new file mode 100644 index 000000000000..1a1a2e07362a --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-373ea2a.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "description": "Fixed an issue where specifying your own retry policy would override AWS and service-specific retry conditions. By default, all retry policies now have AWS and service-specific retry conditions added. This can be disabled via the new `RetryPolicy.furtherRefinementsAllowed(false)`." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-c758c22.json b/.changes/next-release/feature-AWSSDKforJavav2-c758c22.json new file mode 100644 index 000000000000..3a48b519773e --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-c758c22.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "description": "Added the ability to configure or disable the default retry throttling behavior of the SDK that 'kicks in' during a large volume of retriable service call errors. This behavior can now be configured via `RetryPolicy.retryCapacityCondition`." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-f7b0ca8.json b/.changes/next-release/feature-AWSSDKforJavav2-f7b0ca8.json new file mode 100644 index 000000000000..5d21dde3b11c --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-f7b0ca8.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "description": "Added support for \"retry modes\". A retry mode allows configuring multiple SDK parameters at once using default retry profiles, some of which are standardized between AWS SDK languages. See RetryMode javadoc for more information." +} 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 f579368383a1..88b8f85b83d5 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 @@ -28,7 +28,6 @@ import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; - import java.util.Collections; import java.util.List; import javax.lang.model.element.Modifier; @@ -160,7 +159,7 @@ private MethodSpec mergeServiceDefaultsMethod() { SdkClientOption.class, crc32FromCompressedDataEnabled); if (StringUtils.isNotBlank(model.getCustomizationConfig().getCustomRetryPolicy())) { - builder.addCode(".option($T.RETRY_POLICY, $T.defaultPolicy())", SdkClientOption.class, + builder.addCode(".option($T.RETRY_POLICY, $T.defaultRetryPolicy())", SdkClientOption.class, PoetUtils.classNameFromFqcn(model.getCustomizationConfig().getCustomRetryPolicy())); } builder.addCode(");"); @@ -198,14 +197,20 @@ private MethodSpec finalizeServiceConfigurationMethod() { .endControlFlow(); builder.addCode("return config.toBuilder()\n" + - " .option($1T.EXECUTION_INTERCEPTORS, interceptors)\n" + - " .option($1T.ENDPOINT_DISCOVERY_ENABLED, endpointDiscoveryEnabled)\n" + - " .build();", SdkClientOption.class); + ".option($T.ENDPOINT_DISCOVERY_ENABLED, endpointDiscoveryEnabled)\n", + SdkClientOption.class); } else { - builder.addCode("return config.toBuilder()\n" + - " .option($T.EXECUTION_INTERCEPTORS, interceptors)\n" + - " .build();", SdkClientOption.class); + builder.addCode("return config.toBuilder()\n"); + } + + builder.addCode(".option($1T.EXECUTION_INTERCEPTORS, interceptors)", SdkClientOption.class); + + if (StringUtils.isNotBlank(model.getCustomizationConfig().getCustomRetryPolicy())) { + builder.addCode(".option($1T.RETRY_POLICY, $2T.addRetryConditions(config.option($1T.RETRY_POLICY)))", + SdkClientOption.class, + PoetUtils.classNameFromFqcn(model.getCustomizationConfig().getCustomRetryPolicy())); } + builder.addCode(".build();"); return builder.build(); } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java index 26839d335cb8..c0238bc3115e 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java @@ -38,6 +38,11 @@ public void baseClientBuilderClass() throws Exception { validateGeneration(BaseClientBuilderClass::new, "test-client-builder-class.java"); } + @Test + public void baseQueryClientBuilderClass() throws Exception { + validateQueryGeneration(BaseClientBuilderClass::new, "test-query-client-builder-class.java"); + } + @Test public void syncClientBuilderInterface() throws Exception { validateGeneration(SyncClientBuilderInterface::new, "test-sync-client-builder-interface.java"); @@ -61,4 +66,8 @@ public void asyncClientBuilderClass() throws Exception { private void validateGeneration(Function generatorConstructor, String expectedClassName) { assertThat(generatorConstructor.apply(ClientTestModels.jsonServiceModels()), generatesTo(expectedClassName)); } + + private void validateQueryGeneration(Function generatorConstructor, String expectedClassName) { + assertThat(generatorConstructor.apply(ClientTestModels.queryServiceModels()), generatesTo(expectedClassName)); + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java index ae99b4def3d6..d7e8d945e7fb 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java @@ -2,6 +2,7 @@ import java.util.List; import software.amazon.MyServiceHttpConfig; +import software.amazon.MyServiceRetryPolicy; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.signer.Aws4Signer; @@ -33,8 +34,9 @@ protected final String serviceName() { @Override protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) { - return config.merge(c -> c.option(SdkAdvancedClientOption.SIGNER, defaultSigner()).option( - SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false)); + return config.merge(c -> c.option(SdkAdvancedClientOption.SIGNER, defaultSigner()) + .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false) + .option(SdkClientOption.RETRY_POLICY, MyServiceRetryPolicy.defaultRetryPolicy())); } @Override @@ -43,7 +45,11 @@ protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientCon List interceptors = interceptorFactory .getInterceptors("software/amazon/awssdk/services/json/execution.interceptors"); interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS)); - return config.toBuilder().option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors).build(); + return config + .toBuilder() + .option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors) + .option(SdkClientOption.RETRY_POLICY, + MyServiceRetryPolicy.addRetryConditions(config.option(SdkClientOption.RETRY_POLICY))).build(); } private Signer defaultSigner() { @@ -70,4 +76,3 @@ protected final AttributeMap serviceHttpConfig() { return result; } } - diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-query-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-query-client-builder-class.java new file mode 100644 index 000000000000..7e3e5318feb7 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-query-client-builder-class.java @@ -0,0 +1,59 @@ +package software.amazon.awssdk.services.query; + +import java.util.Collections; +import java.util.List; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.interceptor.ClasspathInterceptorChainFactory; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.protocols.query.interceptor.QueryParametersToBodyInterceptor; +import software.amazon.awssdk.utils.CollectionUtils; + +/** + * Internal base class for {@link DefaultQueryClientBuilder} and {@link DefaultQueryAsyncClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +abstract class DefaultQueryBaseClientBuilder, C> extends AwsDefaultClientBuilder { + @Override + protected final String serviceEndpointPrefix() { + return "query-service"; + } + + @Override + protected final String serviceName() { + return "Query"; + } + + @Override + protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) { + return config.merge(c -> c.option(SdkAdvancedClientOption.SIGNER, defaultSigner()).option( + SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false)); + } + + @Override + protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) { + ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory(); + List interceptors = interceptorFactory + .getInterceptors("software/amazon/awssdk/services/query/execution.interceptors"); + interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS)); + List protocolInterceptors = Collections.singletonList(new QueryParametersToBodyInterceptor()); + interceptors = CollectionUtils.mergeLists(interceptors, protocolInterceptors); + return config.toBuilder().option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors).build(); + } + + private Signer defaultSigner() { + return Aws4Signer.create(); + } + + @Override + protected final String signingName() { + return "query-service"; + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config index 33a8160445fc..ba01475ef717 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config @@ -5,6 +5,7 @@ "presignersFqcn": "software.amazon.awssdk.services.acm.presign.AcmClientPresigners", "serviceSpecificHttpConfig": "software.amazon.MyServiceHttpConfig", "serviceSpecificClientConfigClass": "ServiceConfiguration", + "customRetryPolicy": "software.amazon.MyServiceRetryPolicy", "verifiedSimpleMethods" : ["paginatedOperationWithResultKey"], "blacklistedSimpleMethods" : [ "eventStreamOperation" diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java index cc40ba6ee548..6999e25af250 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java @@ -20,6 +20,7 @@ import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.SdkAutoCloseable; import software.amazon.awssdk.utils.ToString; @@ -29,11 +30,10 @@ */ @SdkInternalApi public class LazyAwsCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable { - private final Supplier delegateConstructor; - private volatile AwsCredentialsProvider delegate; + private final Lazy delegate; private LazyAwsCredentialsProvider(Supplier delegateConstructor) { - this.delegateConstructor = delegateConstructor; + this.delegate = new Lazy<>(delegateConstructor); } public static LazyAwsCredentialsProvider create(Supplier delegateConstructor) { @@ -42,14 +42,7 @@ public static LazyAwsCredentialsProvider create(Supplier @Override public AwsCredentials resolveCredentials() { - if (delegate == null) { - synchronized (this) { - if (delegate == null) { - delegate = delegateConstructor.get(); - } - } - } - return delegate.resolveCredentials(); + return delegate.getValue().resolveCredentials(); } @Override @@ -60,7 +53,6 @@ public void close() { @Override public String toString() { return ToString.builder("LazyAwsCredentialsProvider") - .add("delegateConstructor", delegateConstructor) .add("delegate", delegate) .build(); } 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 222bfa890931..934ee86cc1f6 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 @@ -33,6 +33,7 @@ import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.regions.Region; @@ -113,10 +114,14 @@ protected AttributeMap serviceHttpConfig() { @Override protected final SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration configuration) { SdkClientConfiguration config = mergeServiceDefaults(configuration); + return config.merge(c -> c.option(AwsClientOption.AWS_REGION, resolveRegion(config)) .option(AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION, true) .option(AwsClientOption.CREDENTIALS_PROVIDER, DefaultCredentialsProvider.create()) - .option(SdkClientOption.RETRY_POLICY, AwsRetryPolicy.defaultRetryPolicy()) + .option(SdkClientOption.RETRY_POLICY, AwsRetryPolicy.defaultRetryPolicy() + .toBuilder() + .additionalRetryConditionsAllowed(false) + .build()) .option(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) .option(AwsClientOption.SERVICE_SIGNING_NAME, signingName()) .option(SdkClientOption.SERVICE_NAME, serviceName()) @@ -137,6 +142,7 @@ protected final SdkClientConfiguration finalizeChildConfiguration(SdkClientConfi .option(SdkClientOption.ENDPOINT, resolveEndpoint(configuration)) .option(SdkClientOption.EXECUTION_INTERCEPTORS, addAwsInterceptors(configuration)) .option(AwsClientOption.SIGNING_REGION, resolveSigningRegion(configuration)) + .option(SdkClientOption.RETRY_POLICY, resolveRetryPolicy(configuration)) .build(); return finalizeServiceConfiguration(config); } @@ -186,6 +192,11 @@ private Region regionFromDefaultProvider(SdkClientConfiguration config) { return DEFAULT_REGION_PROVIDER.getRegion(); } + private RetryPolicy resolveRetryPolicy(SdkClientConfiguration configuration) { + RetryPolicy policy = configuration.option(SdkClientOption.RETRY_POLICY); + return policy.additionalRetryConditionsAllowed() ? AwsRetryPolicy.addRetryConditions(policy) : policy; + } + @Override public final BuilderT region(Region region) { clientConfiguration.option(AwsClientOption.AWS_REGION, region); diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicy.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicy.java index ae08ef5c3dda..4899a2601b02 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicy.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicy.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.awscore.internal.AwsErrorCode; import software.amazon.awssdk.awscore.retry.conditions.RetryOnErrorCodeCondition; +import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.retry.conditions.OrRetryCondition; import software.amazon.awssdk.core.retry.conditions.RetryCondition; @@ -31,12 +32,38 @@ public final class AwsRetryPolicy { private AwsRetryPolicy() { } + /** + * Retrieve the {@link RetryCondition#defaultRetryCondition()} with AWS-specific conditions added. + */ public static RetryCondition defaultRetryCondition() { - return OrRetryCondition.create(RetryCondition.defaultRetryCondition(), - RetryOnErrorCodeCondition.create(AwsErrorCode.RETRYABLE_ERROR_CODES)); + return OrRetryCondition.create(RetryCondition.defaultRetryCondition(), awsRetryCondition()); } + /** + * Retrieve the {@link RetryPolicy#defaultRetryPolicy()} with AWS-specific conditions added. + */ public static RetryPolicy defaultRetryPolicy() { - return RetryPolicy.defaultRetryPolicy().toBuilder().retryCondition(defaultRetryCondition()).build(); + return forRetryMode(RetryMode.defaultRetryMode()); + } + + /** + * Retrieve the {@link RetryPolicy#defaultRetryPolicy()} with AWS-specific conditions added. This uses the specified + * {@link RetryMode} when constructing the {@link RetryPolicy}. + */ + public static RetryPolicy forRetryMode(RetryMode retryMode) { + return addRetryConditions(RetryPolicy.forRetryMode(retryMode)); + } + + /** + * Update the provided {@link RetryPolicy} to add AWS-specific conditions. + */ + public static RetryPolicy addRetryConditions(RetryPolicy condition) { + return condition.toBuilder() + .retryCondition(OrRetryCondition.create(condition.retryCondition(), awsRetryCondition())) + .build(); + } + + private static RetryOnErrorCodeCondition awsRetryCondition() { + return RetryOnErrorCodeCondition.create(AwsErrorCode.RETRYABLE_ERROR_CODES); } } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java index 1e4b9cfb3e8d..e7ff085f290d 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/utils/HttpTestUtils.java @@ -21,8 +21,8 @@ import java.util.concurrent.Executors; import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; import software.amazon.awssdk.core.retry.RetryPolicy; diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java index 74dce4e7cf9f..99f893676653 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java @@ -80,7 +80,10 @@ public static Aggregator aggregator() { * Get the default profile file, using the credentials file from "~/.aws/credentials", the config file from "~/.aws/config" * and the "default" profile. This default behavior can be customized using the * {@link ProfileFileSystemSetting#AWS_SHARED_CREDENTIALS_FILE}, {@link ProfileFileSystemSetting#AWS_CONFIG_FILE} and - * {@link ProfileFileSystemSetting#AWS_PROFILE} settings or by specifying a different profile file and profile name + * {@link ProfileFileSystemSetting#AWS_PROFILE} settings or by specifying a different profile file and profile name. + * + *

+ * The file is read each time this method is invoked. */ public static ProfileFile defaultProfileFile() { return ProfileFile.aggregator() diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java index 01084a1d6e11..c31bc4f16e70 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java @@ -93,5 +93,11 @@ public final class ProfileProperty { */ public static final String S3_US_EAST_1_REGIONAL_ENDPOINT = "s3_us_east_1_regional_endpoint"; + /** + * The "retry mode" to be used for clients created using the currently-configured profile. Values supported by all SDKs are + * "legacy" and "standard". See the {@code RetryMode} class JavaDoc for more information. + */ + public static final String RETRY_MODE = "retry_mode"; + private ProfileProperty() {} } diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProvider.java b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProvider.java index 72a50d2182eb..bd376c3b7c89 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProvider.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProvider.java @@ -27,7 +27,6 @@ */ @SdkProtectedApi public final class AwsProfileRegionProvider implements AwsRegionProvider { - private final String profileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); @Override diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java index 620f0a6de0be..3bfe3e9280c9 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java @@ -18,6 +18,7 @@ import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.ToString; /** @@ -26,29 +27,20 @@ */ @SdkProtectedApi public class LazyAwsRegionProvider implements AwsRegionProvider { - private final Supplier delegateConstructor; - private volatile AwsRegionProvider delegate; + private final Lazy delegate; public LazyAwsRegionProvider(Supplier delegateConstructor) { - this.delegateConstructor = delegateConstructor; + this.delegate = new Lazy<>(delegateConstructor); } @Override public Region getRegion() { - if (delegate == null) { - synchronized (this) { - if (delegate == null) { - delegate = delegateConstructor.get(); - } - } - } - return delegate.getRegion(); + return delegate.getValue().getRegion(); } @Override public String toString() { return ToString.builder("LazyAwsRegionProvider") - .add("delegateConstructor", delegateConstructor) .add("delegate", delegate) .build(); } diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProviderTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProviderTest.java index 34292b7fd74c..1a73d8025b4e 100644 --- a/core/regions/src/test/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProviderTest.java +++ b/core/regions/src/test/java/software/amazon/awssdk/regions/providers/AwsProfileRegionProviderTest.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; 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 aefa25ffb2eb..cc68c6c88aef 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 @@ -16,6 +16,9 @@ package software.amazon.awssdk.core; import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.utils.SystemSetting; /** @@ -149,7 +152,21 @@ public enum SdkSystemSetting implements SystemSetting { * the SDK to use the {@code s3.us-east-1.amazonaws.com} endpoint when using the {@code US_EAST_1} region instead of * the global {@code s3.amazonaws.com}. Using the regional endpoint is disabled by default. */ - AWS_S3_US_EAST_1_REGIONAL_ENDPOINT("aws.s3UseUsEast1RegionalEndpoint", null); + AWS_S3_US_EAST_1_REGIONAL_ENDPOINT("aws.s3UseUsEast1RegionalEndpoint", null), + + /** + * Which {@link RetryMode} to use for the default {@link RetryPolicy}, when one is not specified at the client level. + */ + AWS_RETRY_MODE("aws.retryMode", null), + + /** + * Defines the default value for {@link RetryPolicy.Builder#numRetries(Integer)}, if the retry count is not overridden in the + * retry policy configured via {@link ClientOverrideConfiguration.Builder#retryPolicy(RetryPolicy)}. This is one more than + * the number of retries, so aws.maxAttempts = 1 is 0 retries. + */ + AWS_MAX_ATTEMPTS("aws.maxAttempts", null), + + ; private final String systemProperty; private final String defaultValue; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index a495048573f3..7ed2a43d7912 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -188,7 +188,10 @@ protected SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration confi private SdkClientConfiguration mergeGlobalDefaults(SdkClientConfiguration configuration) { return configuration.merge(c -> c.option(EXECUTION_INTERCEPTORS, new ArrayList<>()) .option(ADDITIONAL_HTTP_HEADERS, new LinkedHashMap<>()) - .option(RETRY_POLICY, RetryPolicy.defaultRetryPolicy()) + .option(RETRY_POLICY, RetryPolicy.defaultRetryPolicy() + .toBuilder() + .additionalRetryConditionsAllowed(false) + .build()) .option(USER_AGENT_PREFIX, UserAgentUtils.getUserAgent()) .option(USER_AGENT_SUFFIX, "") .option(CRC32_FROM_COMPRESSED_DATA_ENABLED, false)); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java index 33d6df33ad57..5c3a4912317d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java @@ -26,6 +26,7 @@ import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.sync.ResponseTransformer; import software.amazon.awssdk.utils.AttributeMap; @@ -223,8 +224,6 @@ default Builder putHeader(String name, String value) { */ Builder retryPolicy(RetryPolicy retryPolicy); - RetryPolicy retryPolicy(); - /** * Configure the retry policy the should be used when handling failure cases. */ @@ -232,6 +231,17 @@ default Builder retryPolicy(Consumer retryPolicy) { return retryPolicy(RetryPolicy.builder().applyMutation(retryPolicy).build()); } + /** + * Configure the retry mode used to determine the retry policy that is used when handling failure cases. This is + * shorthand for {@code retryPolicy(RetryPolicy.forRetryMode(retryMode))}, and overrides any configured retry policy on + * this builder. + */ + default Builder retryPolicy(RetryMode retryMode) { + return retryPolicy(RetryPolicy.forRetryMode(retryMode)); + } + + RetryPolicy retryPolicy(); + /** * Configure a list of execution interceptors that will have access to read and modify the request and response objcets as * they are processed by the SDK. These will replace any interceptors configured previously with this method or diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/ExecutionContext.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/ExecutionContext.java index 82f1ed9c857d..e9375a75e14d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/ExecutionContext.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/ExecutionContext.java @@ -21,7 +21,6 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptorChain; import software.amazon.awssdk.core.interceptor.InterceptorContext; import software.amazon.awssdk.core.signer.Signer; -import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -37,10 +36,10 @@ public final class ExecutionContext implements ToCopyableBuilder tryAcquire(int amountToAcquire) { + Validate.isTrue(amountToAcquire >= 0, "Amount must not be negative."); + + if (amountToAcquire == 0) { + return Optional.of(Capacity.builder() + .capacityAcquired(0) + .capacityRemaining(capacity.get()) + .build()); + } + + int currentCapacity; + int newCapacity; + do { + currentCapacity = capacity.get(); + newCapacity = currentCapacity - amountToAcquire; + + if (newCapacity < 0) { + return Optional.empty(); + } + } while (!capacity.compareAndSet(currentCapacity, newCapacity)); + + return Optional.of(Capacity.builder() + .capacityAcquired(amountToAcquire) + .capacityRemaining(newCapacity) + .build()); + } + + /** + * Release a certain number of tokens back to this bucket. If this number of tokens would exceed the maximum number of tokens + * configured for the bucket, the bucket is instead set to the maximum value and the additional tokens are discarded. + */ + public void release(int amountToRelease) { + Validate.isTrue(amountToRelease >= 0, "Amount must not be negative."); + + if (amountToRelease == 0) { + return; + } + + int currentCapacity; + int newCapacity; + do { + currentCapacity = capacity.get(); + + if (currentCapacity == maxCapacity) { + return; + } + + newCapacity = Math.min(currentCapacity + amountToRelease, maxCapacity); + } while (!capacity.compareAndSet(currentCapacity, newCapacity)); + } + + /** + * Retrieve a snapshot of the current number of tokens in the bucket. Because this number is constantly changing, it's + * recommended to refer to the {@link Capacity#capacityRemaining()} returned by the {@link #tryAcquire(int)} method whenever + * possible. + */ + public int currentCapacity() { + return capacity.get(); + } + + /** + * Retrieve the maximum capacity of the bucket configured when the bucket was created. + */ + public int maxCapacity() { + return maxCapacity; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TokenBucket that = (TokenBucket) o; + + if (maxCapacity != that.maxCapacity) { + return false; + } + return capacity.get() == that.capacity.get(); + } + + @Override + public int hashCode() { + int result = maxCapacity; + result = 31 * result + capacity.get(); + return result; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java index 7ede52beef87..d1784ebf9058 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonAsyncHttpClient.java @@ -18,7 +18,6 @@ import static software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder.async; import java.util.concurrent.CompletableFuture; - import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.Response; @@ -42,8 +41,6 @@ import software.amazon.awssdk.core.internal.http.pipeline.stages.MergeCustomQueryParamsStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.SigningStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.UnwrapResponseContainer; -import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.core.internal.util.ThrowableUtils; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.utils.SdkAutoCloseable; @@ -57,16 +54,9 @@ public final class AmazonAsyncHttpClient implements SdkAutoCloseable { public AmazonAsyncHttpClient(SdkClientConfiguration clientConfiguration) { this.httpClientDependencies = HttpClientDependencies.builder() .clientConfiguration(clientConfiguration) - .capacityManager(createCapacityManager()) .build(); } - private CapacityManager createCapacityManager() { - // When enabled, total retry capacity is computed based on retry cost and desired number of retries. - // TODO: Allow customers to configure throttled retries (https://github.com/aws/aws-sdk-java-v2/issues/17) - return new CapacityManager(SdkDefaultRetrySetting.RETRY_THROTTLING_COST * SdkDefaultRetrySetting.THROTTLED_RETRIES); - } - /** * Shuts down this HTTP client object, releasing any resources that might be held open. This is * an optional method, and callers are not expected to call it, but can if they want to diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java index 2bfa3d352f84..38feba879847 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/AmazonSyncHttpClient.java @@ -44,8 +44,6 @@ import software.amazon.awssdk.core.internal.http.pipeline.stages.SigningStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.UnwrapResponseContainer; -import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpFullResponse; import software.amazon.awssdk.utils.SdkAutoCloseable; @@ -59,16 +57,9 @@ public final class AmazonSyncHttpClient implements SdkAutoCloseable { public AmazonSyncHttpClient(SdkClientConfiguration clientConfiguration) { this.httpClientDependencies = HttpClientDependencies.builder() .clientConfiguration(clientConfiguration) - .capacityManager(createCapacityManager()) .build(); } - private CapacityManager createCapacityManager() { - // When enabled, total retry capacity is computed based on retry cost and desired number of retries. - // TODO: Allow customers to configure throttled retries (https://github.com/aws/aws-sdk-java-v2/issues/17) - return new CapacityManager(SdkDefaultRetrySetting.RETRY_THROTTLING_COST * SdkDefaultRetrySetting.THROTTLED_RETRIES); - } - /** * Shuts down this HTTP client object, releasing any resources that might be held open. This is * an optional method, and callers are not expected to call it, but can if they want to diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java index 2ba66fbca45c..8680fcc059f2 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/HttpClientDependencies.java @@ -17,13 +17,13 @@ import static software.amazon.awssdk.utils.Validate.paramNotNull; +import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.SdkGlobalTime; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder; import software.amazon.awssdk.core.internal.retry.ClockSkewAdjuster; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.utils.SdkAutoCloseable; /** @@ -32,9 +32,8 @@ */ @SdkInternalApi public final class HttpClientDependencies implements SdkAutoCloseable { - private final ClockSkewAdjuster clockSkewAdjuster = new ClockSkewAdjuster(); + private final ClockSkewAdjuster clockSkewAdjuster; private final SdkClientConfiguration clientConfiguration; - private final CapacityManager capacityManager; /** * Time offset may be mutated by {@link RequestPipeline} implementations if a clock skew is detected. @@ -42,8 +41,8 @@ public final class HttpClientDependencies implements SdkAutoCloseable { private volatile int timeOffset = SdkGlobalTime.getGlobalTimeOffset(); private HttpClientDependencies(Builder builder) { + this.clockSkewAdjuster = builder.clockSkewAdjuster != null ? builder.clockSkewAdjuster : new ClockSkewAdjuster(); this.clientConfiguration = paramNotNull(builder.clientConfiguration, "ClientConfiguration"); - this.capacityManager = paramNotNull(builder.capacityManager, "CapacityManager"); } public static Builder builder() { @@ -54,13 +53,6 @@ public SdkClientConfiguration clientConfiguration() { return clientConfiguration; } - /** - * @return CapacityManager object used for retry throttling. - */ - public CapacityManager retryCapacity() { - return capacityManager; - } - /** * @return The adjuster used for adjusting the {@link #timeOffset} for this client. */ @@ -92,18 +84,25 @@ public void close() { * Builder for {@link HttpClientDependencies}. */ public static class Builder { + private ClockSkewAdjuster clockSkewAdjuster; private SdkClientConfiguration clientConfiguration; - private CapacityManager capacityManager; private Builder() {} + public Builder clockSkewAdjuster(ClockSkewAdjuster clockSkewAdjuster) { + this.clockSkewAdjuster = clockSkewAdjuster; + return this; + } + public Builder clientConfiguration(SdkClientConfiguration clientConfiguration) { this.clientConfiguration = clientConfiguration; return this; } - public Builder capacityManager(CapacityManager capacityManager) { - this.capacityManager = capacityManager; + public Builder clientConfiguration(Consumer clientConfiguration) { + SdkClientConfiguration.Builder c = SdkClientConfiguration.builder(); + clientConfiguration.accept(c); + clientConfiguration(c.build()); return this; } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java index 1adc164bc07d..814bb85917a0 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java @@ -15,51 +15,36 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.io.IOException; import java.time.Duration; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.Response; -import software.amazon.awssdk.core.SdkStandardLogger; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.client.config.SdkClientOption; -import software.amazon.awssdk.core.exception.NonRetryableException; import software.amazon.awssdk.core.exception.SdkException; -import software.amazon.awssdk.core.internal.InternalCoreExecutionAttribute; 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.retry.ClockSkewAdjuster; -import software.amazon.awssdk.core.internal.retry.RetryHandler; -import software.amazon.awssdk.core.internal.util.CapacityManager; -import software.amazon.awssdk.core.internal.util.ThrowableUtils; -import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.RetryableStageHelper; import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpFullResponse; import software.amazon.awssdk.utils.CompletableFutureUtils; /** - * Wrapper around the pipeline for a single request to provide retry functionality. + * Wrapper around the pipeline for a single request to provide retry, clockskew and request throttling functionality. */ @SdkInternalApi public final class AsyncRetryableStage implements RequestPipeline>> { - private static final Logger log = LoggerFactory.getLogger(AsyncRetryableStage.class); - private final TransformingAsyncResponseHandler> responseHandler; private final RequestPipeline>> requestPipeline; private final ScheduledExecutorService scheduledExecutor; private final HttpClientDependencies dependencies; - private final CapacityManager retryCapacity; - private final RetryPolicy retryPolicy; public AsyncRetryableStage(TransformingAsyncResponseHandler> responseHandler, HttpClientDependencies dependencies, @@ -67,145 +52,96 @@ public AsyncRetryableStage(TransformingAsyncResponseHandler> r this.responseHandler = responseHandler; this.dependencies = dependencies; this.scheduledExecutor = dependencies.clientConfiguration().option(SdkClientOption.SCHEDULED_EXECUTOR_SERVICE); - this.retryPolicy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_POLICY); - this.retryCapacity = dependencies.retryCapacity(); this.requestPipeline = requestPipeline; } @Override - public CompletableFuture> execute(SdkHttpFullRequest request, RequestExecutionContext context) throws - Exception { - return new RetryExecutor(request, context).execute(); + public CompletableFuture> execute(SdkHttpFullRequest request, + RequestExecutionContext context) throws Exception { + return new RetryingExecutor(request, context).execute(); } - /** - * Created for every request to encapsulate mutable state between retries. - */ - private class RetryExecutor { - - private final SdkHttpFullRequest request; - private final RequestExecutionContext context; - private final RetryHandler retryHandler; + private class RetryingExecutor { private final AsyncRequestBody originalRequestBody; + private final RequestExecutionContext context; + private final RetryableStageHelper retryableStageHelper; - private int requestCount = 0; - - private RetryExecutor(SdkHttpFullRequest request, RequestExecutionContext context) { - this.request = request; - this.context = context; + private RetryingExecutor(SdkHttpFullRequest request, RequestExecutionContext context) { this.originalRequestBody = context.requestProvider(); - this.retryHandler = new RetryHandler(retryPolicy, retryCapacity); + this.context = context; + this.retryableStageHelper = new RetryableStageHelper(request, context, dependencies); } public CompletableFuture> execute() throws Exception { CompletableFuture> future = new CompletableFuture<>(); - return execute(future); + maybeAttemptExecute(future); + return future; } - public CompletableFuture> execute(CompletableFuture> future) throws Exception { - beforeExecute(); - CompletableFuture> executeFuture = doExecute(); - executeFuture.whenComplete((resp, err) -> retryIfNeeded(future, resp, err)); - return CompletableFutureUtils.forwardExceptionTo(future, executeFuture); - } + public void maybeAttemptExecute(CompletableFuture> future) { + retryableStageHelper.startingAttempt(); - private void retryIfNeeded(CompletableFuture> future, - Response resp, - Throwable err) { - if (future.isDone()) { + if (!retryableStageHelper.retryPolicyAllowsRetry()) { + future.completeExceptionally(retryableStageHelper.retryPolicyDisallowedRetryException()); return; } - try { - if (resp != null) { - retryResponseIfNeeded(resp, future); - } else { - if (err instanceof CompletionException) { - err = err.getCause(); - } - SdkException sdkException = ThrowableUtils.asSdkException(err); - retryErrorIfNeeded(sdkException, future); - } - } catch (Throwable t) { - future.completeExceptionally(t); - } - } + if (retryableStageHelper.getAttemptNumber() > 1) { + // We failed the last attempt, but will retry. The response handler wants to know when that happens. + responseHandler.onError(retryableStageHelper.getLastException()); - private void retryResponseIfNeeded(Response resp, CompletableFuture> future) { - if (resp.isSuccess()) { - retryHandler.releaseRetryCapacity(); - future.complete(resp); - return; + // Reset the request provider to the original one before retries, in case it was modified downstream. + context.requestProvider(originalRequestBody); } - SdkException err = resp.exception(); - - ClockSkewAdjuster clockSkewAdjuster = dependencies.clockSkewAdjuster(); - if (clockSkewAdjuster.shouldAdjust(err)) { - dependencies.updateTimeOffset(clockSkewAdjuster.getAdjustmentInSeconds(resp.httpResponse())); - } - - if (shouldRetry(resp.httpResponse(), resp.exception())) { - // We only notify onError if we are retrying the request. - // Otherwise we rely on the generated code in the in the - // client class to forward exception to the handler's - // exceptionOcurred method. - responseHandler.onError(err); - retryHandler.setLastRetriedException(err); - executeRetry(future); + Duration backoffDelay = retryableStageHelper.getBackoffDelay(); + if (!backoffDelay.isZero()) { + retryableStageHelper.logBackingOff(backoffDelay); + scheduledExecutor.schedule(() -> attemptExecute(future), backoffDelay.toMillis(), MILLISECONDS); } else { - future.completeExceptionally(err); + attemptExecute(future); } } - private void retryErrorIfNeeded(SdkException err, CompletableFuture> future) { - if (err instanceof NonRetryableException) { - future.completeExceptionally(err); + private void attemptExecute(CompletableFuture> future) { + CompletableFuture> responseFuture; + try { + retryableStageHelper.logSendingRequest(); + responseFuture = requestPipeline.execute(retryableStageHelper.requestToSend(), context); + + // If the result future fails, go ahead and fail the response future. + CompletableFutureUtils.forwardExceptionTo(future, responseFuture); + } catch (SdkException | IOException e) { + maybeRetryExecute(future, e); + return; + } catch (Throwable e) { + future.completeExceptionally(e); return; } - if (shouldRetry(null, err)) { - // We only notify onError if we are retrying the request. - // Otherwise we rely on the generated code in the in the client - // class to forward exception to the handler's exceptionOcurred - // method. - responseHandler.onError(err); - retryHandler.setLastRetriedException(err); - executeRetry(future); - } else { - future.completeExceptionally(err); - } - } + responseFuture.whenComplete((response, exception) -> { - private boolean shouldRetry(SdkHttpFullResponse httpResponse, SdkException exception) { - return retryHandler.shouldRetry(httpResponse, request, context, exception, requestCount); - } + if (exception != null) { + maybeRetryExecute(future, exception); + return; + } - private void executeRetry(CompletableFuture> future) { - Duration delay = retryHandler.computeDelayBeforeNextRetry(); + retryableStageHelper.setLastResponse(response.httpResponse()); - SdkStandardLogger.REQUEST_LOGGER.debug(() -> "Retryable error detected, will retry in " + delay.toMillis() + "ms," - + " attempt number " + requestCount); - scheduledExecutor.schedule(() -> { - execute(future); - return null; - }, delay.toMillis(), TimeUnit.MILLISECONDS); - } + if (!response.isSuccess()) { + retryableStageHelper.adjustClockIfClockSkew(response); + maybeRetryExecute(future, response.exception()); + return; + } - private void beforeExecute() { - retryHandler.retryCapacityConsumed(false); - ++requestCount; + retryableStageHelper.attemptSucceeded(); + future.complete(response); + }); } - private CompletableFuture> doExecute() throws Exception { - SdkStandardLogger.REQUEST_LOGGER.debug(() -> (retryHandler.isRetry() ? "Retrying " : "Sending ") + - "Request: " + request); - - // Before each attempt, Modify the context to use original request body provider - context.requestProvider(originalRequestBody); - - context.executionAttributes().putAttribute(InternalCoreExecutionAttribute.EXECUTION_ATTEMPT, requestCount); - return requestPipeline.execute(retryHandler.addRetryInfoHeader(request, requestCount), context); + private void maybeRetryExecute(CompletableFuture> future, Throwable exception) { + retryableStageHelper.setLastException(exception); + maybeAttemptExecute(future); } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java index 2f1ba42f4195..3854e42c5dc6 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java @@ -18,144 +18,65 @@ import java.io.IOException; import java.time.Duration; import java.util.concurrent.TimeUnit; - import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.Response; -import software.amazon.awssdk.core.SdkStandardLogger; -import software.amazon.awssdk.core.client.config.SdkClientOption; -import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.core.internal.http.HttpClientDependencies; -import software.amazon.awssdk.core.internal.http.InterruptMonitor; import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; import software.amazon.awssdk.core.internal.http.pipeline.RequestToResponsePipeline; -import software.amazon.awssdk.core.internal.retry.ClockSkewAdjuster; -import software.amazon.awssdk.core.internal.retry.RetryHandler; -import software.amazon.awssdk.core.internal.util.CapacityManager; -import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.RetryableStageHelper; import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.utils.Logger; /** - * Wrapper around the pipeline for a single request to provide retry functionality. + * Wrapper around the pipeline for a single request to provide retry, clock-skew and request throttling functionality. */ @SdkInternalApi public final class RetryableStage implements RequestToResponsePipeline { - - private static final Logger log = Logger.loggerFor(RetryableStage.class); - private final RequestPipeline> requestPipeline; - private final HttpClientDependencies dependencies; - private final CapacityManager retryCapacity; - private final RetryPolicy retryPolicy; public RetryableStage(HttpClientDependencies dependencies, RequestPipeline> requestPipeline) { this.dependencies = dependencies; - this.retryCapacity = dependencies.retryCapacity(); - this.retryPolicy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_POLICY); this.requestPipeline = requestPipeline; } public Response execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - return new RetryExecutor(request, context).execute(); - } - - /** - * Created for every request to encapsulate mutable state between retries. - */ - private class RetryExecutor { - - private final SdkHttpFullRequest request; - private final RequestExecutionContext context; - private final RetryHandler retryHandler; - - private int requestCount = 0; - - private RetryExecutor(SdkHttpFullRequest request, RequestExecutionContext context) { - this.request = request; - this.context = context; - this.retryHandler = new RetryHandler(retryPolicy, retryCapacity); - } - - public Response execute() throws Exception { - while (true) { - try { - beforeExecute(); - Response response = doExecute(); - if (response.isSuccess()) { - retryHandler.releaseRetryCapacity(); - return response; - } else { - retryHandler.setLastRetriedException(handleUnmarshalledException(response)); - } - } catch (SdkClientException | IOException e) { - retryHandler.setLastRetriedException(handleThrownException(e)); - } - } - } + RetryableStageHelper retryableStageHelper = new RetryableStageHelper(request, context, dependencies); - private void beforeExecute() throws InterruptedException { - retryHandler.retryCapacityConsumed(false); - InterruptMonitor.checkInterrupted(); - ++requestCount; - } + while (true) { + retryableStageHelper.startingAttempt(); - private Response doExecute() throws Exception { - if (retryHandler.isRetry()) { - doPauseBeforeRetry(); + if (!retryableStageHelper.retryPolicyAllowsRetry()) { + throw retryableStageHelper.retryPolicyDisallowedRetryException(); } - SdkStandardLogger.REQUEST_LOGGER.debug(() -> (retryHandler.isRetry() ? "Retrying " : "Sending ") + "Request: " + - request); - - return requestPipeline.execute(retryHandler.addRetryInfoHeader(request, requestCount), context); - } - - private SdkException handleUnmarshalledException(Response response) { - SdkException exception = response.exception(); - - ClockSkewAdjuster clockSkewAdjuster = dependencies.clockSkewAdjuster(); - if (clockSkewAdjuster.shouldAdjust(exception)) { - dependencies.updateTimeOffset(clockSkewAdjuster.getAdjustmentInSeconds(response.httpResponse())); + Duration backoffDelay = retryableStageHelper.getBackoffDelay(); + if (!backoffDelay.isZero()) { + retryableStageHelper.logBackingOff(backoffDelay); + TimeUnit.MILLISECONDS.sleep(backoffDelay.toMillis()); } - if (!retryHandler.shouldRetry(response.httpResponse(), request, context, exception, requestCount)) { - throw exception; + Response response; + try { + retryableStageHelper.logSendingRequest(); + response = requestPipeline.execute(retryableStageHelper.requestToSend(), context); + } catch (SdkException | IOException e) { + retryableStageHelper.setLastException(e); + continue; } - return exception; - } - - private SdkException handleThrownException(Exception e) { - SdkClientException sdkClientException = e instanceof SdkClientException ? - (SdkClientException) e : SdkClientException.builder() - .message("Unable to execute HTTP request: " + e.getMessage()) - .cause(e) - .build(); - boolean willRetry = retryHandler.shouldRetry(null, request, context, sdkClientException, requestCount); - - log.debug(() -> sdkClientException.getMessage() + (willRetry ? " Request will be retried." : ""), e); + retryableStageHelper.setLastResponse(response.httpResponse()); - if (!willRetry) { - throw sdkClientException; + if (!response.isSuccess()) { + retryableStageHelper.adjustClockIfClockSkew(response); + retryableStageHelper.setLastException(response.exception()); + continue; } - return sdkClientException; - } - - /** - * Sleep for a period of time on failed request to avoid flooding a service with retries. - */ - private void doPauseBeforeRetry() throws InterruptedException { - int retriesAttempted = requestCount - 2; - Duration delay = retryHandler.computeDelayBeforeNextRetry(); - - SdkStandardLogger.REQUEST_LOGGER.debug(() -> "Retryable error detected, will retry in " + delay.toMillis() + "ms," - + " attempt number " + retriesAttempted); - TimeUnit.MILLISECONDS.sleep(delay.toMillis()); + retryableStageHelper.attemptSucceeded(); + return response; } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java new file mode 100644 index 000000000000..8ad4dc3df0b3 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/RetryableStageHelper.java @@ -0,0 +1,239 @@ +/* + * 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.internal.http.pipeline.stages.utils; + +import static software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting.SDK_RETRY_INFO_HEADER; + +import java.time.Duration; +import java.util.concurrent.CompletionException; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.Response; +import software.amazon.awssdk.core.SdkStandardLogger; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.exception.NonRetryableException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.internal.InternalCoreExecutionAttribute; +import software.amazon.awssdk.core.internal.http.HttpClientDependencies; +import software.amazon.awssdk.core.internal.http.RequestExecutionContext; +import software.amazon.awssdk.core.internal.http.pipeline.stages.AsyncRetryableStage; +import software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage; +import software.amazon.awssdk.core.internal.retry.ClockSkewAdjuster; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.RetryPolicyContext; +import software.amazon.awssdk.core.retry.RetryUtils; +import software.amazon.awssdk.core.retry.conditions.TokenBucketRetryCondition; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpResponse; + +/** + * Contains the logic shared by {@link RetryableStage} and {@link AsyncRetryableStage} when querying and interacting with a + * {@link RetryPolicy}. + */ +@SdkInternalApi +public class RetryableStageHelper { + private final SdkHttpFullRequest request; + private final RequestExecutionContext context; + private final RetryPolicy retryPolicy; + private final HttpClientDependencies dependencies; + + private int attemptNumber = 0; + private SdkHttpResponse lastResponse = null; + private SdkException lastException = null; + private Duration lastBackoffDelay = null; + + public RetryableStageHelper(SdkHttpFullRequest request, + RequestExecutionContext context, + HttpClientDependencies dependencies) { + this.request = request; + this.context = context; + this.retryPolicy = dependencies.clientConfiguration().option(SdkClientOption.RETRY_POLICY); + this.dependencies = dependencies; + } + + /** + * Invoke when starting a request attempt, before querying the retry policy. + */ + public void startingAttempt() { + ++attemptNumber; + context.executionAttributes().putAttribute(InternalCoreExecutionAttribute.EXECUTION_ATTEMPT, attemptNumber); + } + + /** + * Returns true if the retry policy allows this attempt. This will always return true if the current attempt is not a retry + * (i.e. it's the first request in the execution). + */ + public boolean retryPolicyAllowsRetry() { + if (isInitialAttempt()) { + return true; + } + + if (lastException instanceof NonRetryableException) { + return false; + } + + RetryPolicyContext context = retryPolicyContext(true); + + boolean willRetry = retryPolicy.aggregateRetryCondition().shouldRetry(context); + if (!willRetry) { + retryPolicy.aggregateRetryCondition().requestWillNotBeRetried(context); + } + + return willRetry; + } + + /** + * Return the exception that should be thrown, because the retry policy did not allow the request to be retried. + */ + public SdkException retryPolicyDisallowedRetryException() { + return lastException; + } + + /** + * Get the amount of time that the request should be delayed before being sent. This may be {@link Duration#ZERO}, such as + * for the first request in the request series. + */ + public Duration getBackoffDelay() { + Duration result; + if (isInitialAttempt()) { + result = Duration.ZERO; + } else { + RetryPolicyContext context = retryPolicyContext(true); + if (RetryUtils.isThrottlingException(lastException)) { + result = retryPolicy.throttlingBackoffStrategy().computeDelayBeforeNextRetry(context); + } else { + result = retryPolicy.backoffStrategy().computeDelayBeforeNextRetry(context); + } + } + lastBackoffDelay = result; + return result; + } + + /** + * Log a message to the user at the debug level to indicate how long we will wait before retrying the request. + */ + public void logBackingOff(Duration backoffDelay) { + SdkStandardLogger.REQUEST_LOGGER.debug(() -> "Retryable error detected. Will retry in " + + backoffDelay.toMillis() + "ms. Request attempt number " + + attemptNumber); + } + + /** + * Retrieve the request to send to the service, including any detailed retry information headers. + */ + public SdkHttpFullRequest requestToSend() { + Integer availableRetryCapacity = TokenBucketRetryCondition.getCapacityForExecution(context.executionAttributes()) + .map(TokenBucketRetryCondition.Capacity::capacityRemaining) + .orElse(null); + + return request.toBuilder() + .putHeader(SDK_RETRY_INFO_HEADER, + String.format("%s/%s/%s", + attemptNumber - 1, + lastBackoffDelay.toMillis(), + availableRetryCapacity != null ? availableRetryCapacity : "")) + .build(); + } + + /** + * Log a message to the user at the debug level to indicate that we are sending the request to the service. + */ + public void logSendingRequest() { + SdkStandardLogger.REQUEST_LOGGER.debug(() -> (isInitialAttempt() ? "Sending" : "Retrying") + " Request: " + request); + } + + /** + * Adjust the client-side clock skew if the provided response indicates that there is a large skew between the client and + * service. This will allow a retried request to be signed with what is likely to be a more accurate time. + */ + public void adjustClockIfClockSkew(Response response) { + ClockSkewAdjuster clockSkewAdjuster = dependencies.clockSkewAdjuster(); + if (!response.isSuccess() && clockSkewAdjuster.shouldAdjust(response.exception())) { + dependencies.updateTimeOffset(clockSkewAdjuster.getAdjustmentInSeconds(response.httpResponse())); + } + } + + /** + * Notify the retry policy that the request attempt succeeded. + */ + public void attemptSucceeded() { + retryPolicy.aggregateRetryCondition().requestSucceeded(retryPolicyContext(false)); + } + + /** + * Retrieve the current attempt number, updated whenever {@link #startingAttempt()} is invoked. + */ + public int getAttemptNumber() { + return attemptNumber; + } + + /** + * Retrieve the last call failure exception encountered by this execution, updated whenever {@link #setLastException} is + * invoked. + */ + public SdkException getLastException() { + return lastException; + } + + /** + * Update the {@link #getLastException()} value for this helper. This will be used to determine whether the request should + * be retried. + */ + public void setLastException(Throwable lastException) { + if (lastException instanceof CompletionException) { + setLastException(lastException.getCause()); + } else if (lastException instanceof SdkException) { + this.lastException = (SdkException) lastException; + } else { + this.lastException = SdkClientException.create("Unable to execute HTTP request: " + lastException.getMessage(), + lastException); + } + } + + /** + * Set the last HTTP response returned by the service. This will be used to determine whether the request should be retried. + */ + public void setLastResponse(SdkHttpResponse lastResponse) { + this.lastResponse = lastResponse; + } + + private boolean isInitialAttempt() { + return attemptNumber == 1; + } + + private RetryPolicyContext retryPolicyContext(boolean isBeforeAttemptSent) { + return RetryPolicyContext.builder() + .request(request) + .originalRequest(context.originalRequest()) + .exception(lastException) + .retriesAttempted(retriesAttemptedSoFar(isBeforeAttemptSent)) + .executionAttributes(context.executionAttributes()) + .httpStatusCode(lastResponse == null ? null : lastResponse.statusCode()) + .build(); + } + + /** + * Retrieve the number of retries sent so far in the request execution. This depends on whether or not we've actually + * sent the request yet during this attempt. + * + * Assuming we're executing attempt 3, the number of retries attempted varies based on whether the request has been sent to + * the service yet. Before we send the request, the number of retries is 1 (from attempt 2). After we send the request, the + * number of retries is 2 (from attempt 2 and attempt 3). + */ + private int retriesAttemptedSoFar(boolean isBeforeAttemptSent) { + return Math.max(0, isBeforeAttemptSent ? attemptNumber - 2 : attemptNumber - 1); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/DefaultTokenBucketExceptionCostFunction.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/DefaultTokenBucketExceptionCostFunction.java new file mode 100644 index 000000000000..e9d553e79047 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/DefaultTokenBucketExceptionCostFunction.java @@ -0,0 +1,98 @@ +/* + * 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.internal.retry; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.retry.RetryUtils; +import software.amazon.awssdk.core.retry.conditions.TokenBucketExceptionCostFunction; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; + +@SdkInternalApi +public class DefaultTokenBucketExceptionCostFunction implements TokenBucketExceptionCostFunction { + private final Integer throttlingExceptionCost; + private final int defaultExceptionCost; + + private DefaultTokenBucketExceptionCostFunction(Builder builder) { + this.throttlingExceptionCost = builder.throttlingExceptionCost; + this.defaultExceptionCost = Validate.paramNotNull(builder.defaultExceptionCost, "defaultExceptionCost"); + } + + @Override + public Integer apply(SdkException e) { + if (throttlingExceptionCost != null && RetryUtils.isThrottlingException(e)) { + return throttlingExceptionCost; + } + + return defaultExceptionCost; + } + + @Override + public String toString() { + return ToString.builder("TokenBucketExceptionCostCalculator") + .add("throttlingExceptionCost", throttlingExceptionCost) + .add("defaultExceptionCost", defaultExceptionCost) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultTokenBucketExceptionCostFunction that = (DefaultTokenBucketExceptionCostFunction) o; + + if (defaultExceptionCost != that.defaultExceptionCost) { + return false; + } + + return throttlingExceptionCost != null ? + throttlingExceptionCost.equals(that.throttlingExceptionCost) : + that.throttlingExceptionCost == null; + } + + @Override + public int hashCode() { + int result = throttlingExceptionCost != null ? throttlingExceptionCost.hashCode() : 0; + result = 31 * result + defaultExceptionCost; + return result; + } + + public static final class Builder implements TokenBucketExceptionCostFunction.Builder { + private Integer throttlingExceptionCost; + private Integer defaultExceptionCost; + + public TokenBucketExceptionCostFunction.Builder throttlingExceptionCost(int cost) { + this.throttlingExceptionCost = cost; + return this; + } + + public TokenBucketExceptionCostFunction.Builder defaultExceptionCost(int cost) { + this.defaultExceptionCost = cost; + return this; + } + + public TokenBucketExceptionCostFunction build() { + return new DefaultTokenBucketExceptionCostFunction(this); + } + + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/RetryHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/RetryHandler.java deleted file mode 100644 index c4b0ccccdd3e..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/RetryHandler.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.internal.retry; - -import static java.util.Collections.singletonList; - -import java.time.Duration; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.core.exception.SdkException; -import software.amazon.awssdk.core.internal.http.RequestExecutionContext; -import software.amazon.awssdk.core.internal.util.CapacityManager; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.core.retry.RetryPolicyContext; -import software.amazon.awssdk.core.retry.RetryUtils; -import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpFullResponse; - -@SdkInternalApi -public final class RetryHandler { - - public static final String HEADER_SDK_RETRY_INFO = "amz-sdk-retry"; - - private final RetryPolicy retryPolicy; - private final CapacityManager retryCapacity; - - private Duration lastBackoffDelay = Duration.ZERO; - private boolean retryCapacityConsumed; - private RetryPolicyContext retryPolicyContext; - private SdkException lastRetriedException; - - public RetryHandler(RetryPolicy retryPolicy, - CapacityManager retryCapacity) { - this.retryPolicy = retryPolicy; - this.retryCapacity = retryCapacity; - } - - public boolean shouldRetry(SdkHttpFullResponse httpResponse, - SdkHttpFullRequest request, - RequestExecutionContext context, - SdkException exception, - int requestCount) { - - int retriesAttempted = requestCount - 1; - - // Do not use retry capacity for throttling exceptions - if (!RetryUtils.isThrottlingException(exception)) { - // See if we have enough available retry capacity to be able to execute this retry attempt. - if (!retryCapacity.acquire(SdkDefaultRetrySetting.RETRY_THROTTLING_COST)) { - return false; - } - this.retryCapacityConsumed = true; - } - - this.retryPolicyContext = RetryPolicyContext.builder() - .request(request) - .originalRequest(context.originalRequest()) - .exception(exception) - .retriesAttempted(retriesAttempted) - .executionAttributes(context.executionAttributes()) - .httpStatusCode(httpResponse == null ? null : httpResponse.statusCode()) - .build(); - // Finally, pass all the context information to the RetryCondition and let it decide whether it should be retried. - if (!retryPolicy.retryCondition().shouldRetry(retryPolicyContext)) { - // If the retry policy fails we immediately return consumed capacity to the pool. - if (retryCapacityConsumed) { - retryCapacity.release(SdkDefaultRetrySetting.RETRY_THROTTLING_COST); - } - return false; - } - - return true; - } - - /** - * If this was a successful retry attempt we'll release the full retry capacity that the attempt originally consumed. If - * this was a successful initial request we release a lesser amount. - */ - public void releaseRetryCapacity() { - if (isRetry() && retryCapacityConsumed) { - retryCapacity.release(SdkDefaultRetrySetting.RETRY_THROTTLING_COST); - } else { - retryCapacity.release(); - } - } - - /** - * Computes the delay before the next retry should be attempted based on the retry policy context. - * @return long value of how long to wait - */ - public Duration computeDelayBeforeNextRetry() { - if (RetryUtils.isThrottlingException(retryPolicyContext.exception())) { - lastBackoffDelay = retryPolicy.throttlingBackoffStrategy().computeDelayBeforeNextRetry(retryPolicyContext); - } else { - lastBackoffDelay = retryPolicy.backoffStrategy().computeDelayBeforeNextRetry(retryPolicyContext); - } - - return lastBackoffDelay; - } - - /** - * Sets whether retry capacity has been consumed for this request - */ - public void retryCapacityConsumed(boolean retryCapacityConsumed) { - this.retryCapacityConsumed = retryCapacityConsumed; - } - - /** - * Add the {@value HEADER_SDK_RETRY_INFO} header to the request. Contains metadata about request count, - * backoff, and retry capacity. - * - * @return Request with retry info header added. - */ - public SdkHttpFullRequest addRetryInfoHeader(SdkHttpFullRequest request, int requestCount) throws Exception { - int availableRetryCapacity = retryCapacity.availableCapacity(); - return request.toBuilder() - .putHeader(HEADER_SDK_RETRY_INFO, - singletonList(String.format("%s/%s/%s", - requestCount - 1, - lastBackoffDelay.toMillis(), - availableRetryCapacity >= 0 ? availableRetryCapacity : ""))) - .build(); - } - - /** - * Sets the last exception the has been seen by the retry handler. - * @param exception - the last exception seen - */ - public void setLastRetriedException(SdkException exception) { - this.lastRetriedException = exception; - } - - /** - * Whether or not the current request is a retry. True if the original request has been retried at least one time. - */ - public boolean isRetry() { - return lastRetriedException != null; - } -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java index 32a7a2a6cbae..9536b6a462d5 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetrySetting.java @@ -22,26 +22,41 @@ import java.util.HashSet; import java.util.Set; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException; import software.amazon.awssdk.core.exception.RetryableException; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.core.retry.conditions.TokenBucketExceptionCostFunction; import software.amazon.awssdk.http.HttpStatusCode; +import software.amazon.awssdk.utils.Validate; @SdkInternalApi public final class SdkDefaultRetrySetting { + public static final String SDK_RETRY_INFO_HEADER = "amz-sdk-retry"; - /** - * When throttled retries are enabled, each retry attempt will consume this much capacity. - * Successful retry attempts will release this capacity back to the pool while failed retries - * will not. Successful initial (non-retry) requests will always release 1 capacity unit to the - * pool. - */ - public static final int RETRY_THROTTLING_COST = 5; + public static final class Legacy { + private static final int THROTTLE_EXCEPTION_TOKEN_COST = 0; + private static final int DEFAULT_EXCEPTION_TOKEN_COST = 5; - /** - * When throttled retries are enabled, this is the total number of subsequent failed retries - * that may be attempted before retry capacity is fully drained. - */ - public static final int THROTTLED_RETRIES = 100; + public static final TokenBucketExceptionCostFunction COST_FUNCTION = + TokenBucketExceptionCostFunction.builder() + .throttlingExceptionCost(THROTTLE_EXCEPTION_TOKEN_COST) + .defaultExceptionCost(DEFAULT_EXCEPTION_TOKEN_COST) + .build(); + } + + public static final class Standard { + private static final int THROTTLE_EXCEPTION_TOKEN_COST = 5; + private static final int DEFAULT_EXCEPTION_TOKEN_COST = 5; + + public static final TokenBucketExceptionCostFunction COST_FUNCTION = + TokenBucketExceptionCostFunction.builder() + .throttlingExceptionCost(THROTTLE_EXCEPTION_TOKEN_COST) + .defaultExceptionCost(DEFAULT_EXCEPTION_TOKEN_COST) + .build(); + } + + public static final int TOKEN_BUCKET_SIZE = 500; public static final Duration BASE_DELAY = Duration.ofMillis(100); @@ -70,4 +85,37 @@ public final class SdkDefaultRetrySetting { } private SdkDefaultRetrySetting() {} + + public static Integer maxAttempts(RetryMode retryMode) { + Integer maxAttempts = SdkSystemSetting.AWS_MAX_ATTEMPTS.getIntegerValue().orElse(null); + + if (maxAttempts == null) { + switch (retryMode) { + case LEGACY: + maxAttempts = 4; + break; + case STANDARD: + maxAttempts = 3; + break; + default: + throw new IllegalArgumentException("Unknown retry mode: " + retryMode); + } + } + + Validate.isPositive(maxAttempts, "Maximum attempts must be positive, but was " + maxAttempts); + + return maxAttempts; + } + + public static TokenBucketExceptionCostFunction tokenCostFunction(RetryMode retryMode) { + switch (retryMode) { + case LEGACY: return Legacy.COST_FUNCTION; + case STANDARD: return Standard.COST_FUNCTION; + default: throw new IllegalStateException("Unsupported RetryMode: " + retryMode); + } + } + + public static Integer defaultMaxAttempts() { + return maxAttempts(RetryMode.defaultRetryMode()); + } } 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 new file mode 100644 index 000000000000..7c8b8b5adef1 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryMode.java @@ -0,0 +1,112 @@ +/* + * 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.SdkPublicApi; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.conditions.TokenBucketRetryCondition; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.utils.OptionalUtils; +import software.amazon.awssdk.utils.StringUtils; + +/** + * A retry mode is a collection of retry behaviors encoded under a single value. For example, the {@link #LEGACY} retry mode will + * retry up to three times, and the {@link #STANDARD} will retry up to two times. + * + *

+ * While the {@link #LEGACY} retry mode is specific to Java, the {@link #STANDARD} retry mode is standardized across all of the + * AWS SDKs. + * + *

+ * The retry mode can be configured: + *

    + *
  1. Directly on a client via {@link ClientOverrideConfiguration.Builder#retryPolicy(RetryMode)}.
  2. + *
  3. Directly on a client via a combination of {@link RetryPolicy#builder(RetryMode)} or + * {@link RetryPolicy#forRetryMode(RetryMode)}, and {@link ClientOverrideConfiguration.Builder#retryPolicy(RetryPolicy)}
  4. + *
  5. On a configuration profile via the "retry_mode" profile file property.
  6. + *
  7. Globally via the "aws.retryMode" system property.
  8. + *
  9. Globally via the "AWS_RETRY_MODE" environment variable.
  10. + *
+ */ +@SdkPublicApi +public enum RetryMode { + /** + * The LEGACY retry mode, specific to the Java SDK, and characterized by: + *
    + *
  1. Up to 3 retries, or more for services like DynamoDB (which has up to 8).
  2. + *
  3. Zero token are subtracted from the {@link TokenBucketRetryCondition} when throttling exceptions are encountered. + *
  4. + *
+ * + *

+ * This is the retry mode that is used when no other mode is configured. + */ + LEGACY, + + + /** + * The STANDARD retry mode, shared by all AWS SDK implementations, and characterized by: + *

    + *
  1. Up to 2 retries, regardless of service.
  2. + *
  3. Throttling exceptions are treated the same as other exceptions for the purposes of the + * {@link TokenBucketRetryCondition}.
  4. + *
+ */ + STANDARD; + + /** + * Retrieve the default retry mode by consulting the locations described in {@link RetryMode}, or LEGACY if no value is + * configured. + */ + public static RetryMode defaultRetryMode() { + return RetryMode.fromDefaultChain(ProfileFile.defaultProfileFile()); + } + + private static Optional fromSystemSettings() { + return SdkSystemSetting.AWS_RETRY_MODE.getStringValue() + .flatMap(RetryMode::fromString); + } + + private static Optional fromProfileFile(ProfileFile profileFile) { + return profileFile.profile(ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow()) + .flatMap(p -> p.property(ProfileProperty.RETRY_MODE)) + .flatMap(RetryMode::fromString); + } + + private static RetryMode fromDefaultChain(ProfileFile profileFile) { + return OptionalUtils.firstPresent(RetryMode.fromSystemSettings(), () -> fromProfileFile(profileFile)) + .orElse(RetryMode.LEGACY); + } + + private static Optional fromString(String string) { + if (string == null || string.isEmpty()) { + return Optional.empty(); + } + + switch (StringUtils.lowerCase(string)) { + case "legacy": + return Optional.of(LEGACY); + case "standard": + return Optional.of(STANDARD); + default: + throw new IllegalStateException("Unsupported retry policy mode configured: " + string); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicy.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicy.java index 8176f609f453..69ec0f783aa7 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicy.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicy.java @@ -23,19 +23,19 @@ import software.amazon.awssdk.core.retry.conditions.AndRetryCondition; import software.amazon.awssdk.core.retry.conditions.MaxNumberOfRetriesCondition; import software.amazon.awssdk.core.retry.conditions.RetryCondition; +import software.amazon.awssdk.core.retry.conditions.TokenBucketRetryCondition; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; /** - * Interface for specifying a retry policy to use when evaluating whether or not a request should be retried. An implementation - * of this interface can be provided to {@link ClientOverrideConfiguration#retryPolicy} or the {@link #builder()}} can be used - * to construct a retry policy from SDK provided policies or policies that directly implement {@link BackoffStrategy} and/or - * {@link RetryCondition}. + * Interface for specifying a retry policy to use when evaluating whether or not a request should be retried. The + * {@link #builder()}} can be used to construct a retry policy from SDK provided policies or policies that directly implement + * {@link BackoffStrategy} and/or {@link RetryCondition}. This is configured on a client via + * {@link ClientOverrideConfiguration.Builder#retryPolicy}. * * When using the {@link #builder()} the SDK will use default values for fields that are not provided. The default number of - * retries that will be used is {@link SdkDefaultRetrySetting#DEFAULT_MAX_RETRIES}. The default retry condition is - * {@link RetryCondition#defaultRetryCondition()} and the default backoff strategy is {@link BackoffStrategy#defaultStrategy()}. + * retries and condition is based on the current {@link RetryMode}. * * @see RetryCondition for a list of SDK provided retry condition strategies * @see BackoffStrategy for a list of SDK provided backoff strategies @@ -43,50 +43,143 @@ @Immutable @SdkPublicApi public final class RetryPolicy implements ToCopyableBuilder { - - private final RetryCondition retryConditionFromBuilder; - private final RetryCondition retryCondition; + private final boolean additionalRetryConditionsAllowed; + private final RetryMode retryMode; private final BackoffStrategy backoffStrategy; private final BackoffStrategy throttlingBackoffStrategy; private final Integer numRetries; + private final RetryCondition retryCondition; + private final RetryCondition retryCapacityCondition; + + private final RetryCondition aggregateRetryCondition; private RetryPolicy(BuilderImpl builder) { + this.additionalRetryConditionsAllowed = builder.additionalRetryConditionsAllowed; + this.retryMode = builder.retryMode; this.backoffStrategy = builder.backoffStrategy; this.throttlingBackoffStrategy = builder.throttlingBackoffStrategy; this.numRetries = builder.numRetries; - this.retryConditionFromBuilder = builder.retryCondition; - this.retryCondition = AndRetryCondition.create(MaxNumberOfRetriesCondition.create(numRetries), - retryConditionFromBuilder); + this.retryCondition = builder.retryCondition; + this.retryCapacityCondition = builder.retryCapacityCondition; + + this.aggregateRetryCondition = generateAggregateRetryCondition(); + } + + /** + * Create a {@link RetryPolicy} using the {@link RetryMode#defaultRetryMode()} defaults. + */ + public static RetryPolicy defaultRetryPolicy() { + return forRetryMode(RetryMode.defaultRetryMode()); + } + + /** + * Create a {@link RetryPolicy} using the provided {@link RetryMode} defaults. + */ + public static RetryPolicy forRetryMode(RetryMode retryMode) { + return RetryPolicy.builder(retryMode).build(); + } + + /** + * Create a {@link RetryPolicy} that will NEVER retry. + */ + public static RetryPolicy none() { + return RetryPolicy.builder() + .numRetries(0) + .backoffStrategy(BackoffStrategy.none()) + .throttlingBackoffStrategy(BackoffStrategy.none()) + .retryCondition(RetryCondition.none()) + .additionalRetryConditionsAllowed(false) + .build(); } + /** + * Create a {@link RetryPolicy.Builder} populated with the defaults from the {@link RetryMode#defaultRetryMode()}. + */ + public static Builder builder() { + return new BuilderImpl(RetryMode.defaultRetryMode()); + } + + /** + * Create a {@link RetryPolicy.Builder} populated with the defaults from the provided {@link RetryMode}. + */ + public static Builder builder(RetryMode retryMode) { + return new BuilderImpl(retryMode); + } + + /** + * Retrieve the {@link RetryMode} that was used to determine the defaults for this retry policy. + */ + public RetryMode retryMode() { + return retryMode; + } + + /** + * Returns true if service-specific conditions are allowed on this policy (e.g. more conditions may be added by the SDK if + * they are recommended). + */ + public boolean additionalRetryConditionsAllowed() { + return additionalRetryConditionsAllowed; + } + + /** + * Retrieve the retry condition that aggregates the {@link Builder#retryCondition(RetryCondition)}, + * {@link Builder#numRetries(Integer)} and {@link Builder#retryCapacityCondition(RetryCondition)} configured on the builder. + */ + public RetryCondition aggregateRetryCondition() { + return aggregateRetryCondition; + } + + /** + * Retrieve the {@link Builder#retryCondition(RetryCondition)} configured on the builder. + */ public RetryCondition retryCondition() { return retryCondition; } + /** + * Retrieve the {@link Builder#backoffStrategy(BackoffStrategy)} configured on the builder. + */ public BackoffStrategy backoffStrategy() { return backoffStrategy; } + /** + * Retrieve the {@link Builder#throttlingBackoffStrategy(BackoffStrategy)} configured on the builder. + */ public BackoffStrategy throttlingBackoffStrategy() { return throttlingBackoffStrategy; } + /** + * Retrieve the {@link Builder#numRetries(Integer)} configured on the builder. + */ public Integer numRetries() { return numRetries; } + private RetryCondition generateAggregateRetryCondition() { + RetryCondition aggregate = AndRetryCondition.create(MaxNumberOfRetriesCondition.create(numRetries), + retryCondition); + if (retryCapacityCondition != null) { + return AndRetryCondition.create(aggregate, retryCapacityCondition); + } + return aggregate; + } + public Builder toBuilder() { - return builder().numRetries(numRetries) - .retryCondition(retryConditionFromBuilder) - .backoffStrategy(backoffStrategy) - .throttlingBackoffStrategy(throttlingBackoffStrategy); + return builder(retryMode).additionalRetryConditionsAllowed(additionalRetryConditionsAllowed) + .numRetries(numRetries) + .retryCondition(retryCondition) + .backoffStrategy(backoffStrategy) + .throttlingBackoffStrategy(throttlingBackoffStrategy) + .retryCapacityCondition(retryCapacityCondition); } @Override public String toString() { return ToString.builder("RetryPolicy") - .add("numRetries", numRetries) - .add("retryCondition", retryCondition) + .add("additionalRetryConditionsAllowed", additionalRetryConditionsAllowed) + .add("aggregateRetryCondition", aggregateRetryCondition) .add("backoffStrategy", backoffStrategy) .add("throttlingBackoffStrategy", throttlingBackoffStrategy) .build(); @@ -103,7 +196,10 @@ public boolean equals(Object o) { RetryPolicy that = (RetryPolicy) o; - if (!retryCondition.equals(that.retryCondition)) { + if (additionalRetryConditionsAllowed != that.additionalRetryConditionsAllowed) { + return false; + } + if (!aggregateRetryCondition.equals(that.aggregateRetryCondition)) { return false; } if (!backoffStrategy.equals(that.backoffStrategy)) { @@ -112,57 +208,103 @@ public boolean equals(Object o) { if (!throttlingBackoffStrategy.equals(that.throttlingBackoffStrategy)) { return false; } - return numRetries.equals(that.numRetries); + return true; } @Override public int hashCode() { - int result = retryCondition.hashCode(); + int result = aggregateRetryCondition.hashCode(); + result = 31 * result + Boolean.hashCode(additionalRetryConditionsAllowed); result = 31 * result + backoffStrategy.hashCode(); result = 31 * result + throttlingBackoffStrategy.hashCode(); - result = 31 * result + numRetries.hashCode(); return result; } - public static Builder builder() { - return new BuilderImpl(); - } - - public static RetryPolicy defaultRetryPolicy() { - return RetryPolicy.builder() - .backoffStrategy(BackoffStrategy.defaultStrategy()) - .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy()) - .numRetries(SdkDefaultRetrySetting.DEFAULT_MAX_RETRIES) - .retryCondition(RetryCondition.defaultRetryCondition()) - .build(); - } - - public static RetryPolicy none() { - return RetryPolicy.builder() - .numRetries(0) - .backoffStrategy(BackoffStrategy.none()) - .throttlingBackoffStrategy(BackoffStrategy.none()) - .retryCondition(RetryCondition.none()) - .build(); - } - public interface Builder extends CopyableBuilder { - Builder numRetries(Integer numRetries); - - Integer numRetries(); - + /** + * Configure whether further conditions can be added to this policy after it is created. This may include service- + * specific retry conditions that may not otherwise be covered by the {@link RetryCondition#defaultRetryCondition()}. + * + *

+ * By default, this is true. + */ + Builder additionalRetryConditionsAllowed(boolean additionalRetryConditionsAllowed); + + /** + * @see #additionalRetryConditionsAllowed(boolean) + */ + boolean additionalRetryConditionsAllowed(); + + /** + * Configure the backoff strategy that should be used for waiting in between retry attempts. If the retry is because of + * throttling reasons, the {@link #throttlingBackoffStrategy(BackoffStrategy)} is used instead. + */ Builder backoffStrategy(BackoffStrategy backoffStrategy); + /** + * @see #backoffStrategy(BackoffStrategy) + */ BackoffStrategy backoffStrategy(); + /** + * Configure the backoff strategy that should be used for waiting in between retry attempts after a throttling error + * is encountered. If the retry is not because of throttling reasons, the {@link #backoffStrategy(BackoffStrategy)} is + * used instead. + */ Builder throttlingBackoffStrategy(BackoffStrategy backoffStrategy); + /** + * @see #throttlingBackoffStrategy(BackoffStrategy) + */ BackoffStrategy throttlingBackoffStrategy(); + /** + * Configure the condition under which the request should be retried. + * + *

+ * While this can be any interface that implements {@link RetryCondition}, it is encouraged to use + * {@link #numRetries(Integer)} when attempting to limit the number of times the SDK will retry an attempt or the + * {@link #retryCapacityCondition(RetryCondition)} when attempting to configure the throttling of retries. This guidance + * is because the SDK uses the {@link #aggregateRetryCondition()} when determining whether or not to retry a request, + * and the {@code aggregateRetryCondition} includes the {@code numRetries} and {@code retryCapacityCondition} in its + * determination. + */ Builder retryCondition(RetryCondition retryCondition); + /** + * @see #retryCondition(RetryCondition) + */ RetryCondition retryCondition(); + /** + * Configure the {@link RetryCondition} that should be used to throttle the number of retries attempted by the SDK client + * as a whole. + * + *

+ * While any {@link RetryCondition} (or null) can be used, by convention these conditions are usually stateful and work + * globally for the whole client to limit the overall capacity of the client to execute retries. + * + *

+ * By default the {@link TokenBucketRetryCondition} is used. This can be disabled by setting the value to {@code null} + * (not {@code RetryPolicy#none()}, which would completely disable retries). + */ + Builder retryCapacityCondition(RetryCondition retryCapacityCondition); + + /** + * @see #retryCapacityCondition(RetryCondition) + */ + RetryCondition retryCapacityCondition(); + + /** + * Configure the maximum number of times that a single request should be retried, assuming it fails for a retryable error. + */ + Builder numRetries(Integer numRetries); + + /** + * @see #numRetries(Integer) + */ + Integer numRetries(); + RetryPolicy build(); } @@ -170,13 +312,38 @@ public interface Builder extends CopyableBuilder { * Builder for a {@link RetryPolicy}. */ private static final class BuilderImpl implements Builder { + private final RetryMode retryMode; + + private boolean additionalRetryConditionsAllowed; + private Integer numRetries; + private BackoffStrategy backoffStrategy; + private BackoffStrategy throttlingBackoffStrategy; + private RetryCondition retryCondition; + private RetryCondition retryCapacityCondition; + + private BuilderImpl(RetryMode retryMode) { + this.retryMode = retryMode; + this.numRetries = SdkDefaultRetrySetting.maxAttempts(retryMode) - 1; + this.additionalRetryConditionsAllowed = true; + this.backoffStrategy = BackoffStrategy.defaultStrategy(); + this.throttlingBackoffStrategy = BackoffStrategy.defaultThrottlingStrategy(); + this.retryCondition = RetryCondition.defaultRetryCondition(); + this.retryCapacityCondition = TokenBucketRetryCondition.forRetryMode(retryMode); + } + + @Override + public Builder additionalRetryConditionsAllowed(boolean additionalRetryConditionsAllowed) { + this.additionalRetryConditionsAllowed = additionalRetryConditionsAllowed; + return this; + } - private Integer numRetries = SdkDefaultRetrySetting.DEFAULT_MAX_RETRIES; - private BackoffStrategy backoffStrategy = BackoffStrategy.defaultStrategy(); - private BackoffStrategy throttlingBackoffStrategy = BackoffStrategy.defaultThrottlingStrategy(); - private RetryCondition retryCondition = RetryCondition.defaultRetryCondition(); + public void setadditionalRetryConditionsAllowed(boolean additionalRetryConditionsAllowed) { + additionalRetryConditionsAllowed(additionalRetryConditionsAllowed); + } - private BuilderImpl(){ + @Override + public boolean additionalRetryConditionsAllowed() { + return additionalRetryConditionsAllowed; } @Override @@ -239,6 +406,21 @@ public RetryCondition retryCondition() { return retryCondition; } + @Override + public Builder retryCapacityCondition(RetryCondition retryCapacityCondition) { + this.retryCapacityCondition = retryCapacityCondition; + return this; + } + + public void setRetryCapacityCondition(RetryCondition retryCapacityCondition) { + retryCapacityCondition(retryCapacityCondition); + } + + @Override + public RetryCondition retryCapacityCondition() { + return this.retryCapacityCondition; + } + @Override public RetryPolicy build() { return new RetryPolicy(this); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicyContext.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicyContext.java index 3993eb0fc535..2d812fa839c1 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicyContext.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/RetryPolicyContext.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.core.retry; import software.amazon.awssdk.annotations.Immutable; -import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.exception.SdkException; @@ -49,7 +48,6 @@ private RetryPolicyContext(Builder builder) { this.httpStatusCode = builder.httpStatusCode; } - @SdkInternalApi public static Builder builder() { return new Builder(); } @@ -93,7 +91,7 @@ public int retriesAttempted() { * @return The total number of requests made thus far. */ public int totalRequests() { - return retriesAttempted() + 1; + return this.retriesAttempted + 1; } /** @@ -108,9 +106,8 @@ public Builder toBuilder() { return new Builder(this); } - @SdkInternalApi + @SdkPublicApi public static final class Builder implements CopyableBuilder { - private SdkRequest originalRequest; private SdkHttpFullRequest request; private SdkException exception; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/AndRetryCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/AndRetryCondition.java index 4d184e586981..5ff9a9cad9c0 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/AndRetryCondition.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/AndRetryCondition.java @@ -16,7 +16,7 @@ package software.amazon.awssdk.core.retry.conditions; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.retry.RetryPolicyContext; @@ -29,12 +29,16 @@ @SdkPublicApi public final class AndRetryCondition implements RetryCondition { - private Set conditions = new HashSet<>(); + private final Set conditions = new LinkedHashSet<>(); private AndRetryCondition(RetryCondition... conditions) { Collections.addAll(this.conditions, Validate.notEmpty(conditions, "%s cannot be empty.", "conditions")); } + public static AndRetryCondition create(RetryCondition... conditions) { + return new AndRetryCondition(conditions); + } + /** * @return True if all conditions are true, false otherwise. */ @@ -43,8 +47,14 @@ public boolean shouldRetry(RetryPolicyContext context) { return conditions.stream().allMatch(r -> r.shouldRetry(context)); } - public static AndRetryCondition create(RetryCondition... conditions) { - return new AndRetryCondition(conditions); + @Override + public void requestWillNotBeRetried(RetryPolicyContext context) { + conditions.forEach(c -> c.requestWillNotBeRetried(context)); + } + + @Override + public void requestSucceeded(RetryPolicyContext context) { + conditions.forEach(c -> c.requestSucceeded(context)); } @Override @@ -58,12 +68,12 @@ public boolean equals(Object o) { AndRetryCondition that = (AndRetryCondition) o; - return conditions != null ? conditions.equals(that.conditions) : that.conditions == null; + return conditions.equals(that.conditions); } @Override public int hashCode() { - return conditions != null ? conditions.hashCode() : 0; + return conditions.hashCode(); } @Override diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/MaxNumberOfRetriesCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/MaxNumberOfRetriesCondition.java index f4e9ce45127a..3ee23869e32b 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/MaxNumberOfRetriesCondition.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/MaxNumberOfRetriesCondition.java @@ -16,6 +16,8 @@ package software.amazon.awssdk.core.retry.conditions; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; +import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicyContext; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.Validate; @@ -32,15 +34,19 @@ private MaxNumberOfRetriesCondition(int maxNumberOfRetries) { this.maxNumberOfRetries = Validate.isNotNegative(maxNumberOfRetries, "maxNumberOfRetries"); } + public static MaxNumberOfRetriesCondition create(int maxNumberOfRetries) { + return new MaxNumberOfRetriesCondition(maxNumberOfRetries); + } + + public static MaxNumberOfRetriesCondition forRetryMode(RetryMode retryMode) { + return create(SdkDefaultRetrySetting.maxAttempts(retryMode)); + } + @Override public boolean shouldRetry(RetryPolicyContext context) { return context.retriesAttempted() < maxNumberOfRetries; } - public static MaxNumberOfRetriesCondition create(int maxNumberOfRetries) { - return new MaxNumberOfRetriesCondition(maxNumberOfRetries); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/OrRetryCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/OrRetryCondition.java index 6f71c521b897..675ff3e4ab2b 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/OrRetryCondition.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/OrRetryCondition.java @@ -16,7 +16,7 @@ package software.amazon.awssdk.core.retry.conditions; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.retry.RetryPolicyContext; @@ -28,12 +28,16 @@ @SdkPublicApi public final class OrRetryCondition implements RetryCondition { - private Set conditions = new HashSet<>(); + private final Set conditions = new LinkedHashSet<>(); private OrRetryCondition(RetryCondition... conditions) { Collections.addAll(this.conditions, conditions); } + public static OrRetryCondition create(RetryCondition... conditions) { + return new OrRetryCondition(conditions); + } + /** * @return True if any condition returns true. False otherwise. */ @@ -42,8 +46,14 @@ public boolean shouldRetry(RetryPolicyContext context) { return conditions.stream().anyMatch(r -> r.shouldRetry(context)); } - public static OrRetryCondition create(RetryCondition... conditions) { - return new OrRetryCondition(conditions); + @Override + public void requestWillNotBeRetried(RetryPolicyContext context) { + conditions.forEach(c -> c.requestWillNotBeRetried(context)); + } + + @Override + public void requestSucceeded(RetryPolicyContext context) { + conditions.forEach(c -> c.requestSucceeded(context)); } @Override diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryCondition.java index 4644e37dedf6..3ec8dce22edc 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryCondition.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryCondition.java @@ -18,12 +18,10 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; import software.amazon.awssdk.core.retry.RetryPolicyContext; -import software.amazon.awssdk.core.retry.RetryUtils; @SdkPublicApi @FunctionalInterface public interface RetryCondition { - /** * Determine whether a request should or should not be retried. * @@ -32,14 +30,31 @@ public interface RetryCondition { */ boolean shouldRetry(RetryPolicyContext context); + /** + * Called by the SDK to notify this condition that the provided request will not be retried, because some retry condition + * determined that it shouldn't be retried. + */ + default void requestWillNotBeRetried(RetryPolicyContext context) { + } + + /** + * Called by the SDK to notify this condition that the provided request succeeded. This method is invoked even if the + * execution never failed before ({@link RetryPolicyContext#retriesAttempted()} is zero). + */ + default void requestSucceeded(RetryPolicyContext context) { + } + static RetryCondition defaultRetryCondition() { return OrRetryCondition.create( RetryOnStatusCodeCondition.create(SdkDefaultRetrySetting.RETRYABLE_STATUS_CODES), RetryOnExceptionsCondition.create(SdkDefaultRetrySetting.RETRYABLE_EXCEPTIONS), - c -> RetryUtils.isClockSkewException(c.exception()), - c -> RetryUtils.isThrottlingException(c.exception())); + RetryOnClockSkewCondition.create(), + RetryOnThrottlingCondition.create()); } + /** + * A retry condition that will NEVER allow retries. + */ static RetryCondition none() { return MaxNumberOfRetriesCondition.create(0); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryOnClockSkewCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryOnClockSkewCondition.java new file mode 100644 index 000000000000..e9b48e6f495c --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryOnClockSkewCondition.java @@ -0,0 +1,58 @@ +/* + * 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.conditions; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.retry.RetryPolicyContext; +import software.amazon.awssdk.core.retry.RetryUtils; +import software.amazon.awssdk.utils.ToString; + +/** + * A {@link RetryCondition} that will return true if the provided exception seems to be due to a clock skew between the + * client and service. + */ +@SdkPublicApi +public final class RetryOnClockSkewCondition implements RetryCondition { + private RetryOnClockSkewCondition() {} + + public static RetryOnClockSkewCondition create() { + return new RetryOnClockSkewCondition(); + } + + @Override + public boolean shouldRetry(RetryPolicyContext context) { + return RetryUtils.isClockSkewException(context.exception()); + } + + @Override + public String toString() { + return ToString.create("RetryOnClockSkewCondition"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return 0; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryOnThrottlingCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryOnThrottlingCondition.java new file mode 100644 index 000000000000..d05f3925ae5b --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/RetryOnThrottlingCondition.java @@ -0,0 +1,58 @@ +/* + * 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.conditions; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.retry.RetryPolicyContext; +import software.amazon.awssdk.core.retry.RetryUtils; +import software.amazon.awssdk.utils.ToString; + +/** + * A {@link RetryCondition} that will return true if the provided exception seems to be due to a throttling error from the + * service to the client. + */ +@SdkPublicApi +public final class RetryOnThrottlingCondition implements RetryCondition { + private RetryOnThrottlingCondition() {} + + public static RetryOnThrottlingCondition create() { + return new RetryOnThrottlingCondition(); + } + + @Override + public boolean shouldRetry(RetryPolicyContext context) { + return RetryUtils.isThrottlingException(context.exception()); + } + + @Override + public String toString() { + return ToString.create("RetryOnThrottlingCondition"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return 0; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/TokenBucketExceptionCostFunction.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/TokenBucketExceptionCostFunction.java new file mode 100644 index 000000000000..5f09f38f42d0 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/TokenBucketExceptionCostFunction.java @@ -0,0 +1,63 @@ +/* + * 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.conditions; + +import java.util.function.Function; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.internal.retry.DefaultTokenBucketExceptionCostFunction; + +/** + * A function used by {@link TokenBucketRetryCondition} to determine how many tokens should be removed from the bucket when an + * exception is encountered. This can be implemented directly, or using the helper methods provided by the {@link #builder()}. + */ +@SdkPublicApi +@FunctionalInterface +@ThreadSafe +public interface TokenBucketExceptionCostFunction extends Function { + /** + * Create an exception cost function using exception type matchers built into the SDK. This interface may be implemented + * directly, or created via a builder. + */ + static Builder builder() { + return new DefaultTokenBucketExceptionCostFunction.Builder(); + } + + /** + * A helper that can be used to assign exception costs to specific exception types, created via {@link #builder()}. + */ + @NotThreadSafe + interface Builder { + /** + * Specify the number of tokens that should be removed from the token bucket when throttling exceptions (e.g. HTTP status + * code 429) are encountered. + */ + Builder throttlingExceptionCost(int cost); + + /** + * Specify the number of tokens that should be removed from the token bucket when no other exception type in this + * function is matched. This field is required. + */ + Builder defaultExceptionCost(int cost); + + /** + * Create a {@link TokenBucketExceptionCostFunction} using the values configured on this builder. + */ + TokenBucketExceptionCostFunction build(); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/TokenBucketRetryCondition.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/TokenBucketRetryCondition.java new file mode 100644 index 000000000000..afb4ad93160d --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/conditions/TokenBucketRetryCondition.java @@ -0,0 +1,275 @@ +/* + * 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.conditions; + +import static software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting.TOKEN_BUCKET_SIZE; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.interceptor.ExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.internal.capacity.TokenBucket; +import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.RetryPolicyContext; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; + +/** + * A {@link RetryCondition} that limits the number of retries made by the SDK using a token bucket algorithm. "Tokens" are + * acquired from the bucket whenever {@link #shouldRetry} returns true, and are released to the bucket whenever + * {@link #requestSucceeded} or {@link #requestWillNotBeRetried} are invoked. + * + *

+ * If "tokens" cannot be acquired from the bucket, it means too many requests have failed and the request will not be allowed + * to retry until we start to see initial non-retried requests succeed via {@link #requestSucceeded(RetryPolicyContext)}. + * + *

+ * This prevents the client from holding the calling thread to retry when it's likely that it will fail anyway. + * + *

+ * This is currently included in the default {@link RetryPolicy#aggregateRetryCondition()}, but can be disabled by setting the + * {@link RetryPolicy.Builder#retryCapacityCondition} to null. + */ +@SdkPublicApi +public class TokenBucketRetryCondition implements RetryCondition { + private static final ExecutionAttribute LAST_ACQUIRED_CAPACITY = + new ExecutionAttribute<>("TokenBucketRetryCondition.LAST_ACQUIRED_CAPACITY"); + + private static final ExecutionAttribute RETRY_COUNT_OF_LAST_CAPACITY_ACQUISITION = + new ExecutionAttribute<>("TokenBucketRetryCondition.RETRY_COUNT_OF_LAST_CAPACITY_ACQUISITION"); + + private final TokenBucket capacity; + private final TokenBucketExceptionCostFunction exceptionCostFunction; + + private TokenBucketRetryCondition(Builder builder) { + this.capacity = new TokenBucket(Validate.notNull(builder.tokenBucketSize, "tokenBucketSize")); + this.exceptionCostFunction = Validate.notNull(builder.exceptionCostFunction, "exceptionCostFunction"); + } + + /** + * Create a condition using the {@link RetryMode#defaultRetryMode()}. This is equivalent to + * {@code forRetryMode(RetryMode.defaultRetryMode())}. + * + *

+ * For more detailed control, see {@link #builder()}. + */ + public static TokenBucketRetryCondition create() { + return forRetryMode(RetryMode.defaultRetryMode()); + } + + /** + * Create a condition using the configured {@link RetryMode}. The {@link RetryMode#LEGACY} does not subtract tokens from + * the token bucket when throttling exceptions are encountered. The {@link RetryMode#STANDARD} treats throttling and non- + * throttling exceptions as the same cost. + * + *

+ * For more detailed control, see {@link #builder()}. + */ + public static TokenBucketRetryCondition forRetryMode(RetryMode retryMode) { + return TokenBucketRetryCondition.builder() + .tokenBucketSize(TOKEN_BUCKET_SIZE) + .exceptionCostFunction(SdkDefaultRetrySetting.tokenCostFunction(retryMode)) + .build(); + } + + /** + * Create a builder that allows fine-grained control over the token policy of this condition. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * If {@link #shouldRetry(RetryPolicyContext)} returned true for the provided execution, this method returns the + * {@link Capacity} consumed by the request. + */ + public static Optional getCapacityForExecution(ExecutionAttributes attributes) { + return Optional.ofNullable(attributes.getAttribute(LAST_ACQUIRED_CAPACITY)); + } + + /** + * Retrieve the number of tokens currently available in the token bucket. This is a volatile snapshot of the current value. + * See {@link #getCapacityForExecution(ExecutionAttributes)} to see how much capacity was left in the bucket after a specific + * execution was considered. + */ + public int tokensAvailable() { + return capacity.currentCapacity(); + } + + @Override + public boolean shouldRetry(RetryPolicyContext context) { + int costOfFailure = exceptionCostFunction.apply(context.exception()); + Validate.isTrue(costOfFailure >= 0, "Cost of failure must not be negative, but was " + costOfFailure); + + Optional capacity = this.capacity.tryAcquire(costOfFailure); + + capacity.ifPresent(c -> { + context.executionAttributes().putAttribute(LAST_ACQUIRED_CAPACITY, c); + context.executionAttributes().putAttribute(RETRY_COUNT_OF_LAST_CAPACITY_ACQUISITION, + context.retriesAttempted()); + }); + + return capacity.isPresent(); + } + + @Override + public void requestWillNotBeRetried(RetryPolicyContext context) { + Integer lastAcquisitionRetryCount = context.executionAttributes().getAttribute(RETRY_COUNT_OF_LAST_CAPACITY_ACQUISITION); + + if (lastAcquisitionRetryCount != null && context.retriesAttempted() == lastAcquisitionRetryCount) { + // We said yes to "should-retry", but something else caused it not to retry + Capacity lastAcquiredCapacity = context.executionAttributes().getAttribute(LAST_ACQUIRED_CAPACITY); + Validate.validState(lastAcquiredCapacity != null, "Last acquired capacity should not be null."); + capacity.release(lastAcquiredCapacity.capacityAcquired()); + } + } + + @Override + public void requestSucceeded(RetryPolicyContext context) { + Capacity lastAcquiredCapacity = context.executionAttributes().getAttribute(LAST_ACQUIRED_CAPACITY); + + if (lastAcquiredCapacity == null || lastAcquiredCapacity.capacityAcquired() == 0) { + capacity.release(1); + } else { + capacity.release(lastAcquiredCapacity.capacityAcquired()); + } + } + + @Override + public String toString() { + return ToString.builder("TokenBucketRetryCondition") + .add("capacity", capacity.currentCapacity() + "/" + capacity.maxCapacity()) + .add("exceptionCostFunction", exceptionCostFunction) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TokenBucketRetryCondition that = (TokenBucketRetryCondition) o; + + if (!capacity.equals(that.capacity)) { + return false; + } + return exceptionCostFunction.equals(that.exceptionCostFunction); + } + + @Override + public int hashCode() { + int result = capacity.hashCode(); + result = 31 * result + exceptionCostFunction.hashCode(); + return result; + } + + /** + * Configure and create a {@link TokenBucketRetryCondition}. + */ + public static final class Builder { + private Integer tokenBucketSize; + private TokenBucketExceptionCostFunction exceptionCostFunction; + + /** + * Create using {@link TokenBucketRetryCondition#builder()}. + */ + private Builder() {} + + /** + * Specify the maximum number of tokens in the token bucket. This is also used as the initial value for the number of + * tokens in the bucket. + */ + public Builder tokenBucketSize(int tokenBucketSize) { + this.tokenBucketSize = tokenBucketSize; + return this; + } + + /** + * Configure a {@link TokenBucketExceptionCostFunction} that is used to calculate the number of tokens that should be + * taken out of the bucket for each specific exception. These tokens will be returned in case of successful retries. + */ + public Builder exceptionCostFunction(TokenBucketExceptionCostFunction exceptionCostFunction) { + this.exceptionCostFunction = exceptionCostFunction; + return this; + } + + /** + * Build a {@link TokenBucketRetryCondition} using the provided configuration. + */ + public TokenBucketRetryCondition build() { + return new TokenBucketRetryCondition(this); + } + } + + /** + * The number of tokens in the token bucket after a specific token acquisition succeeds. This can be retrieved via + * {@link #getCapacityForExecution(ExecutionAttributes)}. + */ + public static final class Capacity { + private final int capacityAcquired; + private final int capacityRemaining; + + private Capacity(Builder builder) { + this.capacityAcquired = Validate.notNull(builder.capacityAcquired, "capacityAcquired"); + this.capacityRemaining = Validate.notNull(builder.capacityRemaining, "capacityRemaining"); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * The number of tokens acquired by the last token acquisition. + */ + public int capacityAcquired() { + return capacityAcquired; + } + + /** + * The number of tokens in the token bucket. + */ + public int capacityRemaining() { + return capacityRemaining; + } + + public static class Builder { + private Integer capacityAcquired; + private Integer capacityRemaining; + + private Builder() {} + + public Builder capacityAcquired(Integer capacityAcquired) { + this.capacityAcquired = capacityAcquired; + return this; + } + + public Builder capacityRemaining(Integer capacityRemaining) { + this.capacityRemaining = capacityRemaining; + return this; + } + + public Capacity build() { + return new Capacity(this); + } + } + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/RetryCountInUserAgentTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/RetryCountInUserAgentTest.java index f7226499638a..847a4d5e1a72 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/RetryCountInUserAgentTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/RetryCountInUserAgentTest.java @@ -17,17 +17,18 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.junit.Assert.fail; -import static software.amazon.awssdk.core.internal.retry.RetryHandler.HEADER_SDK_RETRY_INFO; +import static software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting.SDK_RETRY_INFO_HEADER; import static software.amazon.awssdk.core.internal.util.ResponseHandlerTestUtils.combinedSyncResponseHandler; import org.junit.Test; - import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.exception.SdkServiceException; @@ -49,28 +50,35 @@ public class RetryCountInUserAgentTest extends WireMockTestBase { public void retriedRequest_AppendsCorrectRetryCountInUserAgent() throws Exception { stubFor(get(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse().withStatus(500))); - executeRequest(); + executeRequest(false); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("0/0/"))); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("1/0/"))); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("2/10/"))); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("3/20/"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("0/0/"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("1/0/"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("2/10/"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("3/20/"))); } @Test public void retriedRequest_AppendsCorrectRetryCountInUserAgent_throttlingEnabled() throws Exception { stubFor(get(urlEqualTo(RESOURCE_PATH)).willReturn(aResponse().withStatus(500))); - executeRequest(); + executeRequest(true); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("0/0/500"))); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("1/0/495"))); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("2/10/490"))); - verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(HEADER_SDK_RETRY_INFO, containing("3/20/485"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("0/0/"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("1/0/495"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("2/10/490"))); + verify(1, getRequestedFor(urlEqualTo(RESOURCE_PATH)).withHeader(SDK_RETRY_INFO_HEADER, equalTo("3/20/485"))); } - private void executeRequest() throws Exception { - RetryPolicy policy = RetryPolicy.builder().backoffStrategy(new SimpleArrayBackoffStrategy(BACKOFF_VALUES)).build(); + private void executeRequest(boolean throttlingEnabled) throws Exception { + RetryPolicy policy = RetryPolicy.builder() + .backoffStrategy(new SimpleArrayBackoffStrategy(BACKOFF_VALUES)) + .applyMutation(b -> { + if (!throttlingEnabled) { + b.retryCapacityCondition(null); + } + }) + .build(); SdkClientConfiguration config = HttpTestUtils.testClientConfiguration().toBuilder() .option(SdkClientOption.SYNC_HTTP_CLIENT, HttpTestUtils.testSdkHttpClient()) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallAttemptTimeoutTrackingStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallAttemptTimeoutTrackingStageTest.java index 329755b15799..7c6974a0d65b 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallAttemptTimeoutTrackingStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallAttemptTimeoutTrackingStageTest.java @@ -26,13 +26,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; - import software.amazon.awssdk.core.Response; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkRequestOverrideConfiguration; @@ -44,7 +42,6 @@ import software.amazon.awssdk.core.internal.http.timers.ApiCallTimeoutTracker; import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; import software.amazon.awssdk.core.internal.http.timers.NoOpTimeoutTracker; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.SdkHttpFullRequest; @RunWith(MockitoJUnitRunner.class) @@ -68,7 +65,6 @@ public void setUp() throws Exception { SdkClientConfiguration.builder() .option(SCHEDULED_EXECUTOR_SERVICE, timeoutExecutor) .build()) - .capacityManager(mock(CapacityManager.class)) .build(), wrapped); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallTimeoutTrackingStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallTimeoutTrackingStageTest.java index dd67c515e008..7a1b9d58f652 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallTimeoutTrackingStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApiCallTimeoutTrackingStageTest.java @@ -25,13 +25,11 @@ import java.time.Duration; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; - import software.amazon.awssdk.core.Response; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkRequestOverrideConfiguration; @@ -44,7 +42,6 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.utils.ThreadFactoryBuilder; @@ -66,7 +63,6 @@ public void setUp() throws Exception { (SdkClientOption .SCHEDULED_EXECUTOR_SERVICE, timeoutExecutor) .build()) - .capacityManager(mock(CapacityManager.class)) .build(), wrapped); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncApiCallTimeoutTrackingStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncApiCallTimeoutTrackingStageTest.java index 0893027bafda..dfecd8482ead 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncApiCallTimeoutTrackingStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncApiCallTimeoutTrackingStageTest.java @@ -34,9 +34,9 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.http.NoopTestRequest; -import software.amazon.awssdk.core.client.config.SdkClientConfiguration; 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; @@ -82,7 +82,6 @@ public void methodSetup() throws Exception { dependencies = HttpClientDependencies.builder() .clientConfiguration(configuration) - .capacityManager(capacityManager) .build(); httpRequest = SdkHttpFullRequest.builder() diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java index a4b4c56c05f6..3e78313a9920 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStageTest.java @@ -31,13 +31,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; - import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.http.ExecutionContext; @@ -46,7 +44,6 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; import software.amazon.awssdk.core.internal.util.AsyncResponseHandlerTestUtils; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import utils.ValidSdkObjects; @@ -105,7 +102,6 @@ private HttpClientDependencies clientDependencies(Duration timeout) { return HttpClientDependencies.builder() .clientConfiguration(configuration) - .capacityManager(new CapacityManager(2)) .build(); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimeoutExceptionHandlingStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimeoutExceptionHandlingStageTest.java index 774c8dd74bb6..753f1babdb1a 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimeoutExceptionHandlingStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/TimeoutExceptionHandlingStageTest.java @@ -23,13 +23,11 @@ import java.io.IOException; import java.net.SocketException; import java.util.concurrent.ScheduledFuture; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; - import software.amazon.awssdk.core.Response; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkRequestOverrideConfiguration; @@ -43,7 +41,6 @@ import software.amazon.awssdk.core.internal.http.timers.ApiCallTimeoutTracker; import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; import software.amazon.awssdk.core.internal.http.timers.TimeoutTask; -import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.http.SdkHttpFullRequest; import utils.ValidSdkObjects; @@ -68,7 +65,6 @@ public class TimeoutExceptionHandlingStageTest { @Before public void setup() { stage = new TimeoutExceptionHandlingStage<>(HttpClientDependencies.builder() - .capacityManager(new CapacityManager(1)) .clientConfiguration(SdkClientConfiguration.builder().build()) .build(), requestPipeline); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/AndRetryConditionTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/AndRetryConditionTest.java index 3915f34f8036..bacd65595137 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/AndRetryConditionTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/AndRetryConditionTest.java @@ -18,12 +18,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.core.retry.conditions.AndRetryCondition; import software.amazon.awssdk.core.retry.conditions.RetryCondition; @@ -95,4 +97,23 @@ public void singleConditionThatReturnsFalse_ReturnsFalse() { assertFalse(AndRetryCondition.create(conditionOne).shouldRetry(RetryPolicyContexts.EMPTY)); } + @Test + public void conditionsAreEvaluatedInOrder() { + int numConditions = 1000; + int firstFalseCondition = 500; + + RetryCondition[] conditions = new RetryCondition[numConditions]; + for (int i = 0; i < numConditions; ++i) { + RetryCondition mock = Mockito.mock(RetryCondition.class); + when(mock.shouldRetry(RetryPolicyContexts.EMPTY)).thenReturn(i != firstFalseCondition); + conditions[i] = mock; + } + + assertFalse(AndRetryCondition.create(conditions).shouldRetry(RetryPolicyContexts.EMPTY)); + + for (int i = 0; i < numConditions; ++i) { + int timesExpected = i <= firstFalseCondition ? 1 : 0; + Mockito.verify(conditions[i], times(timesExpected)).shouldRetry(RetryPolicyContexts.EMPTY); + } + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/OrRetryConditionTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/OrRetryConditionTest.java index 4ab0b8ba51ad..d277b32969a4 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/OrRetryConditionTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/OrRetryConditionTest.java @@ -18,12 +18,16 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.core.retry.conditions.OrRetryCondition; import software.amazon.awssdk.core.retry.conditions.RetryCondition; @@ -82,4 +86,24 @@ public void singleConditionThatReturnsFalse_ReturnsFalse() { assertFalse(OrRetryCondition.create(conditionOne).shouldRetry(RetryPolicyContexts.EMPTY)); } + @Test + public void conditionsAreEvaluatedInOrder() { + int numConditions = 1000; + int firstTrueCondition = 500; + + RetryCondition[] conditions = new RetryCondition[numConditions]; + for (int i = 0; i < numConditions; ++i) { + RetryCondition mock = Mockito.mock(RetryCondition.class); + when(mock.shouldRetry(RetryPolicyContexts.EMPTY)).thenReturn(i == firstTrueCondition); + conditions[i] = mock; + } + + assertTrue(OrRetryCondition.create(conditions).shouldRetry(RetryPolicyContexts.EMPTY)); + + for (int i = 0; i < numConditions; ++i) { + int timesExpected = i <= firstTrueCondition ? 1 : 0; + Mockito.verify(conditions[i], times(timesExpected)).shouldRetry(RetryPolicyContexts.EMPTY); + } + } + } 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 new file mode 100644 index 000000000000..4020e955bb21 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryModeTest.java @@ -0,0 +1,120 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.Validate; + +@RunWith(Parameterized.class) +public class RetryModeTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @Parameterized.Parameter + public TestData testData; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { + // Test defaults + new TestData(null, null, null, RetryMode.LEGACY), + new TestData(null, null, "PropertyNotSet", RetryMode.LEGACY), + + // Test precedence + new TestData("standard", "legacy", "PropertySetToLegacy", RetryMode.STANDARD), + new TestData("standard", null, null, RetryMode.STANDARD), + new TestData(null, "standard", "PropertySetToLegacy", RetryMode.STANDARD), + new TestData(null, "standard", null, RetryMode.STANDARD), + new TestData(null, null, "PropertySetToStandard", RetryMode.STANDARD), + + // Test invalid values + new TestData("wrongValue", null, null, null), + new TestData(null, "wrongValue", null, null), + new TestData(null, null, "PropertySetToUnsupportedValue", null), + + // Test capitalization standardization + new TestData("sTaNdArD", null, null, RetryMode.STANDARD), + new TestData(null, "sTaNdArD", null, RetryMode.STANDARD), + new TestData(null, null, "PropertyMixedCase", RetryMode.STANDARD), + }); + } + + @Before + @After + public void methodSetup() { + ENVIRONMENT_VARIABLE_HELPER.reset(); + System.clearProperty(SdkSystemSetting.AWS_RETRY_MODE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_PROFILE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property()); + } + + @Test + public void differentCombinationOfConfigs_shouldResolveCorrectly() { + if (testData.envVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), testData.envVarValue); + } + + if (testData.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_RETRY_MODE.property(), testData.systemProperty); + } + + if (testData.configFile != null) { + String diskLocationForFile = diskLocationForConfig(testData.configFile); + Validate.isTrue(Files.isReadable(Paths.get(diskLocationForFile)), diskLocationForFile + " is not readable."); + System.setProperty(ProfileFileSystemSetting.AWS_PROFILE.property(), "default"); + System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), diskLocationForFile); + } + + if (testData.expected == null) { + assertThatThrownBy(RetryMode::defaultRetryMode).isInstanceOf(RuntimeException.class); + } else { + assertThat(RetryMode.defaultRetryMode()).isEqualTo(testData.expected); + } + } + + private String diskLocationForConfig(String configFileName) { + return getClass().getResource(configFileName).getFile(); + } + + + private static class TestData { + private final String envVarValue; + private final String systemProperty; + private final String configFile; + private final RetryMode expected; + + TestData(String systemProperty, String envVarValue, String configFile, RetryMode expected) { + this.envVarValue = envVarValue; + this.systemProperty = systemProperty; + this.configFile = configFile; + this.expected = expected; + } + } +} \ No newline at end of file diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyMaxRetriesTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyMaxRetriesTest.java new file mode 100644 index 000000000000..84e3d5ac51d7 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyMaxRetriesTest.java @@ -0,0 +1,140 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.Validate; + +@RunWith(Parameterized.class) +public class RetryPolicyMaxRetriesTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @Parameterized.Parameter + public TestData testData; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { + // Test defaults + new TestData(null, null, null, null, null, 3), + new TestData(null, null, null, null, "PropertyNotSet", 3), + + // Test precedence + new TestData("9", "2", "standard", "standard", "PropertySetToStandard", 8), + new TestData(null, "9", "standard", "standard", "PropertySetToStandard", 8), + new TestData(null, null, "standard", "standard", "PropertySetToStandard", 2), + new TestData(null, null, null, "standard", "PropertySetToStandard", 2), + new TestData(null, null, null, null, "PropertySetToStandard", 2), + + // Test invalid values + new TestData("wrongValue", null, null, null, null, null), + new TestData(null, "wrongValue", null, null, null, null), + new TestData(null, null, "wrongValue", null, null, null), + new TestData(null, null, null, "wrongValue", null, null), + new TestData(null, null, null, null, "PropertySetToUnsupportedValue", null), + }); + } + + @BeforeClass + public static void classSetup() { + // If this caches any values, make sure it's cached with the default (non-modified) configuration. + RetryPolicy.defaultRetryPolicy(); + } + + @Before + @After + public void methodSetup() { + ENVIRONMENT_VARIABLE_HELPER.reset(); + System.clearProperty(SdkSystemSetting.AWS_MAX_ATTEMPTS.property()); + System.clearProperty(SdkSystemSetting.AWS_RETRY_MODE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_PROFILE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property()); + } + + @Test + public void differentCombinationOfConfigs_shouldResolveCorrectly() { + if (testData.attemptCountEnvVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_MAX_ATTEMPTS.environmentVariable(), testData.attemptCountEnvVarValue); + } + + if (testData.attemptCountSystemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_MAX_ATTEMPTS.property(), testData.attemptCountSystemProperty); + } + + if (testData.envVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), testData.envVarValue); + } + + if (testData.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_RETRY_MODE.property(), testData.systemProperty); + } + + if (testData.configFile != null) { + String diskLocationForFile = diskLocationForConfig(testData.configFile); + Validate.isTrue(Files.isReadable(Paths.get(diskLocationForFile)), diskLocationForFile + " is not readable."); + System.setProperty(ProfileFileSystemSetting.AWS_PROFILE.property(), "default"); + System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), diskLocationForFile); + } + + if (testData.expected == null) { + assertThatThrownBy(() -> RetryPolicy.forRetryMode(RetryMode.defaultRetryMode())).isInstanceOf(RuntimeException.class); + } else { + assertThat(RetryPolicy.forRetryMode(RetryMode.defaultRetryMode()).numRetries()).isEqualTo(testData.expected); + } + } + + private String diskLocationForConfig(String configFileName) { + return getClass().getResource(configFileName).getFile(); + } + + private static class TestData { + private final String attemptCountSystemProperty; + private final String attemptCountEnvVarValue; + private final String envVarValue; + private final String systemProperty; + private final String configFile; + private final Integer expected; + + TestData(String attemptCountSystemProperty, + String attemptCountEnvVarValue, + String retryModeSystemProperty, + String retryModeEnvVarValue, + String configFile, + Integer expected) { + this.attemptCountSystemProperty = attemptCountSystemProperty; + this.attemptCountEnvVarValue = attemptCountEnvVarValue; + this.envVarValue = retryModeEnvVarValue; + this.systemProperty = retryModeSystemProperty; + this.configFile = configFile; + this.expected = expected; + } + } +} \ No newline at end of file diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyTest.java index b2008f8b1c46..9dd6925dd899 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/RetryPolicyTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -80,4 +81,26 @@ public void nonRetryPolicy_shouldUseNullCondition() { assertThat(noneRetry.backoffStrategy()).isEqualTo(BackoffStrategy.none()); assertThat(noneRetry.throttlingBackoffStrategy()).isEqualTo(BackoffStrategy.none()); } + + @Test + public void maxRetriesFromRetryModeIsCorrect() { + assertThat(RetryPolicy.forRetryMode(RetryMode.LEGACY).numRetries()).isEqualTo(3); + assertThat(RetryPolicy.forRetryMode(RetryMode.STANDARD).numRetries()).isEqualTo(2); + } + + @Test + public void maxRetriesFromDefaultRetryModeIsCorrect() { + switch (RetryMode.defaultRetryMode()) { + case LEGACY: + assertThat(RetryPolicy.defaultRetryPolicy().numRetries()).isEqualTo(3); + assertThat(RetryPolicy.builder().build().numRetries()).isEqualTo(3); + break; + case STANDARD: + assertThat(RetryPolicy.defaultRetryPolicy().numRetries()).isEqualTo(2); + assertThat(RetryPolicy.builder().build().numRetries()).isEqualTo(2); + break; + default: + Assert.fail(); + } + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/conditions/TokenBucketRetryConditionTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/conditions/TokenBucketRetryConditionTest.java new file mode 100644 index 000000000000..1c9b6965d861 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/retry/conditions/TokenBucketRetryConditionTest.java @@ -0,0 +1,185 @@ +/* + * 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.conditions; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Assert; +import org.junit.Test; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.retry.RetryPolicyContext; + +public class TokenBucketRetryConditionTest { + private static final SdkException EXCEPTION = SdkClientException.create(""); + private static final SdkException EXCEPTION_2 = SdkClientException.create(""); + + @Test + public void maximumTokensCannotBeExceeded() { + TokenBucketRetryCondition condition = create(3, e -> 1); + for (int i = 1; i < 10; ++i) { + condition.requestSucceeded(context(null)); + assertThat(condition.tokensAvailable()).isEqualTo(3); + } + } + + @Test + public void releasingMoreCapacityThanAvailableSetsCapacityToMax() { + ExecutionAttributes attributes = new ExecutionAttributes(); + + TokenBucketRetryCondition condition = create(11, e -> e == EXCEPTION ? 1 : 3); + assertThat(condition.shouldRetry(context(EXCEPTION, attributes))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(10); + assertThat(condition.shouldRetry(context(EXCEPTION_2, attributes))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(7); + condition.requestSucceeded(context(EXCEPTION_2, attributes)); + assertThat(condition.tokensAvailable()).isEqualTo(10); + condition.requestSucceeded(context(EXCEPTION_2, attributes)); + assertThat(condition.tokensAvailable()).isEqualTo(11); + } + + @Test + public void nonFirstAttemptsAreNotFree() { + TokenBucketRetryCondition condition = create(2, e -> 1); + + assertThat(condition.shouldRetry(context(EXCEPTION))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(1); + + assertThat(condition.shouldRetry(context(EXCEPTION))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(0); + + assertThat(condition.shouldRetry(context(EXCEPTION))).isFalse(); + assertThat(condition.tokensAvailable()).isEqualTo(0); + } + + @Test + public void exceptionCostIsHonored() { + // EXCEPTION costs 1, anything else costs 10 + TokenBucketRetryCondition condition = create(20, e -> e == EXCEPTION ? 1 : 10); + + assertThat(condition.shouldRetry(context(EXCEPTION))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(19); + + assertThat(condition.shouldRetry(context(EXCEPTION_2))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(9); + + assertThat(condition.shouldRetry(context(EXCEPTION_2))).isFalse(); + assertThat(condition.tokensAvailable()).isEqualTo(9); + + assertThat(condition.shouldRetry(context(EXCEPTION))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(8); + } + + @Test + public void successReleasesAcquiredCost() { + ExecutionAttributes attributes = new ExecutionAttributes(); + + TokenBucketRetryCondition condition = create(20, e -> 10); + + assertThat(condition.shouldRetry(context(EXCEPTION, attributes))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(10); + + condition.requestSucceeded(context(EXCEPTION, attributes)); + assertThat(condition.tokensAvailable()).isEqualTo(20); + } + + @Test + public void firstRequestSuccessReleasesOne() { + TokenBucketRetryCondition condition = create(20, e -> 10); + + assertThat(condition.shouldRetry(context(null))).isTrue(); + assertThat(condition.tokensAvailable()).isEqualTo(10); + + condition.requestSucceeded(context(null)); + assertThat(condition.tokensAvailable()).isEqualTo(11); + + condition.requestSucceeded(context(null)); + assertThat(condition.tokensAvailable()).isEqualTo(12); + } + + @Test + public void conditionSeemsToBeThreadSafe() throws InterruptedException { + int bucketSize = 5; + TokenBucketRetryCondition condition = create(bucketSize, e -> 1); + + AtomicInteger concurrentCalls = new AtomicInteger(0); + AtomicBoolean failure = new AtomicBoolean(false); + int parallelism = bucketSize * 2; + ExecutorService executor = Executors.newFixedThreadPool(parallelism); + for (int i = 0; i < parallelism; ++i) { + executor.submit(() -> { + try { + for (int j = 0; j < 1000; ++j) { + ExecutionAttributes attributes = new ExecutionAttributes(); + if (condition.shouldRetry(context(EXCEPTION, attributes))) { + int calls = concurrentCalls.addAndGet(1); + if (calls > bucketSize) { + failure.set(true); + } + Thread.sleep(1); + concurrentCalls.addAndGet(-1); + condition.requestSucceeded(context(EXCEPTION, attributes)); + } + else { + Thread.sleep(1); + } + } + } catch (Throwable t) { + t.printStackTrace(); + failure.set(true); + } + }); + + // Stagger the threads a bit. + Thread.sleep(1); + } + + executor.shutdown(); + if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { + Assert.fail(); + } + + assertThat(failure.get()).isFalse(); + } + + private RetryPolicyContext context(SdkException lastException) { + return RetryPolicyContext.builder() + .executionAttributes(new ExecutionAttributes()) + .exception(lastException) + .build(); + } + + private RetryPolicyContext context(SdkException lastException, ExecutionAttributes attributes) { + return RetryPolicyContext.builder() + .executionAttributes(attributes) + .exception(lastException) + .build(); + } + + private TokenBucketRetryCondition create(int size, TokenBucketExceptionCostFunction function) { + return TokenBucketRetryCondition.builder() + .tokenBucketSize(size) + .exceptionCostFunction(function) + .build(); + } + +} \ No newline at end of file diff --git a/core/sdk-core/src/test/java/utils/HttpTestUtils.java b/core/sdk-core/src/test/java/utils/HttpTestUtils.java index 2402f5733200..ba7929f229bf 100644 --- a/core/sdk-core/src/test/java/utils/HttpTestUtils.java +++ b/core/sdk-core/src/test/java/utils/HttpTestUtils.java @@ -26,8 +26,8 @@ import java.util.stream.Collectors; import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.internal.http.AmazonAsyncHttpClient; import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder; diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertyMixedCase b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertyMixedCase new file mode 100644 index 000000000000..87f4e3cc86d6 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertyMixedCase @@ -0,0 +1,2 @@ +[default] +retry_mode = sTanDard \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertyNotSet b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertyNotSet new file mode 100644 index 000000000000..399487f9b5e5 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertyNotSet @@ -0,0 +1 @@ +[default] \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetMaxAttempts10 b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetMaxAttempts10 new file mode 100644 index 000000000000..563a6a7d6859 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetMaxAttempts10 @@ -0,0 +1,2 @@ +[default] +max_attempts = 10 \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToLegacy b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToLegacy new file mode 100644 index 000000000000..0adbcda85ce5 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToLegacy @@ -0,0 +1,2 @@ +[default] +retry_mode = legacy \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToStandard b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToStandard new file mode 100644 index 000000000000..14dba934fa14 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToStandard @@ -0,0 +1,2 @@ +[default] +retry_mode = standard \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToUnsupportedValue b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToUnsupportedValue new file mode 100644 index 000000000000..e145fd53d204 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/retry/PropertySetToUnsupportedValue @@ -0,0 +1,3 @@ +[default] +retry_mode = unsupported-value +max_attempts = unsupported-value \ No newline at end of file diff --git a/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java b/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java index 4575a903a386..9c19c0fc5b59 100644 --- a/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java +++ b/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java @@ -16,9 +16,10 @@ package software.amazon.awssdk.services.dynamodb; import java.time.Duration; -import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.awscore.retry.AwsRetryPolicy; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; +import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; import software.amazon.awssdk.core.retry.backoff.FullJitterBackoffStrategy; @@ -26,44 +27,60 @@ /** * Default retry policy for DynamoDB Client. */ -@SdkProtectedApi -public final class DynamoDbRetryPolicy { +@SdkInternalApi +final class DynamoDbRetryPolicy { - /** Default max retry count for DynamoDB client **/ - private static final int DEFAULT_MAX_ERROR_RETRY = 8; + /** + * Default max retry count for DynamoDB client, when using the LEGACY retry mode. + **/ + private static final int LEGACY_MAX_ERROR_RETRY = 8; /** - * Default base sleep time for DynamoDB. + * Default base sleep time for DynamoDB, when using the LEGACY retry mode. **/ - private static final Duration DEFAULT_BASE_DELAY = Duration.ofMillis(25); + private static final Duration LEGACY_BASE_DELAY = Duration.ofMillis(25); /** * The default back-off strategy for DynamoDB client, which increases * exponentially up to a max amount of delay. Compared to the SDK default * back-off strategy, it applies a smaller scale factor. + * + * This is only used when using the LEGACY retry mode. */ - private static final BackoffStrategy DEFAULT_BACKOFF_STRATEGY = + private static final BackoffStrategy LEGACY_BACKOFF_STRATEGY = FullJitterBackoffStrategy.builder() - .baseDelay(DEFAULT_BASE_DELAY) + .baseDelay(LEGACY_BASE_DELAY) .maxBackoffTime(SdkDefaultRetrySetting.MAX_BACKOFF) .build(); /** - * Default retry policy for DynamoDB. + * Default retry policy for DynamoDB, when using the LEGACY retry mode. */ - private static final RetryPolicy DEFAULT = - AwsRetryPolicy.defaultRetryPolicy().toBuilder() - .numRetries(DEFAULT_MAX_ERROR_RETRY) - .backoffStrategy(DEFAULT_BACKOFF_STRATEGY).build(); + private static final RetryPolicy LEGACY_RETRY_POLICY = + AwsRetryPolicy.defaultRetryPolicy() + .toBuilder() + .numRetries(LEGACY_MAX_ERROR_RETRY) + .backoffStrategy(LEGACY_BACKOFF_STRATEGY) + .additionalRetryConditionsAllowed(false) + .build(); - private DynamoDbRetryPolicy() { - - } + private DynamoDbRetryPolicy() {} /** * @return Default retry policy used by DynamoDbClient */ - public static RetryPolicy defaultPolicy() { - return DEFAULT; + public static RetryPolicy defaultRetryPolicy() { + if (RetryMode.defaultRetryMode() == RetryMode.LEGACY) { + return LEGACY_RETRY_POLICY; + } + + return AwsRetryPolicy.defaultRetryPolicy() + .toBuilder() + .additionalRetryConditionsAllowed(false) + .build(); + } + + public static RetryPolicy addRetryConditions(RetryPolicy policy) { + return policy; } } diff --git a/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java b/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java index 01e177600571..aaf33f84fb37 100644 --- a/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java +++ b/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java @@ -18,34 +18,39 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.awscore.retry.AwsRetryPolicy; import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.RetryPolicyContext; import software.amazon.awssdk.core.retry.conditions.AndRetryCondition; import software.amazon.awssdk.services.kinesis.model.SubscribeToShardRequest; /** - * Default retry policy for the Kinesis Client. + * Default retry policy for the Kinesis Client. Disables retries on subscribe-to-shard. */ @SdkInternalApi final class KinesisRetryPolicy { + private KinesisRetryPolicy() {} + + public static RetryPolicy defaultRetryPolicy() { + return AwsRetryPolicy.defaultRetryPolicy() + .toBuilder() + .retryCondition(AndRetryCondition.create(KinesisRetryPolicy::isNotSubscribeToShard, + AwsRetryPolicy.defaultRetryCondition())) + .additionalRetryConditionsAllowed(false) + .build(); + } - /** - * Default retry policy for Kinesis. Turns off retries for SubscribeToShard - */ - private static final RetryPolicy DEFAULT = - AwsRetryPolicy.defaultRetryPolicy().toBuilder() - .retryCondition(AndRetryCondition.create( - c -> !(c.originalRequest() instanceof SubscribeToShardRequest), - AwsRetryPolicy.defaultRetryCondition())) - .build(); - - private KinesisRetryPolicy() { + public static RetryPolicy addRetryConditions(RetryPolicy policy) { + if (!policy.additionalRetryConditionsAllowed()) { + return policy; + } + return policy.toBuilder() + .retryCondition(AndRetryCondition.create(KinesisRetryPolicy::isNotSubscribeToShard, + policy.retryCondition())) + .build(); } - /** - * @return Default retry policy used by Kinesis - */ - public static RetryPolicy defaultPolicy() { - return DEFAULT; + private static boolean isNotSubscribeToShard(RetryPolicyContext context) { + return !(context.originalRequest() instanceof SubscribeToShardRequest); } } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/usearnregion/ProfileUseArnRegionProvider.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/usearnregion/ProfileUseArnRegionProvider.java index 628cb4b768cf..0fa71ff2f6d1 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/usearnregion/ProfileUseArnRegionProvider.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/usearnregion/ProfileUseArnRegionProvider.java @@ -19,6 +19,7 @@ import static software.amazon.awssdk.profiles.ProfileFileSystemSetting.AWS_PROFILE; import java.util.Optional; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.utils.StringUtils; @@ -33,17 +34,20 @@ public final class ProfileUseArnRegionProvider implements UseArnRegionProvider { */ private static final String AWS_USE_ARN_REGION = "s3_use_arn_region"; - private ProfileUseArnRegionProvider() { + private Supplier profileFile; + + private ProfileUseArnRegionProvider(Supplier profileFile) { + this.profileFile = profileFile; } public static ProfileUseArnRegionProvider create() { - return new ProfileUseArnRegionProvider(); + return new ProfileUseArnRegionProvider(ProfileFile::defaultProfileFile); } @Override public Optional resolveUseArnRegion() { return AWS_CONFIG_FILE.getStringValue() - .flatMap(s -> ProfileFile.defaultProfileFile().profile(AWS_PROFILE.getStringValueOrThrow())) + .flatMap(s -> profileFile.get().profile(AWS_PROFILE.getStringValueOrThrow())) .map(p -> p.properties().get(AWS_USE_ARN_REGION)) .map(StringUtils::safeStringToBoolean); } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AsyncClientRetryModeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AsyncClientRetryModeTest.java new file mode 100644 index 000000000000..1151107bb359 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/AsyncClientRetryModeTest.java @@ -0,0 +1,42 @@ +/* + * 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.services.retry; + +import java.util.concurrent.CompletionException; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClientBuilder; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public class AsyncClientRetryModeTest + extends ClientRetryModeTestSuite { + @Override + protected ProtocolRestJsonAsyncClientBuilder newClientBuilder() { + return ProtocolRestJsonAsyncClient.builder(); + } + + @Override + protected AllTypesResponse callAllTypes(ProtocolRestJsonAsyncClient client) { + try { + return client.allTypes().join(); + } catch (CompletionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + + throw e; + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/ClientRetryModeTestSuite.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/ClientRetryModeTestSuite.java new file mode 100644 index 000000000000..ca25b6e80aaf --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/ClientRetryModeTestSuite.java @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2020 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.services.retry; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public abstract class ClientRetryModeTestSuite> { + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + @Test + public void legacyRetryModeIsFourAttempts() { + stubThrottlingResponse(); + ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); + assertThatThrownBy(() -> callAllTypes(client)).isInstanceOf(SdkException.class); + verifyRequestCount(4); + } + + @Test + public void standardRetryModeIsThreeAttempts() { + stubThrottlingResponse(); + ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.STANDARD)).build(); + assertThatThrownBy(() -> callAllTypes(client)).isInstanceOf(SdkException.class); + verifyRequestCount(3); + } + + @Test + public void legacyRetryModeExcludesThrottlingExceptions() throws InterruptedException { + stubThrottlingResponse(); + + ExecutorService executor = Executors.newFixedThreadPool(51); + ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); + + for (int i = 0; i < 51; ++i) { + executor.execute(() -> assertThatThrownBy(() -> callAllTypes(client)).isInstanceOf(SdkException.class)); + } + executor.shutdown(); + assertThat(executor.awaitTermination(30, TimeUnit.SECONDS)).isTrue(); + + // 51 requests * 4 attempts = 204 requests + verifyRequestCount(204); + } + + @Test + public void standardRetryModeIncludesThrottlingExceptions() throws InterruptedException { + stubThrottlingResponse(); + + ExecutorService executor = Executors.newFixedThreadPool(51); + ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.STANDARD)).build(); + + for (int i = 0; i < 51; ++i) { + executor.execute(() -> assertThatThrownBy(() -> callAllTypes(client)).isInstanceOf(SdkException.class)); + } + executor.shutdown(); + assertThat(executor.awaitTermination(30, TimeUnit.SECONDS)).isTrue(); + + // Would receive 153 without throttling (51 requests * 3 attempts = 153 requests) + verifyRequestCount(151); + } + + private BuilderT clientBuilder() { + return newClientBuilder().credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())); + } + + protected abstract BuilderT newClientBuilder(); + + protected abstract AllTypesResponse callAllTypes(ClientT client); + + private void verifyRequestCount(int count) { + verify(count, anyRequestedFor(anyUrl())); + } + + private void stubThrottlingResponse() { + stubFor(post(anyUrl()) + .willReturn(aResponse().withStatus(429))); + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/SyncClientRetryModeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/SyncClientRetryModeTest.java new file mode 100644 index 000000000000..1d6f4e60adb4 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/retry/SyncClientRetryModeTest.java @@ -0,0 +1,32 @@ +/* + * 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.services.retry; + +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClientBuilder; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public class SyncClientRetryModeTest extends ClientRetryModeTestSuite { + @Override + protected ProtocolRestJsonClientBuilder newClientBuilder() { + return ProtocolRestJsonClient.builder(); + } + + @Override + protected AllTypesResponse callAllTypes(ProtocolRestJsonClient client) { + return client.allTypes(); + } +} diff --git a/utils/src/main/java/software/amazon/awssdk/utils/Lazy.java b/utils/src/main/java/software/amazon/awssdk/utils/Lazy.java new file mode 100644 index 000000000000..6923ab8a0bd9 --- /dev/null +++ b/utils/src/main/java/software/amazon/awssdk/utils/Lazy.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.utils; + +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A class that lazily constructs a value the first time {@link #getValue()} is invoked. + */ +@SdkPublicApi +public class Lazy { + private final Supplier initializer; + + private volatile T value; + + public Lazy(Supplier initializer) { + this.initializer = initializer; + } + + public T getValue() { + T result = value; + if (result == null) { + synchronized (this) { + result = value; + if (result == null) { + result = initializer.get(); + value = result; + } + } + } + + return result; + } + + @Override + public String toString() { + T value = this.value; + return ToString.builder("Lazy") + .add("value", value == null ? "Uninitialized" : value) + .build(); + } +} diff --git a/utils/src/main/java/software/amazon/awssdk/utils/SystemSetting.java b/utils/src/main/java/software/amazon/awssdk/utils/SystemSetting.java index b5d377bf80ed..c5e3dff8df86 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/SystemSetting.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/SystemSetting.java @@ -67,6 +67,32 @@ default String getStringValueOrThrow() { + "property " + property() + " must be set.")); } + /** + * Attempt to load a system setting from {@link System#getProperty(String)} and {@link System#getenv(String)}. This should be + * used in favor of those methods because the SDK should support both methods of configuration. + * + * The result will be converted to an integer. + * + * {@link System#getProperty(String)} takes precedent over {@link System#getenv(String)} if both are specified. + * + * @return The requested setting, or {@link Optional#empty()} if the values were not set, or the security manager did not + * allow reading the setting. + */ + default Optional getIntegerValue() { + return getStringValue().map(Integer::parseInt); + } + + + /** + * Load the requested system setting as per the documentation in {@link #getIntegerValue()}, throwing an exception if the + * value was not set and had no default. + * + * @return The requested setting. + */ + default Integer getIntegerValueOrThrow() { + return Integer.parseInt(getStringValueOrThrow()); + } + /** * Attempt to load a system setting from {@link System#getProperty(String)} and {@link System#getenv(String)}. This should be * used in favor of those methods because the SDK should support both methods of configuration. diff --git a/utils/src/test/java/software/amazon/awssdk/utils/LazyTest.java b/utils/src/test/java/software/amazon/awssdk/utils/LazyTest.java new file mode 100644 index 000000000000..288f74ce9d38 --- /dev/null +++ b/utils/src/test/java/software/amazon/awssdk/utils/LazyTest.java @@ -0,0 +1,95 @@ +/* + * 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.utils; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.times; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class LazyTest { + private Lazy lazy; + private Supplier mockDelegate; + + @Before + public void setup() { + mockDelegate = Mockito.mock(Supplier.class); + lazy = new Lazy<>(mockDelegate); + } + + @Test + public void delegateNotCalledOnCreation() { + Mockito.verifyZeroInteractions(mockDelegate); + } + + @Test + public void nullIsNotCached() { + Mockito.when(mockDelegate.get()).thenReturn(null); + lazy.getValue(); + lazy.getValue(); + Mockito.verify(mockDelegate, times(2)).get(); + } + + @Test + public void exceptionsAreNotCached() { + IllegalStateException exception = new IllegalStateException(); + Mockito.when(mockDelegate.get()).thenThrow(exception); + assertThatThrownBy(lazy::getValue).isEqualTo(exception); + assertThatThrownBy(lazy::getValue).isEqualTo(exception); + Mockito.verify(mockDelegate, times(2)).get(); + } + + @Test(timeout = 10_000) + public void delegateCalledOnlyOnce() throws Exception { + final int threads = 5; + ExecutorService executor = Executors.newFixedThreadPool(threads); + try { + for (int i = 0; i < 1000; ++i) { + mockDelegate = Mockito.mock(Supplier.class); + Mockito.when(mockDelegate.get()).thenReturn(""); + lazy = new Lazy<>(mockDelegate); + + CountDownLatch everyoneIsWaitingLatch = new CountDownLatch(threads); + CountDownLatch everyoneIsDoneLatch = new CountDownLatch(threads); + CountDownLatch callGetValueLatch = new CountDownLatch(1); + + for (int j = 0; j < threads; ++j) { + executor.submit(() -> { + everyoneIsWaitingLatch.countDown(); + callGetValueLatch.await(); + lazy.getValue(); + everyoneIsDoneLatch.countDown(); + return null; + }); + } + + everyoneIsWaitingLatch.await(); + callGetValueLatch.countDown(); + everyoneIsDoneLatch.await(); + + Mockito.verify(mockDelegate, times(1)).get(); + } + } finally { + executor.shutdownNow(); + } + } +} \ No newline at end of file diff --git a/utils/src/test/java/software/amazon/awssdk/utils/NumericUtilsTest.java b/utils/src/test/java/software/amazon/awssdk/utils/NumericUtilsTest.java index 7bef92be97b2..5f4a54b8f279 100644 --- a/utils/src/test/java/software/amazon/awssdk/utils/NumericUtilsTest.java +++ b/utils/src/test/java/software/amazon/awssdk/utils/NumericUtilsTest.java @@ -1,3 +1,18 @@ +/* + * 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.utils; import java.time.Duration;