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

Replenish cross-request retry allowance on successful response #1197

Merged
merged 7 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -256,3 +256,21 @@ message = "Fixed example showing how to use hardcoded credentials in `aws-types`
references = ["smithy-rs#1180"]
meta = { "breaking" = false, "tada" = false, "bug" = true }
author = "jdisanti"

[[aws-sdk-rust]]
message = "Fixed a bug that caused clients to eventually stop retrying. The cross-request retry allowance wasn't being reimbursed upon receiving a successful response, so once this allowance reached zero, no further retries would ever be attempted."
references = ["smithy-rs#1197"]
meta = { "breaking" = false, "tada" = false, "bug" = true }
author = "jdisanti"

[[smithy-rs]]
message = "Fixed a bug that caused clients to eventually stop retrying. The cross-request retry allowance wasn't being reimbursed upon receiving a successful response, so once this allowance reached zero, no further retries would ever be attempted."
references = ["smithy-rs#1197"]
meta = { "breaking" = false, "tada" = false, "bug" = true }
author = "jdisanti"

[[smithy-rs]]
message = "`aws_smithy_types::retry::RetryKind` had its `NotRetryable` variant split into `UnretryableFailure` and `Unnecessary`. If you implement the `ClassifyResponse`, then successful responses need to return `Unnecessary`, and failures that shouldn't be retried need to return `UnretryableFailure`."
references = ["smithy-rs#1197"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"
8 changes: 4 additions & 4 deletions aws/rust-runtime/aws-config/src/http_credential_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ impl ClassifyResponse<SdkSuccess<Credentials>, SdkError<CredentialsError>>
* - Non-parseable 200 responses.
* */
match response {
Ok(_) => RetryKind::NotRetryable,
Ok(_) => RetryKind::Unnecessary,
// socket errors, networking timeouts
Err(SdkError::DispatchFailure(client_err))
if client_err.is_timeout() || client_err.is_io() =>
Expand All @@ -192,7 +192,7 @@ impl ClassifyResponse<SdkSuccess<Credentials>, SdkError<CredentialsError>>
{
RetryKind::Error(ErrorKind::ServerError)
}
Err(_) => RetryKind::NotRetryable,
Err(_) => RetryKind::UnretryableFailure,
}
}
}
Expand Down Expand Up @@ -260,7 +260,7 @@ mod test {

assert_eq!(
HttpCredentialRetryPolicy.classify(sdk_result.as_ref()),
RetryKind::NotRetryable
RetryKind::Unnecessary
);

assert!(sdk_result.is_ok(), "should be ok: {:?}", sdk_result)
Expand All @@ -275,7 +275,7 @@ mod test {
let sdk_result = sdk_resp(error_response);
assert_eq!(
HttpCredentialRetryPolicy.classify(sdk_result.as_ref()),
RetryKind::NotRetryable
RetryKind::UnretryableFailure
);
let sdk_error = sdk_result.expect_err("should be error");

Expand Down
45 changes: 40 additions & 5 deletions aws/rust-runtime/aws-config/src/imds/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,8 @@ impl ImdsErrorPolicy {
_ if status.is_server_error() => RetryKind::Error(ErrorKind::ServerError),
// 401 indicates that the token has expired, this is retryable
_ if status.as_u16() == 401 => RetryKind::Error(ErrorKind::ServerError),
_ => RetryKind::NotRetryable,
// This catch-all includes successful responses that fail to parse. These should not be retried.
_ => RetryKind::UnretryableFailure,
}
}
}
Expand All @@ -710,11 +711,11 @@ impl ImdsErrorPolicy {
impl<T, E> ClassifyResponse<SdkSuccess<T>, SdkError<E>> for ImdsErrorPolicy {
fn classify(&self, response: Result<&SdkSuccess<T>, &SdkError<E>>) -> RetryKind {
match response {
Ok(_) => RetryKind::NotRetryable,
Ok(_) => RetryKind::Unnecessary,
Err(SdkError::ResponseError { raw, .. }) | Err(SdkError::ServiceError { raw, .. }) => {
ImdsErrorPolicy::classify(raw)
}
_ => RetryKind::NotRetryable,
_ => RetryKind::UnretryableFailure,
}
}
}
Expand All @@ -723,20 +724,24 @@ impl<T, E> ClassifyResponse<SdkSuccess<T>, SdkError<E>> for ImdsErrorPolicy {
pub(crate) mod test {
use std::collections::HashMap;
use std::error::Error;
use std::io;
use std::time::{Duration, UNIX_EPOCH};

use aws_smithy_async::rt::sleep::TokioSleep;
use aws_smithy_client::erase::DynConnector;
use aws_smithy_client::test_connection::{capture_request, TestConnection};
use aws_smithy_client::{SdkError, SdkSuccess};
use aws_smithy_http::body::SdkBody;
use aws_smithy_http::operation;
use aws_smithy_types::retry::RetryKind;
use aws_types::os_shim_internal::{Env, Fs, ManualTimeSource, TimeSource};
use http::header::USER_AGENT;
use http::Uri;
use serde::Deserialize;
use tracing_test::traced_test;

use crate::imds::client::{Client, EndpointMode};
use crate::imds::client::{Client, EndpointMode, ImdsErrorPolicy};
use crate::provider_config::ProviderConfig;
use http::header::USER_AGENT;

const TOKEN_A: &str = "AQAEAFTNrA4eEGx0AQgJ1arIq_Cc-t4tWt3fB0Hd8RKhXlKc5ccvhg==";
const TOKEN_B: &str = "alternatetoken==";
Expand Down Expand Up @@ -977,6 +982,36 @@ pub(crate) mod test {
connection.assert_requests_match(&[]);
}

/// Successful responses should classify as `RetryKind::Unnecessary`
#[test]
fn successful_response_properly_classified() {
use aws_smithy_http::retry::ClassifyResponse;

struct GenericAppeaser; // Need a type to appease the generics; don't care what it is
jdisanti marked this conversation as resolved.
Show resolved Hide resolved
let policy = ImdsErrorPolicy;
fn response_200() -> operation::Response {
operation::Response::new(imds_response("").map(|_| SdkBody::empty()))
}
let success = SdkSuccess {
raw: response_200(),
parsed: GenericAppeaser,
};
assert_eq!(
RetryKind::Unnecessary,
policy.classify(Ok::<_, &SdkError<GenericAppeaser>>(&success))
);

// Emulate a failure to parse the response body (using an io error since it's easy to construct in a test)
let failure = SdkError::<GenericAppeaser>::ResponseError {
err: Box::new(io::Error::new(io::ErrorKind::BrokenPipe, "fail to parse")),
raw: response_200(),
};
assert_eq!(
RetryKind::UnretryableFailure,
policy.classify(Err::<&SdkSuccess<GenericAppeaser>, _>(&failure))
);
}

// since tokens are sent as headers, the tokens need to be valid header values
#[tokio::test]
async fn invalid_token() {
Expand Down
12 changes: 6 additions & 6 deletions aws/rust-runtime/aws-http/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,18 @@ where
{
fn classify(&self, err: Result<&T, &SdkError<E>>) -> RetryKind {
let (err, response) = match err {
Ok(_) => return RetryKind::NotRetryable,
Ok(_) => return RetryKind::Unnecessary,
Err(SdkError::ServiceError { err, raw }) => (err, raw),
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::NotRetryable
RetryKind::UnretryableFailure
}
}
Err(_) => return RetryKind::NotRetryable,
Err(_) => return RetryKind::UnretryableFailure,
};
if let Some(retry_after_delay) = response
.http()
Expand All @@ -95,7 +95,7 @@ where
return RetryKind::Error(ErrorKind::TransientError);
};
// TODO(https://github.com/awslabs/smithy-rs/issues/966): IDPCommuncation error needs to be retried
RetryKind::NotRetryable
RetryKind::UnretryableFailure
}
}

Expand Down Expand Up @@ -151,7 +151,7 @@ mod test {
let test_response = http::Response::new("OK");
assert_eq!(
policy.classify(make_err(UnmodeledError, test_response).as_ref()),
RetryKind::NotRetryable
RetryKind::UnretryableFailure
);
}

Expand All @@ -177,7 +177,7 @@ mod test {
.unwrap();
assert_eq!(
policy.classify(make_err(UnmodeledError, test_resp).as_ref()),
RetryKind::NotRetryable
RetryKind::UnretryableFailure
);
}

Expand Down
8 changes: 4 additions & 4 deletions aws/sdk/integration-tests/dynamodb/tests/movies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ where
&self,
response: Result<&SdkSuccess<DescribeTableOutput>, &SdkError<DescribeTableError>>,
) -> RetryKind {
match self.inner.classify(response.clone()) {
RetryKind::NotRetryable => (),
match self.inner.classify(response) {
jdisanti marked this conversation as resolved.
Show resolved Hide resolved
RetryKind::UnretryableFailure | RetryKind::Unnecessary => (),
other => return other,
};
match response {
Expand All @@ -142,10 +142,10 @@ where
{
RetryKind::Explicit(Duration::from_secs(1))
} else {
RetryKind::NotRetryable
RetryKind::Unnecessary
}
}
_ => RetryKind::NotRetryable,
_ => RetryKind::UnretryableFailure,
}
}
}
Expand Down
Loading