diff --git a/aws/rust-runtime/aws-http/Cargo.toml b/aws/rust-runtime/aws-http/Cargo.toml index be5cedd5d8..f09f8bc8e3 100644 --- a/aws/rust-runtime/aws-http/Cargo.toml +++ b/aws/rust-runtime/aws-http/Cargo.toml @@ -28,7 +28,6 @@ aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" } aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" } bytes-utils = "0.1.2" env_logger = "0.9" -http = "0.2.3" tokio = { version = "1.23.1", features = ["macros", "rt", "rt-multi-thread", "test-util", "time"] } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } proptest = "1" diff --git a/aws/rust-runtime/aws-runtime/external-types.toml b/aws/rust-runtime/aws-runtime/external-types.toml index 90b77e92e0..a38a0e0b57 100644 --- a/aws/rust-runtime/aws-runtime/external-types.toml +++ b/aws/rust-runtime/aws-runtime/external-types.toml @@ -1,7 +1,8 @@ allowed_external_types = [ "aws_credential_types::*", "aws_sigv4::*", - "aws_smithy_http::body::SdkBody", + "aws_smithy_http::*", + "aws_smithy_types::*", "aws_smithy_runtime_api::*", "aws_types::*", "http::request::Request", diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index e218303269..9df20ae3bc 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -25,5 +25,8 @@ pub mod recursion_detection; /// Supporting code for user agent headers in the AWS SDK. pub mod user_agent; +/// Supporting code for retry behavior specific to the AWS SDK. +pub mod retries; + /// Supporting code for invocation ID headers in the AWS SDK. pub mod invocation_id; diff --git a/aws/rust-runtime/aws-runtime/src/retries.rs b/aws/rust-runtime/aws-runtime/src/retries.rs new file mode 100644 index 0000000000..ed12aa9cde --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/retries.rs @@ -0,0 +1,7 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// Classifiers that can inspect a response and determine if it should be retried. +pub mod classifier; diff --git a/aws/rust-runtime/aws-runtime/src/retries/classifier.rs b/aws/rust-runtime/aws-runtime/src/retries/classifier.rs new file mode 100644 index 0000000000..7e9c10d59b --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/retries/classifier.rs @@ -0,0 +1,174 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_http::http::HttpHeaders; +use aws_smithy_http::result::SdkError; +use aws_smithy_runtime_api::client::retries::RetryReason; +use aws_smithy_types::error::metadata::ProvideErrorMetadata; +use aws_smithy_types::retry::ErrorKind; + +/// AWS error codes that represent throttling errors. +pub const THROTTLING_ERRORS: &[&str] = &[ + "Throttling", + "ThrottlingException", + "ThrottledException", + "RequestThrottledException", + "TooManyRequestsException", + "ProvisionedThroughputExceededException", + "TransactionInProgressException", + "RequestLimitExceeded", + "BandwidthLimitExceeded", + "LimitExceededException", + "RequestThrottled", + "SlowDown", + "PriorRequestNotComplete", + "EC2ThrottledException", +]; + +/// AWS error codes that represent transient errors. +pub const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"]; + +/// A retry classifier for determining if the response sent by an AWS service requires a retry. +#[derive(Debug)] +pub struct AwsErrorCodeClassifier; + +impl AwsErrorCodeClassifier { + /// Classify an error code to check if represents a retryable error. The codes of retryable + /// errors are defined [here](THROTTLING_ERRORS) and [here](TRANSIENT_ERRORS). + pub fn classify_error( + &self, + error: &SdkError, + ) -> Option { + if let Some(error_code) = error.code() { + if THROTTLING_ERRORS.contains(&error_code) { + return Some(RetryReason::Error(ErrorKind::ThrottlingError)); + } else if TRANSIENT_ERRORS.contains(&error_code) { + return Some(RetryReason::Error(ErrorKind::TransientError)); + } + }; + + None + } +} + +/// A retry classifier that checks for `x-amz-retry-after` headers. If one is found, a +/// [`RetryReason::Explicit`] is returned containing the duration to wait before retrying. +#[derive(Debug)] +pub struct AmzRetryAfterHeaderClassifier; + +impl AmzRetryAfterHeaderClassifier { + /// Classify an AWS responses error code to determine how (and if) it should be retried. + pub fn classify_error(&self, error: &SdkError) -> Option { + error + .raw_response() + .and_then(|res| res.http_headers().get("x-amz-retry-after")) + .and_then(|header| header.to_str().ok()) + .and_then(|header| header.parse::().ok()) + .map(|retry_after_delay| { + RetryReason::Explicit(std::time::Duration::from_millis(retry_after_delay)) + }) + } +} + +#[cfg(test)] +mod test { + use super::{AmzRetryAfterHeaderClassifier, AwsErrorCodeClassifier}; + use aws_smithy_http::body::SdkBody; + use aws_smithy_http::operation; + use aws_smithy_http::result::SdkError; + use aws_smithy_runtime_api::client::retries::RetryReason; + use aws_smithy_types::error::metadata::ProvideErrorMetadata; + use aws_smithy_types::error::ErrorMetadata; + use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind}; + use std::fmt; + use std::time::Duration; + + #[derive(Debug)] + struct UnmodeledError; + + impl fmt::Display for UnmodeledError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UnmodeledError") + } + } + + impl std::error::Error for UnmodeledError {} + + struct CodedError { + metadata: ErrorMetadata, + } + + impl CodedError { + fn new(code: &'static str) -> Self { + Self { + metadata: ErrorMetadata::builder().code(code).build(), + } + } + } + + impl ProvideErrorKind for UnmodeledError { + fn retryable_error_kind(&self) -> Option { + None + } + + fn code(&self) -> Option<&str> { + None + } + } + + impl ProvideErrorMetadata for CodedError { + fn meta(&self) -> &ErrorMetadata { + &self.metadata + } + } + + #[test] + fn classify_by_error_code() { + let policy = AwsErrorCodeClassifier; + let res = http::Response::new("OK"); + let err = SdkError::service_error(CodedError::new("Throttling"), res); + + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::ThrottlingError)) + ); + + let res = http::Response::new("OK"); + let err = SdkError::service_error(CodedError::new("RequestTimeout"), res); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)) + ) + } + + #[test] + fn classify_generic() { + let policy = AwsErrorCodeClassifier; + let res = http::Response::new("OK"); + let err = aws_smithy_types::Error::builder().code("SlowDown").build(); + let err = SdkError::service_error(err, res); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::ThrottlingError)) + ); + } + + #[test] + fn test_retry_after_header() { + let policy = AmzRetryAfterHeaderClassifier; + let res = http::Response::builder() + .header("x-amz-retry-after", "5000") + .body("retry later") + .unwrap() + .map(SdkBody::from); + let res = operation::Response::new(res); + let err = SdkError::service_error(UnmodeledError, res); + + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Explicit(Duration::from_millis(5000))), + ); + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt index 0d488fb21f..ef730207a7 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt @@ -8,7 +8,12 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginSection +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.derive import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType @@ -23,13 +28,21 @@ class RetryClassifierDecorator : ClientCodegenDecorator { codegenContext: ClientCodegenContext, operation: OperationShape, baseCustomizations: List, - ): List { - return baseCustomizations + RetryClassifierFeature(codegenContext.runtimeConfig) - } + ): List = + baseCustomizations + RetryClassifierFeature(codegenContext.runtimeConfig) + + override fun operationRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + operation: OperationShape, + baseCustomizations: List, + ): List = + baseCustomizations + OperationRetryClassifiersFeature(codegenContext, operation) } class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : OperationCustomization() { - override fun retryType(): RuntimeType = AwsRuntimeType.awsHttp(runtimeConfig).resolve("retry::AwsResponseRetryClassifier") + override fun retryType(): RuntimeType = + AwsRuntimeType.awsHttp(runtimeConfig).resolve("retry::AwsResponseRetryClassifier") + override fun section(section: OperationSection) = when (section) { is OperationSection.FinalizeOperation -> writable { rust( @@ -41,3 +54,140 @@ class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : Operati else -> emptySection } } + +class OperationRetryClassifiersFeature( + codegenContext: ClientCodegenContext, + operation: OperationShape, +) : OperationRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) + private val smithyRuntime = RuntimeType.smithyRuntime(runtimeConfig) + private val smithyRuntimeApi = RuntimeType.smithyRuntimeApi(runtimeConfig) + private val codegenScope = arrayOf( + "HttpStatusCodeClassifier" to smithyRuntime.resolve("client::retries::classifier::HttpStatusCodeClassifier"), + "AwsErrorCodeClassifier" to awsRuntime.resolve("retries::classifier::AwsErrorCodeClassifier"), + "ModeledAsRetryableClassifier" to smithyRuntime.resolve("client::retries::classifier::ModeledAsRetryableClassifier"), + "AmzRetryAfterHeaderClassifier" to awsRuntime.resolve("retries::classifier::AmzRetryAfterHeaderClassifier"), + "SmithyErrorClassifier" to smithyRuntime.resolve("client::retries::classifier::SmithyErrorClassifier"), + "RetryReason" to smithyRuntimeApi.resolve("client::retries::RetryReason"), + "ClassifyRetry" to smithyRuntimeApi.resolve("client::retries::ClassifyRetry"), + "RetryClassifiers" to smithyRuntimeApi.resolve("client::retries::RetryClassifiers"), + "OperationError" to codegenContext.symbolProvider.symbolForOperationError(operation), + "SdkError" to RuntimeType.smithyHttp(runtimeConfig).resolve("result::SdkError"), + "ErasedError" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("type_erasure::TypeErasedBox"), + ) + + override fun section(section: OperationRuntimePluginSection) = when (section) { + is OperationRuntimePluginSection.RuntimePluginSupportingTypes -> writable { + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct HttpStatusCodeClassifier(#{HttpStatusCodeClassifier}); + impl HttpStatusCodeClassifier { + fn new() -> Self { + Self(#{HttpStatusCodeClassifier}::default()) + } + } + impl #{ClassifyRetry} for HttpStatusCodeClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct AwsErrorCodeClassifier(#{AwsErrorCodeClassifier}); + impl AwsErrorCodeClassifier { + fn new() -> Self { + Self(#{AwsErrorCodeClassifier}) + } + } + impl #{ClassifyRetry} for AwsErrorCodeClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct ModeledAsRetryableClassifier(#{ModeledAsRetryableClassifier}); + impl ModeledAsRetryableClassifier { + fn new() -> Self { + Self(#{ModeledAsRetryableClassifier}) + } + } + impl #{ClassifyRetry} for ModeledAsRetryableClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct AmzRetryAfterHeaderClassifier(#{AmzRetryAfterHeaderClassifier}); + impl AmzRetryAfterHeaderClassifier { + fn new() -> Self { + Self(#{AmzRetryAfterHeaderClassifier}) + } + } + impl #{ClassifyRetry} for AmzRetryAfterHeaderClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct SmithyErrorClassifier(#{SmithyErrorClassifier}); + impl SmithyErrorClassifier { + fn new() -> Self { + Self(#{SmithyErrorClassifier}) + } + } + impl #{ClassifyRetry} for SmithyErrorClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + } + + is OperationRuntimePluginSection.RetryClassifier -> writable { + rustTemplate( + """ + .with_classifier(SmithyErrorClassifier::new()) + .with_classifier(AmzRetryAfterHeaderClassifier::new()) + .with_classifier(ModeledAsRetryableClassifier::new()) + .with_classifier(AwsErrorCodeClassifier::new()) + .with_classifier(HttpStatusCodeClassifier::new()) + """, + *codegenScope, + ) + } + + else -> emptySection + } +} diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs index 70dd23d400..28d8d9a3b7 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs @@ -24,11 +24,13 @@ use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorConte use aws_smithy_runtime_api::client::orchestrator::{ BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, RequestTime, TraceProbe, }; +use aws_smithy_runtime_api::client::retries::RetryClassifiers; use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; use aws_smithy_runtime_api::config_bag::ConfigBag; use aws_smithy_runtime_api::type_erasure::TypedBox; use aws_types::region::SigningRegion; use aws_types::SigningService; +use http::Uri; use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; @@ -104,7 +106,7 @@ async fn sra_manual_test() { cfg.put(params_builder); cfg.set_retry_strategy( - aws_smithy_runtime_api::client::retries::NeverRetryStrategy::new(), + aws_smithy_runtime::client::retries::strategy::StandardRetryStrategy::default(), ); let connection: Box = Box::new(DynConnectorAdapter::new( @@ -170,7 +172,7 @@ async fn sra_manual_test() { .ok_or_else(|| "failed to downcast to ListObjectsV2Input")?; let mut params_builder = cfg .get::() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); params_builder = params_builder.set_bucket(input.bucket.clone()); cfg.put(params_builder); @@ -189,7 +191,7 @@ async fn sra_manual_test() { ) -> Result<(), BoxError> { let params_builder = cfg .get::() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); let params = params_builder.build().map_err(|err| { ContextAttachedError::new("endpoint params could not be built", err) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt index 243f563d4e..7f23f7a853 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt @@ -250,6 +250,8 @@ private class HttpAuthOperationRuntimePluginCustomization( rustTemplate("${section.configBagName}.set_auth_option_resolver(auth_option_resolver);", *codegenScope) } + + else -> emptySection } } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt index 0299eb21c7..b00a020508 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt @@ -84,10 +84,10 @@ class EndpointParamsInterceptorGenerator( let input = context.input()?; let _input = input .downcast_ref::<${operationInput.name}>() - .ok_or_else(|| "failed to downcast to ${operationInput.name}")?; + .ok_or("failed to downcast to ${operationInput.name}")?; let params_builder = cfg .get::<#{ParamsBuilder}>() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); ${"" /* TODO(EndpointResolver): Call setters on `params_builder` to update its fields by using values from `_input` */} cfg.put(params_builder); @@ -131,7 +131,7 @@ class EndpointParamsInterceptorGenerator( let _ = context; let params_builder = cfg .get::<#{ParamsBuilder}>() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); let params = params_builder .build() diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt index 2ad5d454a2..7c2d1e7662 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt @@ -40,6 +40,29 @@ sealed class OperationRuntimePluginSection(name: String) : Section(name) { ) } } + + /** + * Hook for adding retry classifiers to an operation's `RetryClassifiers` bundle. + * + * Should emit 1+ lines of code that look like the following: + * ```rust + * .with_classifier(AwsErrorCodeClassifier::new()) + * .with_classifier(HttpStatusCodeClassifier::new()) + * ``` + */ + data class RetryClassifier( + val configBagName: String, + val operationShape: OperationShape, + ) : OperationRuntimePluginSection("RetryClassifier") + + /** + * Hook for adding supporting types for operation-specific runtime plugins. + * Examples include various operation-specific types (retry classifiers, config bag types, etc.) + */ + data class RuntimePluginSupportingTypes( + val configBagName: String, + val operationShape: OperationShape, + ) : OperationRuntimePluginSection("RuntimePluginSupportingTypes") } typealias OperationRuntimePluginCustomization = NamedCustomization @@ -58,6 +81,7 @@ class OperationRuntimePluginGenerator( "BoxError" to runtimeApi.resolve("client::runtime_plugin::BoxError"), "ConfigBag" to runtimeApi.resolve("config_bag::ConfigBag"), "ConfigBagAccessors" to runtimeApi.resolve("client::orchestrator::ConfigBagAccessors"), + "RetryClassifiers" to runtimeApi.resolve("client::retries::RetryClassifiers"), "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), ) } @@ -79,10 +103,17 @@ class OperationRuntimePluginGenerator( ${"" /* TODO(IdentityAndAuth): Resolve auth parameters from input for services that need this */} cfg.set_auth_option_resolver_params(#{AuthOptionResolverParams}::new(#{AuthOptionListResolverParams}::new())); + // Retry classifiers are operation-specific because they need to downcast operation-specific error types. + let retry_classifiers = #{RetryClassifiers}::new() + #{retry_classifier_customizations}; + cfg.set_retry_classifiers(retry_classifiers); + #{additional_config} Ok(()) } } + + #{runtime_plugin_supporting_types} """, *codegenScope, "additional_config" to writable { @@ -91,6 +122,15 @@ class OperationRuntimePluginGenerator( OperationRuntimePluginSection.AdditionalConfig("cfg", operationShape), ) }, + "retry_classifier_customizations" to writable { + writeCustomizations(customizations, OperationRuntimePluginSection.RetryClassifier("cfg", operationShape)) + }, + "runtime_plugin_supporting_types" to writable { + writeCustomizations( + customizations, + OperationRuntimePluginSection.RuntimePluginSupportingTypes("cfg", operationShape), + ) + }, ) } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt index ac5804aed9..764efb35df 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt @@ -9,6 +9,7 @@ import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.customize.TestUtilFeature import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfigGenerator import software.amazon.smithy.rust.codegen.client.smithy.generators.error.ServiceErrorGenerator import software.amazon.smithy.rust.codegen.core.rustlang.Attribute @@ -47,6 +48,9 @@ class ServiceGenerator( serviceConfigGenerator.render(this) if (codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + // Enable users to opt in to the test-utils in the runtime crate + rustCrate.mergeFeature(TestUtilFeature.copy(deps = listOf("aws-smithy-runtime/test-util"))) + ServiceRuntimePluginGenerator(codegenContext) .render(this, decorator.serviceRuntimePluginCustomizations(codegenContext, emptyList())) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt index c9955e5fad..7f5963c572 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt @@ -66,8 +66,8 @@ class ServiceRuntimePluginGenerator( private val endpointTypesGenerator = EndpointTypesGenerator.fromContext(codegenContext) private val codegenScope = codegenContext.runtimeConfig.let { rc -> val http = RuntimeType.smithyHttp(rc) - val runtimeApi = RuntimeType.smithyRuntimeApi(rc) val runtime = RuntimeType.smithyRuntime(rc) + val runtimeApi = RuntimeType.smithyRuntimeApi(rc) arrayOf( "AnonymousIdentityResolver" to runtimeApi.resolve("client::identity::AnonymousIdentityResolver"), "AuthOptionListResolver" to runtimeApi.resolve("client::auth::option_resolver::AuthOptionListResolver"), @@ -80,7 +80,7 @@ class ServiceRuntimePluginGenerator( "DynConnectorAdapter" to runtime.resolve("client::connections::adapter::DynConnectorAdapter"), "HttpAuthSchemes" to runtimeApi.resolve("client::orchestrator::HttpAuthSchemes"), "IdentityResolvers" to runtimeApi.resolve("client::identity::IdentityResolvers"), - "NeverRetryStrategy" to runtimeApi.resolve("client::retries::NeverRetryStrategy"), + "NeverRetryStrategy" to runtime.resolve("client::retries::strategy::NeverRetryStrategy"), "Params" to endpointTypesGenerator.paramsStruct(), "ResolveEndpoint" to http.resolve("endpoint::ResolveEndpoint"), "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), diff --git a/rust-runtime/aws-smithy-http/src/result.rs b/rust-runtime/aws-smithy-http/src/result.rs index 0ce42c7cbd..bd6d2f1f3b 100644 --- a/rust-runtime/aws-smithy-http/src/result.rs +++ b/rust-runtime/aws-smithy-http/src/result.rs @@ -5,15 +5,17 @@ //! `Result` wrapper types for [success](SdkSuccess) and [failure](SdkError) responses. -use crate::connection::ConnectionMetadata; -use crate::operation; -use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA}; -use aws_smithy_types::error::ErrorMetadata; -use aws_smithy_types::retry::ErrorKind; use std::error::Error; use std::fmt; use std::fmt::{Debug, Display, Formatter}; +use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA}; +use aws_smithy_types::error::ErrorMetadata; +use aws_smithy_types::retry::ErrorKind; + +use crate::connection::ConnectionMetadata; +use crate::operation; + type BoxError = Box; /// Successful SDK Result @@ -434,6 +436,15 @@ impl SdkError { } } + /// Return a reference to this error's raw response, if it contains one. Otherwise, return `None`. + pub fn raw_response(&self) -> Option<&R> { + match self { + Self::ServiceError(inner) => Some(inner.raw()), + Self::ResponseError(inner) => Some(inner.raw()), + _ => None, + } + } + /// Maps the service error type in `SdkError::ServiceError` #[doc(hidden)] pub fn map_service_error(self, map: impl FnOnce(E) -> E2) -> SdkError { diff --git a/rust-runtime/aws-smithy-runtime-api/src/client.rs b/rust-runtime/aws-smithy-runtime-api/src/client.rs index 192b97a5d4..1c4a97a62b 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client.rs @@ -18,6 +18,7 @@ pub mod orchestrator; /// This code defines when and how failed requests should be retried. It also defines the behavior /// used to limit the rate that requests are sent. pub mod retries; + /// Runtime plugin type definitions. pub mod runtime_plugin; diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs index bd2dcb3289..85233f12ce 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs @@ -6,7 +6,8 @@ use super::identity::{IdentityResolver, IdentityResolvers}; use crate::client::identity::Identity; use crate::client::interceptors::context::{Input, OutputOrError}; -use crate::client::interceptors::InterceptorContext; +use crate::client::retries::RetryClassifiers; +use crate::client::retries::RetryStrategy; use crate::config_bag::ConfigBag; use crate::type_erasure::{TypeErasedBox, TypedBox}; use aws_smithy_async::future::now_or_later::NowOrLater; @@ -54,16 +55,6 @@ impl Connection for Box { } } -pub trait RetryStrategy: Send + Sync + Debug { - fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result<(), BoxError>; - - fn should_attempt_retry( - &self, - context: &InterceptorContext, - cfg: &ConfigBag, - ) -> Result; -} - #[derive(Debug)] pub struct AuthOptionResolverParams(TypeErasedBox); @@ -241,6 +232,9 @@ pub trait ConfigBagAccessors { response_serializer: impl ResponseDeserializer + 'static, ); + fn retry_classifiers(&self) -> &RetryClassifiers; + fn set_retry_classifiers(&mut self, retry_classifier: RetryClassifiers); + fn retry_strategy(&self) -> &dyn RetryStrategy; fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static); @@ -277,25 +271,6 @@ impl ConfigBagAccessors for ConfigBag { self.put::>(Box::new(auth_option_resolver)); } - fn http_auth_schemes(&self) -> &HttpAuthSchemes { - self.get::() - .expect("auth schemes must be set") - } - - fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) { - self.put::(http_auth_schemes); - } - - fn retry_strategy(&self) -> &dyn RetryStrategy { - &**self - .get::>() - .expect("a retry strategy must be set") - } - - fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) { - self.put::>(Box::new(retry_strategy)); - } - fn endpoint_resolver_params(&self) -> &EndpointResolverParams { self.get::() .expect("endpoint resolver params must be set") @@ -334,6 +309,15 @@ impl ConfigBagAccessors for ConfigBag { self.put::>(Box::new(connection)); } + fn http_auth_schemes(&self) -> &HttpAuthSchemes { + self.get::() + .expect("auth schemes must be set") + } + + fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) { + self.put::(http_auth_schemes); + } + fn request_serializer(&self) -> &dyn RequestSerializer { &**self .get::>() @@ -357,6 +341,25 @@ impl ConfigBagAccessors for ConfigBag { self.put::>(Box::new(response_deserializer)); } + fn retry_classifiers(&self) -> &RetryClassifiers { + self.get::() + .expect("retry classifiers must be set") + } + + fn set_retry_classifiers(&mut self, retry_classifiers: RetryClassifiers) { + self.put::(retry_classifiers); + } + + fn retry_strategy(&self) -> &dyn RetryStrategy { + &**self + .get::>() + .expect("a retry strategy must be set") + } + + fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) { + self.put::>(Box::new(retry_strategy)); + } + fn trace_probe(&self) -> &dyn TraceProbe { &**self .get::>() diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs index fd840a7bc9..a53f1d242c 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs @@ -3,32 +3,72 @@ * SPDX-License-Identifier: Apache-2.0 */ +use crate::client::interceptors::context::Error; use crate::client::interceptors::InterceptorContext; -use crate::client::orchestrator::{BoxError, RetryStrategy}; +use crate::client::orchestrator::{BoxError, HttpRequest, HttpResponse}; use crate::config_bag::ConfigBag; -use aws_smithy_http::body::SdkBody; +use aws_smithy_types::retry::ErrorKind; +use std::fmt::Debug; +use std::time::Duration; -pub mod rate_limiting; +/// An answer to the question "should I make a request attempt?" +pub enum ShouldAttempt { + Yes, + No, + YesAfterDelay(Duration), +} + +pub trait RetryStrategy: Send + Sync + Debug { + fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result; -#[derive(Debug, Clone)] -pub struct NeverRetryStrategy {} + fn should_attempt_retry( + &self, + context: &InterceptorContext, + cfg: &ConfigBag, + ) -> Result; +} + +#[non_exhaustive] +#[derive(Eq, PartialEq, Debug)] +pub enum RetryReason { + Error(ErrorKind), + Explicit(Duration), +} + +/// Classifies what kind of retry is needed for a given [`Error`]. +pub trait ClassifyRetry: Send + Sync + Debug { + /// Run this classifier against an error to determine if it should be retried. Returns + /// `Some(RetryKind)` if the error should be retried; Otherwise returns `None`. + fn classify_retry(&self, error: &Error) -> Option; +} -impl NeverRetryStrategy { +#[derive(Debug)] +pub struct RetryClassifiers { + inner: Vec>, +} + +impl RetryClassifiers { pub fn new() -> Self { - Self {} + Self { + // It's always expected that at least one classifier will be defined, + // so we eagerly allocate for it. + inner: Vec::with_capacity(1), + } } -} -impl RetryStrategy for NeverRetryStrategy { - fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result<(), BoxError> { - Ok(()) + pub fn with_classifier(mut self, retry_classifier: impl ClassifyRetry + 'static) -> Self { + self.inner.push(Box::new(retry_classifier)); + + self } - fn should_attempt_retry( - &self, - _context: &InterceptorContext, http::Response>, - _cfg: &ConfigBag, - ) -> Result { - Ok(false) + // TODO(https://github.com/awslabs/smithy-rs/issues/2632) make a map function so users can front-run or second-guess the classifier's decision + // pub fn map_classifiers(mut self, fun: Fn() -> RetryClassifiers) +} + +impl ClassifyRetry for RetryClassifiers { + fn classify_retry(&self, error: &Error) -> Option { + // return the first non-None result + self.inner.iter().find_map(|cr| cr.classify_retry(error)) } } diff --git a/rust-runtime/aws-smithy-runtime/external-types.toml b/rust-runtime/aws-smithy-runtime/external-types.toml index a8a47f10d9..92360e722a 100644 --- a/rust-runtime/aws-smithy-runtime/external-types.toml +++ b/rust-runtime/aws-smithy-runtime/external-types.toml @@ -1,6 +1,7 @@ allowed_external_types = [ "aws_smithy_runtime_api::*", "aws_smithy_http::*", + "aws_smithy_types::*", "aws_smithy_client::erase::DynConnector", # TODO(audit-external-type-usage) We should newtype these or otherwise avoid exposing them "http::header::name::HeaderName", diff --git a/rust-runtime/aws-smithy-runtime/src/client.rs b/rust-runtime/aws-smithy-runtime/src/client.rs index 25c86c7788..d477ce9eae 100644 --- a/rust-runtime/aws-smithy-runtime/src/client.rs +++ b/rust-runtime/aws-smithy-runtime/src/client.rs @@ -9,3 +9,9 @@ pub mod orchestrator; /// Smithy connector runtime plugins pub mod connections; + +/// Smithy code related to retry handling and token buckets. +/// +/// This code defines when and how failed requests should be retried. It also defines the behavior +/// used to limit the rate at which requests are sent. +pub mod retries; diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs index dff37ec7b0..adba3dfb04 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs @@ -13,6 +13,7 @@ use aws_smithy_runtime_api::client::interceptors::{InterceptorContext, Intercept use aws_smithy_runtime_api::client::orchestrator::{ BoxError, ConfigBagAccessors, HttpRequest, HttpResponse, }; +use aws_smithy_runtime_api::client::retries::ShouldAttempt; use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugins; use aws_smithy_runtime_api::config_bag::ConfigBag; use tracing::{debug_span, Instrument}; @@ -61,9 +62,18 @@ pub async fn invoke( let retry_strategy = cfg.retry_strategy(); match retry_strategy.should_attempt_initial_request(cfg) { // Yes, let's make a request - Ok(_) => {} + Ok(ShouldAttempt::Yes) => {} + // No, this request shouldn't be sent + Ok(ShouldAttempt::No) => { + return Err(Phase::dispatch(context).fail( + "The retry strategy indicates that an initial request shouldn't be made, but it didn't specify why.", + )) + } // No, we shouldn't make a request because... Err(err) => return Err(Phase::dispatch(context).fail(err)), + Ok(ShouldAttempt::YesAfterDelay(_)) => { + unreachable!("Delaying the initial request is currently unsupported. If this feature is important to you, please file an issue in GitHub.") + } } } @@ -79,9 +89,12 @@ pub async fn invoke( let retry_strategy = cfg.retry_strategy(); match retry_strategy.should_attempt_retry(&context, cfg) { // Yes, let's retry the request - Ok(true) => continue, + Ok(ShouldAttempt::Yes) => continue, // No, this request shouldn't be retried - Ok(false) => {} + Ok(ShouldAttempt::No) => {} + Ok(ShouldAttempt::YesAfterDelay(_delay)) => { + todo!("implement retries with an explicit delay.") + } // I couldn't determine if the request should be retried because an error occurred. Err(err) => { return Err(Phase::response_handling(context).fail(err)); diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries.rs b/rust-runtime/aws-smithy-runtime/src/client/retries.rs new file mode 100644 index 0000000000..f8bbe70060 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries.rs @@ -0,0 +1,7 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub mod classifier; +pub mod strategy; diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/classifier.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/classifier.rs new file mode 100644 index 0000000000..64960707ad --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/classifier.rs @@ -0,0 +1,195 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_http::result::SdkError; +use aws_smithy_runtime_api::client::retries::RetryReason; +use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind}; +use std::borrow::Cow; + +/// A retry classifier for checking if an error is modeled as retryable. +#[derive(Debug)] +pub struct ModeledAsRetryableClassifier; + +impl ModeledAsRetryableClassifier { + /// Check if an error is modeled as retryable, returning a [`RetryReason::Error`] if it is. + pub fn classify_error( + &self, + error: &SdkError, + ) -> Option { + match error { + SdkError::ServiceError(inner) => { + inner.err().retryable_error_kind().map(RetryReason::Error) + } + _ => None, + } + } +} + +#[derive(Debug)] +pub struct SmithyErrorClassifier; + +impl SmithyErrorClassifier { + pub fn classify_error(&self, result: &SdkError) -> Option { + match result { + SdkError::TimeoutError(_err) => Some(RetryReason::Error(ErrorKind::TransientError)), + SdkError::ResponseError { .. } => Some(RetryReason::Error(ErrorKind::TransientError)), + SdkError::DispatchFailure(err) if (err.is_timeout() || err.is_io()) => { + Some(RetryReason::Error(ErrorKind::TransientError)) + } + SdkError::DispatchFailure(err) => err.is_other().map(RetryReason::Error), + _ => None, + } + } +} + +const TRANSIENT_ERROR_STATUS_CODES: &[u16] = &[500, 502, 503, 504]; + +/// A retry classifier that will treat HTTP response with those status codes as retryable. +/// The `Default` version will retry 500, 502, 503, and 504 errors. +#[derive(Debug)] +pub struct HttpStatusCodeClassifier { + retryable_status_codes: Cow<'static, [u16]>, +} + +impl HttpStatusCodeClassifier { + /// Given a `Vec` where the `u16`s represent status codes, create a retry classifier that will + /// treat HTTP response with those status codes as retryable. The `Default` version will retry + /// 500, 502, 503, and 504 errors. + pub fn new_from_codes(retryable_status_codes: impl Into>) -> Self { + Self { + retryable_status_codes: retryable_status_codes.into(), + } + } + + /// Classify an HTTP response based on its status code. + pub fn classify_error(&self, error: &SdkError) -> Option { + error + .raw_response() + .map(|res| res.http().status().as_u16()) + .map(|status| self.retryable_status_codes.contains(&status)) + .unwrap_or_default() + .then_some(RetryReason::Error(ErrorKind::TransientError)) + } +} + +impl Default for HttpStatusCodeClassifier { + fn default() -> Self { + Self::new_from_codes(TRANSIENT_ERROR_STATUS_CODES.to_owned()) + } +} + +// Generic smithy clients would have something like this: +// pub fn default_retry_classifiers() -> RetryClassifiers { +// RetryClassifiers::new() +// .with_classifier(SmithyErrorClassifier::new()) +// .with_classifier(ModeledAsRetryableClassifier::new()) +// .with_classifier(HttpStatusCodeClassifier::new()) +// } +// This ordering is different than the default AWS ordering because the old generic client classifer +// was the same. + +#[cfg(test)] +mod test { + use std::fmt; + + use crate::client::retries::classifier::{ + HttpStatusCodeClassifier, ModeledAsRetryableClassifier, + }; + use aws_smithy_http::body::SdkBody; + use aws_smithy_http::operation; + use aws_smithy_http::result::SdkError; + use aws_smithy_runtime_api::client::retries::RetryReason; + use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind}; + + use super::SmithyErrorClassifier; + + #[derive(Debug)] + struct UnmodeledError; + + impl fmt::Display for UnmodeledError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UnmodeledError") + } + } + + impl std::error::Error for UnmodeledError {} + + #[test] + fn classify_by_response_status() { + let policy = HttpStatusCodeClassifier::default(); + let res = http::Response::builder() + .status(500) + .body("error!") + .unwrap() + .map(SdkBody::from); + let res = operation::Response::new(res); + let err = SdkError::service_error(UnmodeledError, res); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)) + ); + } + + #[test] + fn classify_by_response_status_not_retryable() { + let policy = HttpStatusCodeClassifier::default(); + let res = http::Response::builder() + .status(408) + .body("error!") + .unwrap() + .map(SdkBody::from); + let res = operation::Response::new(res); + let err = SdkError::service_error(UnmodeledError, res); + + assert_eq!(policy.classify_error(&err), None); + } + + #[test] + fn classify_by_error_kind() { + struct ModeledRetries; + + impl ProvideErrorKind for ModeledRetries { + fn retryable_error_kind(&self) -> Option { + Some(ErrorKind::ClientError) + } + + fn code(&self) -> Option<&str> { + // code should not be called when `error_kind` is provided + unimplemented!() + } + } + + let policy = ModeledAsRetryableClassifier; + let res = http::Response::new("OK"); + let err = SdkError::service_error(ModeledRetries, res); + + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::ClientError)), + ); + } + + #[test] + fn classify_response_error() { + let policy = SmithyErrorClassifier; + let test_response = http::Response::new("OK").map(SdkBody::from); + let err: SdkError = + SdkError::response_error(UnmodeledError, operation::Response::new(test_response)); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)), + ); + } + + #[test] + fn test_timeout_error() { + let policy = SmithyErrorClassifier; + let err: SdkError = SdkError::timeout_error("blah"); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)), + ); + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs new file mode 100644 index 0000000000..6b7854fc2c --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs @@ -0,0 +1,8 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod never; + +pub use never::NeverRetryStrategy; diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/never.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/never.rs new file mode 100644 index 0000000000..49366a273d --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/never.rs @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime_api::client::interceptors::InterceptorContext; +use aws_smithy_runtime_api::client::orchestrator::{BoxError, HttpRequest, HttpResponse}; +use aws_smithy_runtime_api::client::retries::{RetryStrategy, ShouldAttempt}; +use aws_smithy_runtime_api::config_bag::ConfigBag; + +#[derive(Debug, Clone, Default)] +pub struct NeverRetryStrategy {} + +impl NeverRetryStrategy { + pub fn new() -> Self { + Self::default() + } +} + +impl RetryStrategy for NeverRetryStrategy { + fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result { + Ok(ShouldAttempt::Yes) + } + + fn should_attempt_retry( + &self, + _context: &InterceptorContext, + _cfg: &ConfigBag, + ) -> Result { + Ok(ShouldAttempt::No) + } +} diff --git a/rust-runtime/aws-smithy-types/src/error/metadata.rs b/rust-runtime/aws-smithy-types/src/error/metadata.rs index 06925e13f9..6629753733 100644 --- a/rust-runtime/aws-smithy-types/src/error/metadata.rs +++ b/rust-runtime/aws-smithy-types/src/error/metadata.rs @@ -46,6 +46,12 @@ pub struct ErrorMetadata { extras: Option>, } +impl ProvideErrorMetadata for ErrorMetadata { + fn meta(&self) -> &ErrorMetadata { + self + } +} + /// Builder for [`ErrorMetadata`]. #[derive(Debug, Default)] pub struct Builder {