Skip to content

Commit ae995fb

Browse files
author
Zelda Hessler
authored
feature: orchestrator retry classifiers (#2621)
## Motivation and Context <!--- Why is this change required? What problem does it solve? --> <!--- If it fixes an open issue, please link to the issue here --> To retry a response, we must first classify it as retryable. ## Description <!--- Describe your changes in detail --> feature: add AWS error code classifier feature: add x-amz-retry-after header classifier feature: add smithy modeled retry classifier feature: add error type classifier feature: add HTTP status code classifier add: tests for classifiers remove: redundant `http` dep from `aws-http` move: `NeverRetryStrategy` to smithy-runtime crate add: RuntimePluginImpls codegen section for operation-specific runtime plugin definitions update: orchestrator retries to work with `ShouldAttempt` add: retry classifier config bag accessor add: raw response getter to SdkError update: RetryStrategy trait signatures to use `ShouldAttempt` add: `RetryClassifiers` struct for holding and calling retry classifiers update: `RetryClassifierDecorator` to define orchestrator classifiers add: `default_retry_classifiers` fn to codegen update: `ServiceGenerator` to add feature flag for aws-smithy-runtime/test-util update: SRA integration test to insert retry classifier plugin ## Testing <!--- Please describe in detail how you tested your changes --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> this change includes tests ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 21249b0 commit ae995fb

File tree

24 files changed

+774
-69
lines changed

24 files changed

+774
-69
lines changed

aws/rust-runtime/aws-http/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" }
2828
aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" }
2929
bytes-utils = "0.1.2"
3030
env_logger = "0.9"
31-
http = "0.2.3"
3231
tokio = { version = "1.23.1", features = ["macros", "rt", "rt-multi-thread", "test-util", "time"] }
3332
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
3433
proptest = "1"

aws/rust-runtime/aws-runtime/external-types.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
allowed_external_types = [
22
"aws_credential_types::*",
33
"aws_sigv4::*",
4-
"aws_smithy_http::body::SdkBody",
4+
"aws_smithy_http::*",
5+
"aws_smithy_types::*",
56
"aws_smithy_runtime_api::*",
67
"aws_types::*",
78
"http::request::Request",

aws/rust-runtime/aws-runtime/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@ pub mod recursion_detection;
2525
/// Supporting code for user agent headers in the AWS SDK.
2626
pub mod user_agent;
2727

28+
/// Supporting code for retry behavior specific to the AWS SDK.
29+
pub mod retries;
30+
2831
/// Supporting code for invocation ID headers in the AWS SDK.
2932
pub mod invocation_id;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/// Classifiers that can inspect a response and determine if it should be retried.
7+
pub mod classifier;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
use aws_smithy_http::http::HttpHeaders;
7+
use aws_smithy_http::result::SdkError;
8+
use aws_smithy_runtime_api::client::retries::RetryReason;
9+
use aws_smithy_types::error::metadata::ProvideErrorMetadata;
10+
use aws_smithy_types::retry::ErrorKind;
11+
12+
/// AWS error codes that represent throttling errors.
13+
pub const THROTTLING_ERRORS: &[&str] = &[
14+
"Throttling",
15+
"ThrottlingException",
16+
"ThrottledException",
17+
"RequestThrottledException",
18+
"TooManyRequestsException",
19+
"ProvisionedThroughputExceededException",
20+
"TransactionInProgressException",
21+
"RequestLimitExceeded",
22+
"BandwidthLimitExceeded",
23+
"LimitExceededException",
24+
"RequestThrottled",
25+
"SlowDown",
26+
"PriorRequestNotComplete",
27+
"EC2ThrottledException",
28+
];
29+
30+
/// AWS error codes that represent transient errors.
31+
pub const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"];
32+
33+
/// A retry classifier for determining if the response sent by an AWS service requires a retry.
34+
#[derive(Debug)]
35+
pub struct AwsErrorCodeClassifier;
36+
37+
impl AwsErrorCodeClassifier {
38+
/// Classify an error code to check if represents a retryable error. The codes of retryable
39+
/// errors are defined [here](THROTTLING_ERRORS) and [here](TRANSIENT_ERRORS).
40+
pub fn classify_error<E: ProvideErrorMetadata, R>(
41+
&self,
42+
error: &SdkError<E, R>,
43+
) -> Option<RetryReason> {
44+
if let Some(error_code) = error.code() {
45+
if THROTTLING_ERRORS.contains(&error_code) {
46+
return Some(RetryReason::Error(ErrorKind::ThrottlingError));
47+
} else if TRANSIENT_ERRORS.contains(&error_code) {
48+
return Some(RetryReason::Error(ErrorKind::TransientError));
49+
}
50+
};
51+
52+
None
53+
}
54+
}
55+
56+
/// A retry classifier that checks for `x-amz-retry-after` headers. If one is found, a
57+
/// [`RetryReason::Explicit`] is returned containing the duration to wait before retrying.
58+
#[derive(Debug)]
59+
pub struct AmzRetryAfterHeaderClassifier;
60+
61+
impl AmzRetryAfterHeaderClassifier {
62+
/// Classify an AWS responses error code to determine how (and if) it should be retried.
63+
pub fn classify_error<E>(&self, error: &SdkError<E>) -> Option<RetryReason> {
64+
error
65+
.raw_response()
66+
.and_then(|res| res.http_headers().get("x-amz-retry-after"))
67+
.and_then(|header| header.to_str().ok())
68+
.and_then(|header| header.parse::<u64>().ok())
69+
.map(|retry_after_delay| {
70+
RetryReason::Explicit(std::time::Duration::from_millis(retry_after_delay))
71+
})
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod test {
77+
use super::{AmzRetryAfterHeaderClassifier, AwsErrorCodeClassifier};
78+
use aws_smithy_http::body::SdkBody;
79+
use aws_smithy_http::operation;
80+
use aws_smithy_http::result::SdkError;
81+
use aws_smithy_runtime_api::client::retries::RetryReason;
82+
use aws_smithy_types::error::metadata::ProvideErrorMetadata;
83+
use aws_smithy_types::error::ErrorMetadata;
84+
use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind};
85+
use std::fmt;
86+
use std::time::Duration;
87+
88+
#[derive(Debug)]
89+
struct UnmodeledError;
90+
91+
impl fmt::Display for UnmodeledError {
92+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93+
write!(f, "UnmodeledError")
94+
}
95+
}
96+
97+
impl std::error::Error for UnmodeledError {}
98+
99+
struct CodedError {
100+
metadata: ErrorMetadata,
101+
}
102+
103+
impl CodedError {
104+
fn new(code: &'static str) -> Self {
105+
Self {
106+
metadata: ErrorMetadata::builder().code(code).build(),
107+
}
108+
}
109+
}
110+
111+
impl ProvideErrorKind for UnmodeledError {
112+
fn retryable_error_kind(&self) -> Option<ErrorKind> {
113+
None
114+
}
115+
116+
fn code(&self) -> Option<&str> {
117+
None
118+
}
119+
}
120+
121+
impl ProvideErrorMetadata for CodedError {
122+
fn meta(&self) -> &ErrorMetadata {
123+
&self.metadata
124+
}
125+
}
126+
127+
#[test]
128+
fn classify_by_error_code() {
129+
let policy = AwsErrorCodeClassifier;
130+
let res = http::Response::new("OK");
131+
let err = SdkError::service_error(CodedError::new("Throttling"), res);
132+
133+
assert_eq!(
134+
policy.classify_error(&err),
135+
Some(RetryReason::Error(ErrorKind::ThrottlingError))
136+
);
137+
138+
let res = http::Response::new("OK");
139+
let err = SdkError::service_error(CodedError::new("RequestTimeout"), res);
140+
assert_eq!(
141+
policy.classify_error(&err),
142+
Some(RetryReason::Error(ErrorKind::TransientError))
143+
)
144+
}
145+
146+
#[test]
147+
fn classify_generic() {
148+
let policy = AwsErrorCodeClassifier;
149+
let res = http::Response::new("OK");
150+
let err = aws_smithy_types::Error::builder().code("SlowDown").build();
151+
let err = SdkError::service_error(err, res);
152+
assert_eq!(
153+
policy.classify_error(&err),
154+
Some(RetryReason::Error(ErrorKind::ThrottlingError))
155+
);
156+
}
157+
158+
#[test]
159+
fn test_retry_after_header() {
160+
let policy = AmzRetryAfterHeaderClassifier;
161+
let res = http::Response::builder()
162+
.header("x-amz-retry-after", "5000")
163+
.body("retry later")
164+
.unwrap()
165+
.map(SdkBody::from);
166+
let res = operation::Response::new(res);
167+
let err = SdkError::service_error(UnmodeledError, res);
168+
169+
assert_eq!(
170+
policy.classify_error(&err),
171+
Some(RetryReason::Explicit(Duration::from_millis(5000))),
172+
);
173+
}
174+
}

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ package software.amazon.smithy.rustsdk
88
import software.amazon.smithy.model.shapes.OperationShape
99
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
1010
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
11+
import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginCustomization
12+
import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginSection
13+
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
14+
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.derive
1115
import software.amazon.smithy.rust.codegen.core.rustlang.rust
16+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
1217
import software.amazon.smithy.rust.codegen.core.rustlang.writable
1318
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
1419
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
@@ -23,13 +28,21 @@ class RetryClassifierDecorator : ClientCodegenDecorator {
2328
codegenContext: ClientCodegenContext,
2429
operation: OperationShape,
2530
baseCustomizations: List<OperationCustomization>,
26-
): List<OperationCustomization> {
27-
return baseCustomizations + RetryClassifierFeature(codegenContext.runtimeConfig)
28-
}
31+
): List<OperationCustomization> =
32+
baseCustomizations + RetryClassifierFeature(codegenContext.runtimeConfig)
33+
34+
override fun operationRuntimePluginCustomizations(
35+
codegenContext: ClientCodegenContext,
36+
operation: OperationShape,
37+
baseCustomizations: List<OperationRuntimePluginCustomization>,
38+
): List<OperationRuntimePluginCustomization> =
39+
baseCustomizations + OperationRetryClassifiersFeature(codegenContext, operation)
2940
}
3041

3142
class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : OperationCustomization() {
32-
override fun retryType(): RuntimeType = AwsRuntimeType.awsHttp(runtimeConfig).resolve("retry::AwsResponseRetryClassifier")
43+
override fun retryType(): RuntimeType =
44+
AwsRuntimeType.awsHttp(runtimeConfig).resolve("retry::AwsResponseRetryClassifier")
45+
3346
override fun section(section: OperationSection) = when (section) {
3447
is OperationSection.FinalizeOperation -> writable {
3548
rust(
@@ -41,3 +54,140 @@ class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : Operati
4154
else -> emptySection
4255
}
4356
}
57+
58+
class OperationRetryClassifiersFeature(
59+
codegenContext: ClientCodegenContext,
60+
operation: OperationShape,
61+
) : OperationRuntimePluginCustomization() {
62+
private val runtimeConfig = codegenContext.runtimeConfig
63+
private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig)
64+
private val smithyRuntime = RuntimeType.smithyRuntime(runtimeConfig)
65+
private val smithyRuntimeApi = RuntimeType.smithyRuntimeApi(runtimeConfig)
66+
private val codegenScope = arrayOf(
67+
"HttpStatusCodeClassifier" to smithyRuntime.resolve("client::retries::classifier::HttpStatusCodeClassifier"),
68+
"AwsErrorCodeClassifier" to awsRuntime.resolve("retries::classifier::AwsErrorCodeClassifier"),
69+
"ModeledAsRetryableClassifier" to smithyRuntime.resolve("client::retries::classifier::ModeledAsRetryableClassifier"),
70+
"AmzRetryAfterHeaderClassifier" to awsRuntime.resolve("retries::classifier::AmzRetryAfterHeaderClassifier"),
71+
"SmithyErrorClassifier" to smithyRuntime.resolve("client::retries::classifier::SmithyErrorClassifier"),
72+
"RetryReason" to smithyRuntimeApi.resolve("client::retries::RetryReason"),
73+
"ClassifyRetry" to smithyRuntimeApi.resolve("client::retries::ClassifyRetry"),
74+
"RetryClassifiers" to smithyRuntimeApi.resolve("client::retries::RetryClassifiers"),
75+
"OperationError" to codegenContext.symbolProvider.symbolForOperationError(operation),
76+
"SdkError" to RuntimeType.smithyHttp(runtimeConfig).resolve("result::SdkError"),
77+
"ErasedError" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("type_erasure::TypeErasedBox"),
78+
)
79+
80+
override fun section(section: OperationRuntimePluginSection) = when (section) {
81+
is OperationRuntimePluginSection.RuntimePluginSupportingTypes -> writable {
82+
Attribute(derive(RuntimeType.Debug)).render(this)
83+
rustTemplate(
84+
"""
85+
struct HttpStatusCodeClassifier(#{HttpStatusCodeClassifier});
86+
impl HttpStatusCodeClassifier {
87+
fn new() -> Self {
88+
Self(#{HttpStatusCodeClassifier}::default())
89+
}
90+
}
91+
impl #{ClassifyRetry} for HttpStatusCodeClassifier {
92+
fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> {
93+
let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known");
94+
self.0.classify_error(error)
95+
}
96+
}
97+
""",
98+
*codegenScope,
99+
)
100+
101+
Attribute(derive(RuntimeType.Debug)).render(this)
102+
rustTemplate(
103+
"""
104+
struct AwsErrorCodeClassifier(#{AwsErrorCodeClassifier});
105+
impl AwsErrorCodeClassifier {
106+
fn new() -> Self {
107+
Self(#{AwsErrorCodeClassifier})
108+
}
109+
}
110+
impl #{ClassifyRetry} for AwsErrorCodeClassifier {
111+
fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> {
112+
let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known");
113+
self.0.classify_error(error)
114+
}
115+
}
116+
""",
117+
*codegenScope,
118+
)
119+
120+
Attribute(derive(RuntimeType.Debug)).render(this)
121+
rustTemplate(
122+
"""
123+
struct ModeledAsRetryableClassifier(#{ModeledAsRetryableClassifier});
124+
impl ModeledAsRetryableClassifier {
125+
fn new() -> Self {
126+
Self(#{ModeledAsRetryableClassifier})
127+
}
128+
}
129+
impl #{ClassifyRetry} for ModeledAsRetryableClassifier {
130+
fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> {
131+
let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known");
132+
self.0.classify_error(error)
133+
}
134+
}
135+
""",
136+
*codegenScope,
137+
)
138+
139+
Attribute(derive(RuntimeType.Debug)).render(this)
140+
rustTemplate(
141+
"""
142+
struct AmzRetryAfterHeaderClassifier(#{AmzRetryAfterHeaderClassifier});
143+
impl AmzRetryAfterHeaderClassifier {
144+
fn new() -> Self {
145+
Self(#{AmzRetryAfterHeaderClassifier})
146+
}
147+
}
148+
impl #{ClassifyRetry} for AmzRetryAfterHeaderClassifier {
149+
fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> {
150+
let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known");
151+
self.0.classify_error(error)
152+
}
153+
}
154+
""",
155+
*codegenScope,
156+
)
157+
158+
Attribute(derive(RuntimeType.Debug)).render(this)
159+
rustTemplate(
160+
"""
161+
struct SmithyErrorClassifier(#{SmithyErrorClassifier});
162+
impl SmithyErrorClassifier {
163+
fn new() -> Self {
164+
Self(#{SmithyErrorClassifier})
165+
}
166+
}
167+
impl #{ClassifyRetry} for SmithyErrorClassifier {
168+
fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> {
169+
let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known");
170+
self.0.classify_error(error)
171+
}
172+
}
173+
""",
174+
*codegenScope,
175+
)
176+
}
177+
178+
is OperationRuntimePluginSection.RetryClassifier -> writable {
179+
rustTemplate(
180+
"""
181+
.with_classifier(SmithyErrorClassifier::new())
182+
.with_classifier(AmzRetryAfterHeaderClassifier::new())
183+
.with_classifier(ModeledAsRetryableClassifier::new())
184+
.with_classifier(AwsErrorCodeClassifier::new())
185+
.with_classifier(HttpStatusCodeClassifier::new())
186+
""",
187+
*codegenScope,
188+
)
189+
}
190+
191+
else -> emptySection
192+
}
193+
}

0 commit comments

Comments
 (0)