Skip to content

Commit 591369a

Browse files
Zelda Hessleraws-sdk-rust-ci
authored andcommitted
[smithy-rs] 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 7febeb0 commit 591369a

File tree

18 files changed

+571
-64
lines changed

18 files changed

+571
-64
lines changed

sdk/aws-http/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ version = "0.55.2"
4141
async-trait = "0.1.50"
4242
bytes-utils = "0.1.2"
4343
env_logger = "0.9"
44-
http = "0.2.3"
4544
proptest = "1"
4645
serde_json = "1"
4746

sdk/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",

sdk/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;

sdk/aws-runtime/src/retries.rs

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+
}

sdk/aws-smithy-http/src/result.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55

66
//! `Result` wrapper types for [success](SdkSuccess) and [failure](SdkError) responses.
77
8-
use crate::connection::ConnectionMetadata;
9-
use crate::operation;
10-
use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA};
11-
use aws_smithy_types::error::ErrorMetadata;
12-
use aws_smithy_types::retry::ErrorKind;
138
use std::error::Error;
149
use std::fmt;
1510
use std::fmt::{Debug, Display, Formatter};
1611

12+
use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA};
13+
use aws_smithy_types::error::ErrorMetadata;
14+
use aws_smithy_types::retry::ErrorKind;
15+
16+
use crate::connection::ConnectionMetadata;
17+
use crate::operation;
18+
1719
type BoxError = Box<dyn Error + Send + Sync>;
1820

1921
/// Successful SDK Result
@@ -434,6 +436,15 @@ impl<E, R> SdkError<E, R> {
434436
}
435437
}
436438

439+
/// Return a reference to this error's raw response, if it contains one. Otherwise, return `None`.
440+
pub fn raw_response(&self) -> Option<&R> {
441+
match self {
442+
Self::ServiceError(inner) => Some(inner.raw()),
443+
Self::ResponseError(inner) => Some(inner.raw()),
444+
_ => None,
445+
}
446+
}
447+
437448
/// Maps the service error type in `SdkError::ServiceError`
438449
#[doc(hidden)]
439450
pub fn map_service_error<E2>(self, map: impl FnOnce(E) -> E2) -> SdkError<E2, R> {

sdk/aws-smithy-runtime-api/src/client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod orchestrator;
1818
/// This code defines when and how failed requests should be retried. It also defines the behavior
1919
/// used to limit the rate that requests are sent.
2020
pub mod retries;
21+
2122
/// Runtime plugin type definitions.
2223
pub mod runtime_plugin;
2324

sdk/aws-smithy-runtime-api/src/client/orchestrator.rs

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use super::identity::{IdentityResolver, IdentityResolvers};
77
use crate::client::identity::Identity;
88
use crate::client::interceptors::context::{Input, OutputOrError};
9-
use crate::client::interceptors::InterceptorContext;
9+
use crate::client::retries::RetryClassifiers;
10+
use crate::client::retries::RetryStrategy;
1011
use crate::config_bag::ConfigBag;
1112
use crate::type_erasure::{TypeErasedBox, TypedBox};
1213
use aws_smithy_async::future::now_or_later::NowOrLater;
@@ -54,16 +55,6 @@ impl Connection for Box<dyn Connection> {
5455
}
5556
}
5657

57-
pub trait RetryStrategy: Send + Sync + Debug {
58-
fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result<(), BoxError>;
59-
60-
fn should_attempt_retry(
61-
&self,
62-
context: &InterceptorContext<HttpRequest, HttpResponse>,
63-
cfg: &ConfigBag,
64-
) -> Result<bool, BoxError>;
65-
}
66-
6758
#[derive(Debug)]
6859
pub struct AuthOptionResolverParams(TypeErasedBox);
6960

@@ -241,6 +232,9 @@ pub trait ConfigBagAccessors {
241232
response_serializer: impl ResponseDeserializer + 'static,
242233
);
243234

235+
fn retry_classifiers(&self) -> &RetryClassifiers;
236+
fn set_retry_classifiers(&mut self, retry_classifier: RetryClassifiers);
237+
244238
fn retry_strategy(&self) -> &dyn RetryStrategy;
245239
fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static);
246240

@@ -277,25 +271,6 @@ impl ConfigBagAccessors for ConfigBag {
277271
self.put::<Box<dyn AuthOptionResolver>>(Box::new(auth_option_resolver));
278272
}
279273

280-
fn http_auth_schemes(&self) -> &HttpAuthSchemes {
281-
self.get::<HttpAuthSchemes>()
282-
.expect("auth schemes must be set")
283-
}
284-
285-
fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) {
286-
self.put::<HttpAuthSchemes>(http_auth_schemes);
287-
}
288-
289-
fn retry_strategy(&self) -> &dyn RetryStrategy {
290-
&**self
291-
.get::<Box<dyn RetryStrategy>>()
292-
.expect("a retry strategy must be set")
293-
}
294-
295-
fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) {
296-
self.put::<Box<dyn RetryStrategy>>(Box::new(retry_strategy));
297-
}
298-
299274
fn endpoint_resolver_params(&self) -> &EndpointResolverParams {
300275
self.get::<EndpointResolverParams>()
301276
.expect("endpoint resolver params must be set")
@@ -334,6 +309,15 @@ impl ConfigBagAccessors for ConfigBag {
334309
self.put::<Box<dyn Connection>>(Box::new(connection));
335310
}
336311

312+
fn http_auth_schemes(&self) -> &HttpAuthSchemes {
313+
self.get::<HttpAuthSchemes>()
314+
.expect("auth schemes must be set")
315+
}
316+
317+
fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) {
318+
self.put::<HttpAuthSchemes>(http_auth_schemes);
319+
}
320+
337321
fn request_serializer(&self) -> &dyn RequestSerializer {
338322
&**self
339323
.get::<Box<dyn RequestSerializer>>()
@@ -357,6 +341,25 @@ impl ConfigBagAccessors for ConfigBag {
357341
self.put::<Box<dyn ResponseDeserializer>>(Box::new(response_deserializer));
358342
}
359343

344+
fn retry_classifiers(&self) -> &RetryClassifiers {
345+
self.get::<RetryClassifiers>()
346+
.expect("retry classifiers must be set")
347+
}
348+
349+
fn set_retry_classifiers(&mut self, retry_classifiers: RetryClassifiers) {
350+
self.put::<RetryClassifiers>(retry_classifiers);
351+
}
352+
353+
fn retry_strategy(&self) -> &dyn RetryStrategy {
354+
&**self
355+
.get::<Box<dyn RetryStrategy>>()
356+
.expect("a retry strategy must be set")
357+
}
358+
359+
fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) {
360+
self.put::<Box<dyn RetryStrategy>>(Box::new(retry_strategy));
361+
}
362+
360363
fn trace_probe(&self) -> &dyn TraceProbe {
361364
&**self
362365
.get::<Box<dyn TraceProbe>>()

0 commit comments

Comments
 (0)