diff --git a/aws/rust-runtime/aws-auth/src/provider.rs b/aws/rust-runtime/aws-auth/src/provider.rs index 22ae7d416cfb44d76b87159ead89bda732718f5e..1f7c6ea167cddc17942250506738527370ec2e5e 100644 --- a/aws/rust-runtime/aws-auth/src/provider.rs +++ b/aws/rust-runtime/aws-auth/src/provider.rs @@ -29,11 +29,17 @@ impl EnvironmentVariableCredentialsProvider { } } +impl Default for EnvironmentVariableCredentialsProvider { + fn default() -> Self { + Self::new() + } +} + fn var(key: &str) -> Result { std::env::var(key) } -const ENV_PROVIDER: &'static str = "EnvironmentVariable"; +const ENV_PROVIDER: &str = "EnvironmentVariable"; impl ProvideCredentials for EnvironmentVariableCredentialsProvider { fn credentials(&self) -> Result { diff --git a/aws/rust-runtime/aws-http/Cargo.toml b/aws/rust-runtime/aws-http/Cargo.toml index db153126f95b00b159e876f455e5ee15f8c182a4..764584d95520922e910fd63ee47a0d9c749f548f 100644 --- a/aws/rust-runtime/aws-http/Cargo.toml +++ b/aws/rust-runtime/aws-http/Cargo.toml @@ -3,12 +3,15 @@ name = "aws-http" version = "0.1.0" authors = ["Russell Cohen "] edition = "2018" +description = "HTTP specific AWS SDK behaviors" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] aws-types = { path = "../aws-types" } smithy-http = { path = "../../../rust-runtime/smithy-http" } -thiserror = "1" +smithy-types = { path = "../../../rust-runtime/smithy-types" } + http = "0.2.3" +thiserror = "1" lazy_static = "1" diff --git a/aws/rust-runtime/aws-http/src/lib.rs b/aws/rust-runtime/aws-http/src/lib.rs index e0b16c8efe4c41e5799d317e9231f1168f94b40d..5db92e4e2a96a7f09585fc212f5681d5b69c9116 100644 --- a/aws/rust-runtime/aws-http/src/lib.rs +++ b/aws/rust-runtime/aws-http/src/lib.rs @@ -1 +1,210 @@ pub mod user_agent; +use smithy_http::retry::ClassifyResponse; +use smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; +use std::time::Duration; + +/// A retry policy that models AWS error codes as outlined in the SEP +/// +/// In order of priority: +/// 1. The `x-amz-retry-after` header is checked +/// 2. The modeled error retry mode is checked +/// 3. The code is checked against a predetermined list of throttling errors & transient error codes +/// 4. The status code is checked against a predetermined list of status codes +#[non_exhaustive] +pub struct AwsErrorRetryPolicy; + +const TRANSIENT_ERROR_STATUS_CODES: [u16; 2] = [400, 408]; +const THROTTLING_ERRORS: &[&str] = &[ + "Throttling", + "ThrottlingException", + "ThrottledException", + "RequestThrottledException", + "TooManyRequestsException", + "ProvisionedThroughputExceededException", + "TransactionInProgressException", + "RequestLimitExceeded", + "BandwidthLimitExceeded", + "LimitExceededException", + "RequestThrottled", + "SlowDown", + "PriorRequestNotComplete", + "EC2ThrottledException", +]; +const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"]; + +impl AwsErrorRetryPolicy { + /// Create an `AwsErrorRetryPolicy` with the default set of known error & status codes + pub fn new() -> Self { + AwsErrorRetryPolicy + } +} + +impl Default for AwsErrorRetryPolicy { + fn default() -> Self { + Self::new() + } +} + +impl ClassifyResponse for AwsErrorRetryPolicy { + fn classify(&self, err: E, response: &http::Response) -> RetryKind + where + E: ProvideErrorKind, + { + if let Some(retry_after_delay) = response + .headers() + .get("x-amz-retry-after") + .and_then(|header| header.to_str().ok()) + .and_then(|header| header.parse::().ok()) + { + return RetryKind::Explicit(Duration::from_millis(retry_after_delay)); + } + if let Some(kind) = err.retryable_error_kind() { + return RetryKind::Error(kind); + }; + if let Some(code) = err.code() { + if THROTTLING_ERRORS.contains(&code) { + return RetryKind::Error(ErrorKind::ThrottlingError); + } + if TRANSIENT_ERRORS.contains(&code) { + return RetryKind::Error(ErrorKind::TransientError); + } + }; + if TRANSIENT_ERROR_STATUS_CODES + .contains(&response.status().as_u16()) + { + return RetryKind::Error(ErrorKind::TransientError); + }; + // TODO: is IDPCommunicationError modeled yet? + RetryKind::NotRetryable + } +} + +#[cfg(test)] +mod test { + use crate::AwsErrorRetryPolicy; + use smithy_http::retry::ClassifyResponse; + use smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; + use std::time::Duration; + + struct UnmodeledError; + + struct CodedError { + code: &'static str, + } + + impl ProvideErrorKind for UnmodeledError { + fn retryable_error_kind(&self) -> Option { + None + } + + fn code(&self) -> Option<&str> { + None + } + } + + impl ProvideErrorKind for CodedError { + fn retryable_error_kind(&self) -> Option { + None + } + + fn code(&self) -> Option<&str> { + Some(self.code) + } + } + + #[test] + fn not_an_error() { + let policy = AwsErrorRetryPolicy::new(); + let test_response = http::Response::new("OK"); + assert_eq!( + policy.classify(UnmodeledError, &test_response), + RetryKind::NotRetryable + ); + } + + #[test] + fn classify_by_response_status() { + let policy = AwsErrorRetryPolicy::new(); + let test_resp = http::Response::builder() + .status(408) + .body("error!") + .unwrap(); + assert_eq!( + policy.classify(UnmodeledError, &test_resp), + RetryKind::Error(ErrorKind::TransientError) + ); + } + + #[test] + fn classify_by_error_code() { + let test_response = http::Response::new("OK"); + let policy = AwsErrorRetryPolicy::new(); + + assert_eq!( + policy.classify(CodedError { code: "Throttling" }, &test_response), + RetryKind::Error(ErrorKind::ThrottlingError) + ); + + assert_eq!( + policy.classify( + CodedError { + code: "RequestTimeout" + }, + &test_response, + ), + RetryKind::Error(ErrorKind::TransientError) + ) + } + + #[test] + fn classify_generic() { + let err = smithy_types::Error { + code: Some("SlowDown".to_string()), + message: None, + request_id: None, + }; + let test_response = http::Response::new("OK"); + let policy = AwsErrorRetryPolicy::new(); + assert_eq!( + policy.classify(err, &test_response), + RetryKind::Error(ErrorKind::ThrottlingError) + ); + } + + #[test] + fn classify_by_error_kind() { + struct ModeledRetries; + let test_response = http::Response::new("OK"); + 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 = AwsErrorRetryPolicy::new(); + + assert_eq!( + policy.classify(ModeledRetries, &test_response), + RetryKind::Error(ErrorKind::ClientError) + ); + } + + #[test] + fn test_retry_after_header() { + let policy = AwsErrorRetryPolicy::new(); + let test_response = http::Response::builder() + .header("x-amz-retry-after", "5000") + .body("retry later") + .unwrap(); + + assert_eq!( + policy.classify(UnmodeledError, &test_response), + RetryKind::Explicit(Duration::from_millis(5000)) + ); + } +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt index d43419d87332133134e457845b329f088985f35d..a8cb6cdf95aa1a3f68f215981b417fbd2318dc6b 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt @@ -53,6 +53,9 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n // TODO: refactor to be RuntimeTypeProvider a la Symbol provider that packages the `RuntimeConfig` state. companion object { + fun errorKind(runtimeConfig: RuntimeConfig) = RuntimeType("ErrorKind", dependency = CargoDependency.SmithyTypes(runtimeConfig), namespace = "${runtimeConfig.cratePrefix}_types::retry") + fun provideErrorKind(runtimeConfig: RuntimeConfig) = RuntimeType("ProvideErrorKind", dependency = CargoDependency.SmithyTypes(runtimeConfig), namespace = "${runtimeConfig.cratePrefix}_types::retry") + // val Blob = RuntimeType("Blob", RustDependency.IO_CORE, "blob") val From = RuntimeType("From", dependency = null, namespace = "std::convert") val AsRef = RuntimeType("AsRef", dependency = null, namespace = "std::convert") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGenerator.kt index caf4677844e2aa7af024ce9cc80be26c5a8cbf87..7df891bc919abaa30d32215af43c485b87adfd34 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGenerator.kt @@ -10,6 +10,8 @@ import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.RetryableTrait import software.amazon.smithy.rust.codegen.rustlang.Attribute import software.amazon.smithy.rust.codegen.rustlang.Derives import software.amazon.smithy.rust.codegen.rustlang.RustMetadata @@ -78,6 +80,32 @@ class CombinedErrorGenerator( } } + val errorKindT = RuntimeType.errorKind(symbolProvider.config().runtimeConfig) + writer.rustBlock( + "impl #T for ${symbol.name}", + RuntimeType.provideErrorKind(symbolProvider.config().runtimeConfig) + ) { + rustBlock("fn code(&self) -> Option<&str>") { + rust("${symbol.name}::code(self)") + } + + rustBlock("fn retryable_error_kind(&self) -> Option<#T>", errorKindT) { + delegateToVariants { + when (it) { + is VariantMatch.Modeled -> writable { + if (it.shape.hasTrait(RetryableTrait::class.java)) { + rust("Some(_inner.retryable_error_kind())") + } else { + rust("None") + } + } + is VariantMatch.Generic -> writable { rust("_inner.retryable_error_kind()") } + is VariantMatch.Unhandled -> writable { rust("None") } + } + } + } + } + writer.rustBlock("impl ${symbol.name}") { writer.rustBlock("pub fn unhandled>>(err: E) -> Self", RuntimeType.StdError) { write("${symbol.name}::Unhandled(err.into())") @@ -122,7 +150,7 @@ class CombinedErrorGenerator( sealed class VariantMatch(name: String) : Section(name) { object Unhandled : VariantMatch("Unhandled") object Generic : VariantMatch("Generic") - data class Modeled(val symbol: Symbol) : VariantMatch("Modeled") + data class Modeled(val symbol: Symbol, val shape: Shape) : VariantMatch("Modeled") } /** @@ -154,7 +182,7 @@ class CombinedErrorGenerator( errors.forEach { val errorSymbol = symbolProvider.toSymbol(it) rust("""${symbol.name}::${errorSymbol.name}(_inner) => """) - handler(VariantMatch.Modeled(errorSymbol))(this) + handler(VariantMatch.Modeled(errorSymbol, it))(this) write(",") } val genericHandler = handler(VariantMatch.Generic) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ErrorGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ErrorGenerator.kt index 3d6943a4bc00660c723e35a30c7202933aabc3bb..62989ba8f68420b7c4dc7c37a31638823045dc1d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ErrorGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/ErrorGenerator.kt @@ -5,21 +5,56 @@ package software.amazon.smithy.rust.codegen.smithy.generators -import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.model.traits.RetryableTrait import software.amazon.smithy.rust.codegen.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.rustlang.Writable import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.rustlang.writable +import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RuntimeType.Companion.StdError import software.amazon.smithy.rust.codegen.smithy.RuntimeType.Companion.StdFmt +import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.util.dq +import software.amazon.smithy.rust.codegen.util.orNull + +sealed class ErrorKind { + abstract fun writable(runtimeConfig: RuntimeConfig): Writable + object Throttling : ErrorKind() { + override fun writable(runtimeConfig: RuntimeConfig) = writable { rust("#T::ThrottlingError", RuntimeType.errorKind(runtimeConfig)) } + } + object Client : ErrorKind() { + override fun writable(runtimeConfig: RuntimeConfig) = writable { rust("#T::ClientError", RuntimeType.errorKind(runtimeConfig)) } + } + + object Server : ErrorKind() { + override fun writable(runtimeConfig: RuntimeConfig) = writable { rust("#T::ServerError", RuntimeType.errorKind(runtimeConfig)) } + } +} + +/** + * Returns the modeled retryKind for this shape + * + * This is _only_ non-null in cases where the @retryable trait has been applied. + */ +fun StructureShape.modeledRetryKind(errorTrait: ErrorTrait): ErrorKind? { + val retryableTrait = this.getTrait(RetryableTrait::class.java).orNull() ?: return null + return when { + retryableTrait.throttling -> ErrorKind.Throttling + errorTrait.isClientError -> ErrorKind.Client + errorTrait.isServerError -> ErrorKind.Server + // The error _must_ be either a client or server error + else -> TODO() + } +} class ErrorGenerator( val model: Model, - private val symbolProvider: SymbolProvider, + private val symbolProvider: RustSymbolProvider, private val writer: RustWriter, private val shape: StructureShape, private val error: ErrorTrait @@ -30,21 +65,18 @@ class ErrorGenerator( private fun renderError() { val symbol = symbolProvider.toSymbol(shape) - val retryableTrait = shape.getTrait(RetryableTrait::class.java) - val throttling = retryableTrait.map { it.throttling }.orElse(false) - val retryable = retryableTrait.isPresent - val errorCause = when { - error.isClientError -> "ErrorCause::Client" - error.isServerError -> "ErrorCause::Server" - else -> "ErrorCause::Unknown(${error.value.dq()})" - } val messageShape = shape.getMember("message") val message = messageShape.map { "self.message.as_deref()" }.orElse("None") + val errorKindT = RuntimeType.errorKind(symbolProvider.config().runtimeConfig) writer.rustBlock("impl ${symbol.name}") { + val retryKindWriteable = shape.modeledRetryKind(error)?.writable(symbolProvider.config().runtimeConfig) + if (retryKindWriteable != null) { + rustBlock("pub fn retryable_error_kind(&self) -> #T", errorKindT) { + retryKindWriteable(this) + } + } rust( """ - pub fn retryable(&self) -> bool { $retryable } - pub fn throttling(&self) -> bool { $throttling } pub fn code(&self) -> &str { ${shape.id.name.dq()} } pub fn message(&self) -> Option<&str> { $message } """ diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt index a363ab33b05203297542bc4241b2646cf1003c6e..b4824baab7ff12fb60a7dc13ba29369501caa36d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/StructureGenerator.kt @@ -15,6 +15,7 @@ import software.amazon.smithy.rust.codegen.rustlang.RustType import software.amazon.smithy.rust.codegen.rustlang.RustWriter import software.amazon.smithy.rust.codegen.rustlang.documentShape import software.amazon.smithy.rust.codegen.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.canUseDefault import software.amazon.smithy.rust.codegen.smithy.expectRustMetadata import software.amazon.smithy.rust.codegen.smithy.isOptional @@ -28,7 +29,7 @@ fun RustWriter.implBlock(structureShape: Shape, symbolProvider: SymbolProvider, class StructureGenerator( val model: Model, - private val symbolProvider: SymbolProvider, + private val symbolProvider: RustSymbolProvider, private val writer: RustWriter, private val shape: StructureShape ) { diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt index bc0158f8872dcd4552922f65b572389208497fc6..13b5e20c8b696af01d07c4d0e508d6adf81ae6ee 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt @@ -7,7 +7,6 @@ package software.amazon.smithy.rust.codegen.generators import io.kotest.matchers.string.shouldContainInOrder import org.junit.jupiter.api.Test -import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.rust.codegen.rustlang.Custom @@ -44,6 +43,7 @@ class StructureGeneratorTest { } @error("server") + @retryable structure MyError { message: String } @@ -75,7 +75,7 @@ class StructureGeneratorTest { @Test fun `generate structures with public fields`() { - val provider: SymbolProvider = testSymbolProvider(model) + val provider = testSymbolProvider(model) val writer = RustWriter.root() writer.withModule("model") { val innerGenerator = StructureGenerator(model, provider, this, inner) @@ -103,11 +103,16 @@ class StructureGeneratorTest { @Test fun `generate error structures`() { - val provider: SymbolProvider = testSymbolProvider(model) + val provider = testSymbolProvider(model) val writer = RustWriter.forModule("error") val generator = StructureGenerator(model, provider, writer, error) generator.render() - writer.compileAndTest() + writer.compileAndTest( + """ + let err = MyError { message: None }; + assert_eq!(err.retryable_error_kind(), smithy_types::retry::ErrorKind::ServerError); + """ + ) } @Test @@ -127,7 +132,7 @@ class StructureGeneratorTest { nested2: Inner }""".asSmithyModel() - val provider: SymbolProvider = testSymbolProvider(model) + val provider = testSymbolProvider(model) val writer = RustWriter.root() writer.docs("module docs") writer diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGeneratorTest.kt index 456c713ae3a0479fc72a8fb4c4073e8b6cf3bcb1..83fbd05d743301d568043fe8070bf2ff3da4a471 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/CombinedErrorGeneratorTest.kt @@ -24,12 +24,12 @@ internal class CombinedErrorGeneratorTest { } @error("client") + @retryable structure InvalidGreeting { message: String, } @error("server") - @tags(["client-only"]) structure FooError {} @error("server") @@ -43,14 +43,18 @@ internal class CombinedErrorGeneratorTest { listOf("FooError", "ComplexError", "InvalidGreeting").forEach { model.lookup("error#$it").renderWithModelBuilder(model, symbolProvider, writer) } + val generator = CombinedErrorGenerator(model, testSymbolProvider(model), model.lookup("error#Greeting")) generator.render(writer) + writer.compileAndTest( """ let error = GreetingError::InvalidGreeting(InvalidGreeting::builder().message("an error").build()); assert_eq!(format!("{}", error), "InvalidGreeting: an error"); assert_eq!(error.message(), Some("an error")); assert_eq!(error.code(), Some("InvalidGreeting")); + use smithy_types::retry::ProvideErrorKind; + assert_eq!(error.retryable_error_kind(), Some(smithy_types::retry::ErrorKind::ClientError)); // unhandled variants properly delegate message diff --git a/rust-runtime/smithy-http/src/lib.rs b/rust-runtime/smithy-http/src/lib.rs index 132ecde2196d9bd0f27acca6405744cf03825508..01e3d5142c4203e3763c75af851ad37048cc0256 100644 --- a/rust-runtime/smithy-http/src/lib.rs +++ b/rust-runtime/smithy-http/src/lib.rs @@ -9,6 +9,7 @@ pub mod endpoint; pub mod label; pub mod middleware; pub mod operation; +pub mod retry; mod pin_util; pub mod property_bag; pub mod query; diff --git a/rust-runtime/smithy-http/src/retry.rs b/rust-runtime/smithy-http/src/retry.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1e50d5e81e87b026bed994ba2fd6c4ec6273276 --- /dev/null +++ b/rust-runtime/smithy-http/src/retry.rs @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! HTTP specific retry behaviors +//! +//! For protocol agnostic retries, see `smithy_types::Retry`. + +use smithy_types::retry::{ProvideErrorKind, RetryKind}; + +pub trait ClassifyResponse { + fn classify(&self, e: E, response: &http::Response) -> RetryKind + where + E: ProvideErrorKind; +} diff --git a/rust-runtime/smithy-types/src/lib.rs b/rust-runtime/smithy-types/src/lib.rs index 970ee2c4f6c69cb1565e477156c9d5fdf0ec92be..a5092216db51255407fea81950f1bd7d4b549fb5 100644 --- a/rust-runtime/smithy-types/src/lib.rs +++ b/rust-runtime/smithy-types/src/lib.rs @@ -4,10 +4,12 @@ */ pub mod instant; +pub mod retry; use std::collections::HashMap; pub use crate::instant::Instant; +use crate::retry::{ErrorKind, ProvideErrorKind}; use std::fmt; use std::fmt::{Display, Formatter}; @@ -72,17 +74,27 @@ impl Error { } } +impl ProvideErrorKind for Error { + fn retryable_error_kind(&self) -> Option { + None + } + + fn code(&self) -> Option<&str> { + Error::code(self) + } +} + impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Smithy Error")?; + let mut fmt = f.debug_struct("Error"); if let Some(code) = &self.code { - write!(f, " code={}", code)?; + fmt.field("code", code); } if let Some(message) = &self.message { - write!(f, " message={}", message)?; + fmt.field("message", message); } if let Some(req_id) = &self.request_id { - write!(f, " request_id={}", req_id)?; + fmt.field("request_id", req_id); } Ok(()) } diff --git a/rust-runtime/smithy-types/src/retry.rs b/rust-runtime/smithy-types/src/retry.rs new file mode 100644 index 0000000000000000000000000000000000000000..be6a5cf74fa4f1b2bcb0991268f63805d8e00c4d --- /dev/null +++ b/rust-runtime/smithy-types/src/retry.rs @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! This module defines types that describe when to retry given a response. + +use std::time::Duration; + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +#[non_exhaustive] +pub enum ErrorKind { + /// A connection-level error. + /// + /// A `TransientError` can represent conditions such as socket timeouts, socket connection errors, or TLS negotiation timeouts. + /// + /// `TransientError` is not modeled by Smithy and is instead determined through client-specific heuristics and response status codes. + /// + /// Typically these should never be applied for non-idempotent request types + /// since in this scenario, it's impossible to know whether the operation had + /// a side effect on the server. + /// + /// TransientErrors are not currently modeled. They are determined based on specific provider + /// level errors & response status code. + TransientError, + + /// An error where the server explicitly told the client to back off, such as a 429 or 503 HTTP error. + ThrottlingError, + + /// Server error that isn't explicitly throttling but is considered by the client + /// to be something that should be retried. + ServerError, + + /// Doesn't count against any budgets. This could be something like a 401 challenge in Http. + ClientError, +} + +pub trait ProvideErrorKind { + /// Returns the `ErrorKind` when the error is modeled as retryable + /// + /// If the error kind cannot be determined (eg. the error is unmodeled at the error kind depends + /// on an HTTP status code, return `None`. + fn retryable_error_kind(&self) -> Option; + + /// Returns the `code` for this error if one exists + fn code(&self) -> Option<&str>; +} + +/// `RetryKind` describes how a request MAY be retried for a given response +/// +/// A `RetryKind` describes how a response MAY be retried; it does not mandate retry behavior. +/// The actual retry behavior is at the sole discretion of the RetryStrategy in place. +/// A RetryStrategy may ignore the suggestion for a number of reasons including but not limited to: +/// - Number of retry attempts exceeded +/// - The required retry delay exceeds the maximum backoff configured by the client +/// - No retry tokens are available due to service health +#[non_exhaustive] +#[derive(Eq, PartialEq, Debug)] +pub enum RetryKind { + /// Retry the associated request due to a known `ErrorKind`. + Error(ErrorKind), + + /// An Explicit retry (eg. from `x-amz-retry-after`). + /// + /// Note: The specified `Duration` is considered a suggestion and may be replaced or ignored. + Explicit(Duration), + + /// The response associated with this variant should not be retried. + NotRetryable, +}