Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix native Smithy client retry and retry response errors #1717

Merged
merged 14 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,21 @@ message = "Smithy IDL v2 mixins are now supported"
references = ["smithy-rs#1680"]
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "all"}
author = "ogudavid"

[[smithy-rs]]
message = "Generated clients now retry transient errors without replacing the retry policy."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message = "Generated clients now retry transient errors without replacing the retry policy."
message = "White-label smithy clients now have a default retry policy and will retry requests returning a transient HTTP error."

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

White label/adhoc AWS clients already had retry. This is specifically fixing for Smithy clients. I think this should be clear by it being a smithy-rs entry.

references = ["smithy-rs#1715", "smithy-rs#1717"]
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
author = "jdisanti"

[[smithy-rs]]
message = "`ClassifyResponse` is no longer implemented for the unit type."
references = ["smithy-rs#1715", "smithy-rs#1717"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti"

[[aws-sdk-rust]]
message = "The `SdkError::ResponseError`, typically caused by a connection terminating before the full response is received, is now treated as a transient failure and retried."
references = ["aws-sdk-rust#303", "smithy-rs#1717"]
meta = { "breaking" = false, "tada" = false, "bug" = true }
author = "jdisanti"
99 changes: 55 additions & 44 deletions aws/rust-runtime/aws-http/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,10 @@
//! AWS-specific retry logic

use aws_smithy_http::result::SdkError;
use aws_smithy_http::retry::ClassifyResponse;
use aws_smithy_http::retry::{ClassifyResponse, DefaultResponseClassifier};
use aws_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]
#[derive(Clone, Debug)]
pub struct AwsErrorRetryPolicy;

const TRANSIENT_ERROR_STATUS_CODES: &[u16] = &[500, 502, 503, 504];
const THROTTLING_ERRORS: &[&str] = &[
"Throttling",
Expand All @@ -39,41 +28,38 @@ const THROTTLING_ERRORS: &[&str] = &[
];
const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"];

impl AwsErrorRetryPolicy {
/// Create an `AwsErrorRetryPolicy` with the default set of known error & status codes
/// Implementation of [`ClassifyResponse`] that models AWS error codes.
///
/// 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]
#[derive(Clone, Debug)]
pub struct AwsResponseClassifier;

impl AwsResponseClassifier {
/// Create an `AwsResponseClassifier` with the default set of known error & status codes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be cool if we could link to code or docs showing what errors get retried

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a way to link to the private constants with rustdoc, then I could add this pretty easily. I'm reluctant to duplicate the list in the documentation since it will get out of date.

pub fn new() -> Self {
AwsErrorRetryPolicy
AwsResponseClassifier
jdisanti marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl Default for AwsErrorRetryPolicy {
impl Default for AwsResponseClassifier {
fn default() -> Self {
Self::new()
}
}

impl<T, E> ClassifyResponse<T, SdkError<E>> for AwsErrorRetryPolicy
impl<T, E> ClassifyResponse<T, SdkError<E>> for AwsResponseClassifier
where
E: ProvideErrorKind,
{
fn classify(&self, err: Result<&T, &SdkError<E>>) -> RetryKind {
let (err, response) = match err {
Ok(_) => return RetryKind::Unnecessary,
Err(SdkError::ServiceError { err, raw }) => (err, raw),
Err(SdkError::TimeoutError(_err)) => {
return RetryKind::Error(ErrorKind::TransientError)
}

Err(SdkError::DispatchFailure(err)) => {
return if err.is_timeout() || err.is_io() {
RetryKind::Error(ErrorKind::TransientError)
} else if let Some(ek) = err.is_other() {
RetryKind::Error(ek)
} else {
RetryKind::UnretryableFailure
};
}
Err(_) => return RetryKind::UnretryableFailure,
fn classify(&self, result: Result<&T, &SdkError<E>>) -> RetryKind {
let (err, response) = match DefaultResponseClassifier::try_extract_err_response(result) {
jdisanti marked this conversation as resolved.
Show resolved Hide resolved
Ok(extracted) => extracted,
Err(retry_kind) => return retry_kind,
};
if let Some(retry_after_delay) = response
.http()
Expand Down Expand Up @@ -105,15 +91,23 @@ where

#[cfg(test)]
mod test {
use crate::retry::AwsErrorRetryPolicy;
use crate::retry::AwsResponseClassifier;
use aws_smithy_http::body::SdkBody;
use aws_smithy_http::operation;
use aws_smithy_http::result::{SdkError, SdkSuccess};
use aws_smithy_http::retry::ClassifyResponse;
use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind};
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 {
code: &'static str,
Expand Down Expand Up @@ -151,7 +145,7 @@ mod test {

#[test]
fn not_an_error() {
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();
let test_response = http::Response::new("OK");
assert_eq!(
policy.classify(make_err(UnmodeledError, test_response).as_ref()),
Expand All @@ -161,7 +155,7 @@ mod test {

#[test]
fn classify_by_response_status() {
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();
let test_resp = http::Response::builder()
.status(500)
.body("error!")
Expand All @@ -174,7 +168,7 @@ mod test {

#[test]
fn classify_by_response_status_not_retryable() {
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();
let test_resp = http::Response::builder()
.status(408)
.body("error!")
Expand All @@ -188,7 +182,7 @@ mod test {
#[test]
fn classify_by_error_code() {
let test_response = http::Response::new("OK");
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();

assert_eq!(
policy.classify(make_err(CodedError { code: "Throttling" }, test_response).as_ref()),
Expand All @@ -214,7 +208,7 @@ mod test {
fn classify_generic() {
let err = aws_smithy_types::Error::builder().code("SlowDown").build();
let test_response = http::Response::new("OK");
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();
assert_eq!(
policy.classify(make_err(err, test_response).as_ref()),
RetryKind::Error(ErrorKind::ThrottlingError)
Expand All @@ -236,7 +230,7 @@ mod test {
}
}

let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();

assert_eq!(
policy.classify(make_err(ModeledRetries, test_response).as_ref()),
Expand All @@ -246,7 +240,7 @@ mod test {

#[test]
fn test_retry_after_header() {
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();
let test_response = http::Response::builder()
.header("x-amz-retry-after", "5000")
.body("retry later")
Expand All @@ -258,9 +252,26 @@ mod test {
);
}

#[test]
fn classify_response_error() {
let policy = AwsResponseClassifier::new();
assert_eq!(
policy.classify(
Result::<SdkSuccess<()>, SdkError<UnmodeledError>>::Err(SdkError::ResponseError {
err: Box::new(UnmodeledError),
raw: operation::Response::new(
http::Response::new("OK").map(|b| SdkBody::from(b))
),
})
.as_ref()
),
RetryKind::Error(ErrorKind::TransientError)
);
}

#[test]
fn test_timeout_error() {
let policy = AwsErrorRetryPolicy::new();
let policy = AwsResponseClassifier::new();
let err: Result<(), SdkError<UnmodeledError>> = Err(SdkError::TimeoutError("blah".into()));
assert_eq!(
policy.classify(err.as_ref()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use http::{self, Uri};

use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion};
use aws_endpoint::{EndpointShim, Params};
use aws_http::retry::AwsErrorRetryPolicy;
use aws_http::retry::AwsResponseClassifier;
use aws_http::user_agent::AwsUserAgent;
use aws_inlineable::middleware::DefaultMiddleware;
use aws_sig_auth::signer::OperationSigningConfig;
Expand Down Expand Up @@ -75,7 +75,7 @@ impl ParseHttpResponse for TestOperationParser {
}
}

fn test_operation() -> Operation<TestOperationParser, AwsErrorRetryPolicy> {
fn test_operation() -> Operation<TestOperationParser, AwsResponseClassifier> {
let req = operation::Request::new(
http::Request::builder()
.uri("https://test-service.test-region.amazonaws.com/")
Expand Down Expand Up @@ -110,7 +110,7 @@ fn test_operation() -> Operation<TestOperationParser, AwsErrorRetryPolicy> {
Result::<_, Infallible>::Ok(req)
})
.unwrap();
Operation::new(req, TestOperationParser).with_retry_policy(AwsErrorRetryPolicy::new())
Operation::new(req, TestOperationParser).with_retry_policy(AwsResponseClassifier::new())
}

#[cfg(any(feature = "native-tls", feature = "rustls"))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class AwsFluentClientDecorator : RustCodegenDecorator<ClientCodegenContext> {
AwsPresignedFluentBuilderMethod(runtimeConfig),
AwsFluentClientDocs(codegenContext),
),
retryPolicy = runtimeConfig.awsHttp().asType().member("retry::AwsErrorRetryPolicy").writable,
retryPolicy = runtimeConfig.awsHttp().asType().member("retry::AwsResponseClassifier").writable,
).render(rustCrate)
rustCrate.withModule(FluentClientGenerator.customizableOperationModule) { writer ->
renderCustomizableOperationSendMethod(runtimeConfig, generics, writer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RetryPolicyDecorator : RustCodegenDecorator<ClientCodegenContext> {
}

class RetryPolicyFeature(private val runtimeConfig: RuntimeConfig) : OperationCustomization() {
override fun retryType(): RuntimeType = runtimeConfig.awsHttp().asType().member("retry::AwsErrorRetryPolicy")
override fun retryType(): RuntimeType = runtimeConfig.awsHttp().asType().member("retry::AwsResponseClassifier")
override fun section(section: OperationSection) = when (section) {
is OperationSection.FinalizeOperation -> writable {
rust(
Expand Down
4 changes: 2 additions & 2 deletions aws/sdk/integration-tests/dynamodb/tests/movies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use aws_sdk_dynamodb as dynamodb;

use aws_http::retry::AwsErrorRetryPolicy;
use aws_http::retry::AwsResponseClassifier;
use aws_sdk_dynamodb::input::CreateTableInput;
use aws_smithy_client::test_connection::TestConnection;
use aws_smithy_http::body::SdkBody;
Expand Down Expand Up @@ -155,7 +155,7 @@ where
async fn wait_for_ready_table(
table_name: &str,
conf: &Config,
) -> Operation<DescribeTable, WaitForReadyTable<AwsErrorRetryPolicy>> {
) -> Operation<DescribeTable, WaitForReadyTable<AwsResponseClassifier>> {
let operation = DescribeTableInput::builder()
.table_name(table_name)
.build()
Expand Down
4 changes: 2 additions & 2 deletions aws/sdk/integration-tests/kms/tests/sensitive-it.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

use aws_http::retry::AwsErrorRetryPolicy;
use aws_http::retry::AwsResponseClassifier;
use aws_sdk_kms as kms;
use aws_smithy_http::body::SdkBody;
use aws_smithy_http::operation::{self, Parts};
Expand Down Expand Up @@ -65,7 +65,7 @@ fn types_are_debug() {
assert_debug::<kms::client::fluent_builders::CreateAlias>();
}

async fn create_alias_op() -> Parts<CreateAlias, AwsErrorRetryPolicy> {
async fn create_alias_op() -> Parts<CreateAlias, AwsResponseClassifier> {
let conf = kms::Config::builder().build();
let (_, parts) = CreateAlias::builder()
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ class FluentClientGenerator(
client = CargoDependency.SmithyClient(codegenContext.runtimeConfig).asType(),
),
private val customizations: List<FluentClientCustomization> = emptyList(),
private val retryPolicy: Writable = RustType.Unit.writable,
private val retryPolicy: Writable = CargoDependency.SmithyHttp(codegenContext.runtimeConfig).asType()
.member("retry::DefaultResponseClassifier").writable,
jdisanti marked this conversation as resolved.
Show resolved Hide resolved
) {
companion object {
fun clientOperationFnName(operationShape: OperationShape, symbolProvider: RustSymbolProvider): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import software.amazon.smithy.model.shapes.BlobShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.Attribute
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.RustType
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.docs
Expand Down Expand Up @@ -48,6 +47,8 @@ open class MakeOperationGenerator(
protected val runtimeConfig = coreCodegenContext.runtimeConfig
protected val symbolProvider = coreCodegenContext.symbolProvider
protected val httpBindingResolver = protocol.httpBindingResolver
private val defaultClassifier = CargoDependency.SmithyHttp(runtimeConfig)
.asType().member("retry::DefaultResponseClassifier")

private val sdkId =
coreCodegenContext.serviceShape.getTrait<ServiceTrait>()?.sdkId?.lowercase()?.replace(" ", "")
Expand Down Expand Up @@ -152,7 +153,7 @@ open class MakeOperationGenerator(
writer.format(symbolProvider.toSymbol(shape))

private fun buildOperationTypeRetry(writer: RustWriter, customizations: List<OperationCustomization>): String =
(customizations.firstNotNullOfOrNull { it.retryType() } ?: RustType.Unit).let { writer.format(it) }
(customizations.firstNotNullOfOrNull { it.retryType() } ?: defaultClassifier).let { writer.format(it) }

private fun needsContentLength(operationShape: OperationShape): Boolean {
return protocol.httpBindingResolver.requestBindings(operationShape)
Expand Down
3 changes: 2 additions & 1 deletion rust-runtime/aws-smithy-client/src/static_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use crate::{Builder, Error, Operation, ParseHttpResponse, ProvideErrorKind};
use aws_smithy_http::operation;
use aws_smithy_http::retry::DefaultResponseClassifier;

#[derive(Debug)]
#[non_exhaustive]
Expand Down Expand Up @@ -40,7 +41,7 @@ impl ParseHttpResponse for TestOperation {
unreachable!("only used for static tests")
}
}
pub type ValidTestOperation = Operation<TestOperation, ()>;
pub type ValidTestOperation = Operation<TestOperation, DefaultResponseClassifier>;

// Statically check that a standard retry can actually be used to build a Client.
#[allow(dead_code)]
Expand Down
6 changes: 5 additions & 1 deletion rust-runtime/aws-smithy-http-tower/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ mod tests {
use aws_smithy_http::operation::{Operation, Request};
use aws_smithy_http::response::ParseStrictResponse;
use aws_smithy_http::result::ConnectorError;
use aws_smithy_http::retry::DefaultResponseClassifier;
use bytes::Bytes;
use http::Response;
use std::convert::{Infallible, TryInto};
Expand Down Expand Up @@ -102,7 +103,10 @@ mod tests {
});

let mut svc = ServiceBuilder::new()
.layer(ParseResponseLayer::<TestParseResponse, ()>::new())
.layer(ParseResponseLayer::<
TestParseResponse,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider calling this "TestResponseHandler" or, instead, changing the verbiage for "response handler" to "response parser"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name is actually fine considering it implements ParseStrictResponse. I think handler might actually be the out of place name that should potentially get fixed in other places.

DefaultResponseClassifier,
>::new())
.layer(MapRequestLayer::for_mapper(AddHeader))
.layer(DispatchLayer)
.service(http_layer);
Expand Down
5 changes: 3 additions & 2 deletions rust-runtime/aws-smithy-http/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use crate::body::SdkBody;
use crate::property_bag::{PropertyBag, SharedPropertyBag};
use crate::retry::DefaultResponseClassifier;
use aws_smithy_types::date_time::DateTimeFormatError;
use http::uri::InvalidUri;
use std::borrow::Cow;
Expand Down Expand Up @@ -232,12 +233,12 @@ impl<H, R> Operation<H, R> {
}

impl<H> Operation<H, ()> {
pub fn new(request: Request, response_handler: H) -> Self {
pub fn new(request: Request, response_handler: H) -> Operation<H, DefaultResponseClassifier> {
Operation {
request,
parts: Parts {
response_handler,
retry_policy: (),
retry_policy: DefaultResponseClassifier::new(),
metadata: None,
},
}
Expand Down
Loading