diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index ccb3d45542..cd0e4bc0cd 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -12,6 +12,7 @@ aws-credential-types = { path = "../aws-credential-types" } aws-http = { path = "../aws-http" } aws-sigv4 = { path = "../aws-sigv4" } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } +aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api" } aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } aws-types = { path = "../aws-types" } diff --git a/aws/rust-runtime/aws-runtime/external-types.toml b/aws/rust-runtime/aws-runtime/external-types.toml index a38a0e0b57..0449ba09bb 100644 --- a/aws/rust-runtime/aws-runtime/external-types.toml +++ b/aws/rust-runtime/aws-runtime/external-types.toml @@ -5,6 +5,10 @@ allowed_external_types = [ "aws_smithy_types::*", "aws_smithy_runtime_api::*", "aws_types::*", + # TODO(audit-external-type-usage) We should newtype these or otherwise avoid exposing them + "http::header::name::HeaderName", + "http::header::value::HeaderValue", "http::request::Request", "http::response::Response", + "http::uri::Uri", ] diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index 9df20ae3bc..a86f74d056 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -30,3 +30,6 @@ pub mod retries; /// Supporting code for invocation ID headers in the AWS SDK. pub mod invocation_id; + +/// Supporting code for request metadata headers in the AWS SDK. +pub mod request_info; diff --git a/aws/rust-runtime/aws-runtime/src/request_info.rs b/aws/rust-runtime/aws-runtime/src/request_info.rs new file mode 100644 index 0000000000..05a80e220b --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/request_info.rs @@ -0,0 +1,224 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime::client::orchestrator::interceptors::{RequestAttempts, ServiceClockSkew}; +use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; +use aws_smithy_runtime_api::client::interceptors::{BoxError, Interceptor, InterceptorContext}; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use aws_smithy_types::date_time::Format; +use aws_smithy_types::retry::RetryConfig; +use aws_smithy_types::timeout::TimeoutConfig; +use aws_smithy_types::DateTime; +use http::{HeaderName, HeaderValue}; +use std::borrow::Cow; +use std::time::{Duration, SystemTime}; + +#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this +const AMZ_SDK_REQUEST: HeaderName = HeaderName::from_static("amz-sdk-request"); + +/// Generates and attaches a request header that communicates request-related metadata. +/// Examples include: +/// +/// - When the client will time out this request. +/// - How many times the request has been retried. +/// - The maximum number of retries that the client will attempt. +#[non_exhaustive] +#[derive(Debug, Default)] +pub struct RequestInfoInterceptor {} + +impl RequestInfoInterceptor { + /// Creates a new `RequestInfoInterceptor` + pub fn new() -> Self { + RequestInfoInterceptor {} + } +} + +impl RequestInfoInterceptor { + fn build_attempts_pair( + &self, + cfg: &ConfigBag, + ) -> Option<(Cow<'static, str>, Cow<'static, str>)> { + let request_attempts = cfg + .get::() + .map(|r_a| r_a.attempts()) + .unwrap_or(1); + let request_attempts = request_attempts.to_string(); + Some((Cow::Borrowed("attempt"), Cow::Owned(request_attempts))) + } + + fn build_max_attempts_pair( + &self, + cfg: &ConfigBag, + ) -> Option<(Cow<'static, str>, Cow<'static, str>)> { + // TODO(enableNewSmithyRuntime) What config will we actually store in the bag? Will it be a whole config or just the max_attempts part? + if let Some(retry_config) = cfg.get::() { + let max_attempts = retry_config.max_attempts().to_string(); + Some((Cow::Borrowed("max"), Cow::Owned(max_attempts))) + } else { + None + } + } + + fn build_ttl_pair(&self, cfg: &ConfigBag) -> Option<(Cow<'static, str>, Cow<'static, str>)> { + let timeout_config = cfg.get::()?; + let socket_read = timeout_config.read_timeout()?; + let estimated_skew: Duration = cfg.get::().cloned()?.into(); + let current_time = SystemTime::now(); + let ttl = current_time.checked_add(socket_read + estimated_skew)?; + let timestamp = DateTime::from(ttl); + let formatted_timestamp = timestamp + .fmt(Format::DateTime) + .expect("the resulting DateTime will always be valid"); + + Some((Cow::Borrowed("ttl"), Cow::Owned(formatted_timestamp))) + } +} + +impl Interceptor for RequestInfoInterceptor { + fn modify_before_transmit( + &self, + context: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let mut pairs = RequestPairs::new(); + if let Some(pair) = self.build_attempts_pair(cfg) { + pairs = pairs.with_pair(pair); + } + if let Some(pair) = self.build_max_attempts_pair(cfg) { + pairs = pairs.with_pair(pair); + } + if let Some(pair) = self.build_ttl_pair(cfg) { + pairs = pairs.with_pair(pair); + } + + let headers = context.request_mut().headers_mut(); + headers.insert(AMZ_SDK_REQUEST, pairs.try_into_header_value()?); + + Ok(()) + } +} + +/// A builder for creating a `RequestPairs` header value. `RequestPairs` is used to generate a +/// retry information header that is sent with every request. The information conveyed by this +/// header allows services to anticipate whether a client will time out or retry a request. +#[derive(Default, Debug)] +pub struct RequestPairs { + inner: Vec<(Cow<'static, str>, Cow<'static, str>)>, +} + +impl RequestPairs { + /// Creates a new `RequestPairs` builder. + pub fn new() -> Self { + Default::default() + } + + /// Adds a pair to the `RequestPairs` builder. + /// Only strings that can be converted to header values are considered valid. + pub fn with_pair( + mut self, + pair: (impl Into>, impl Into>), + ) -> Self { + let pair = (pair.0.into(), pair.1.into()); + self.inner.push(pair); + self + } + + /// Converts the `RequestPairs` builder into a `HeaderValue`. + pub fn try_into_header_value(self) -> Result { + self.try_into() + } +} + +impl TryFrom for HeaderValue { + type Error = BoxError; + + fn try_from(value: RequestPairs) -> Result { + let mut pairs = String::new(); + for (key, value) in value.inner { + if !pairs.is_empty() { + pairs.push_str("; "); + } + + pairs.push_str(&key); + pairs.push('='); + pairs.push_str(&value); + continue; + } + HeaderValue::from_str(&pairs).map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use super::RequestInfoInterceptor; + use crate::request_info::RequestPairs; + use aws_smithy_http::body::SdkBody; + use aws_smithy_runtime::client::orchestrator::interceptors::RequestAttempts; + use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; + use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; + use aws_smithy_runtime_api::config_bag::ConfigBag; + use aws_smithy_runtime_api::type_erasure::TypedBox; + use aws_smithy_types::retry::RetryConfig; + use aws_smithy_types::timeout::TimeoutConfig; + use http::HeaderValue; + use std::time::Duration; + + fn expect_header<'a>( + context: &'a InterceptorContext, + header_name: &str, + ) -> &'a str { + context + .request() + .headers() + .get(header_name) + .unwrap() + .to_str() + .unwrap() + } + + #[test] + fn test_request_pairs_for_initial_attempt() { + let context = InterceptorContext::<()>::new(TypedBox::new("doesntmatter").erase()); + let mut context = context.into_serialization_phase(); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let mut config = ConfigBag::base(); + config.put(RetryConfig::standard()); + config.put( + TimeoutConfig::builder() + .read_timeout(Duration::from_secs(30)) + .build(), + ); + config.put(RequestAttempts::new()); + + let _ = context.take_input(); + let mut context = context.into_before_transmit_phase(); + let interceptor = RequestInfoInterceptor::new(); + interceptor + .modify_before_transmit(&mut context, &mut config) + .unwrap(); + + assert_eq!( + expect_header(&context, "amz-sdk-request"), + "attempt=0; max=3" + ); + } + + #[test] + fn test_header_value_from_request_pairs_supports_all_valid_characters() { + // The list of valid characters is defined by an internal-only spec. + let rp = RequestPairs::new() + .with_pair(("allowed-symbols", "!#$&'*+-.^_`|~")) + .with_pair(("allowed-digits", "01234567890")) + .with_pair(( + "allowed-characters", + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + )) + .with_pair(("allowed-whitespace", " \t")); + let _header_value: HeaderValue = rp + .try_into() + .expect("request pairs can be converted into valid header value."); + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 6d445f271d..8959054d16 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -54,6 +54,7 @@ val DECORATORS: List = listOf( DisabledAuthDecorator(), RecursionDetectionDecorator(), InvocationIdDecorator(), + RetryInformationHeaderDecorator(), ), // Service specific decorators diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt index f3f1da9720..ad95b54e2b 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt @@ -57,7 +57,7 @@ class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : Operati class OperationRetryClassifiersFeature( codegenContext: ClientCodegenContext, - operation: OperationShape, + operationShape: OperationShape, ) : OperationRuntimePluginCustomization() { private val runtimeConfig = codegenContext.runtimeConfig private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) @@ -72,7 +72,7 @@ class OperationRetryClassifiersFeature( "RetryReason" to smithyRuntimeApi.resolve("client::retries::RetryReason"), "ClassifyRetry" to smithyRuntimeApi.resolve("client::retries::ClassifyRetry"), "RetryClassifiers" to smithyRuntimeApi.resolve("client::retries::RetryClassifiers"), - "OperationError" to codegenContext.symbolProvider.symbolForOperationError(operation), + "OperationError" to codegenContext.symbolProvider.symbolForOperationError(operationShape), "SdkError" to RuntimeType.smithyHttp(runtimeConfig).resolve("result::SdkError"), "ErasedError" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("type_erasure::TypeErasedError"), ) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryInformationHeaderDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryInformationHeaderDecorator.kt new file mode 100644 index 0000000000..d930693988 --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryInformationHeaderDecorator.kt @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.util.letIf + +class RetryInformationHeaderDecorator : ClientCodegenDecorator { + override val name: String = "RetryInformationHeader" + override val order: Byte = 10 + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations.letIf(codegenContext.smithyRuntimeMode.generateOrchestrator) { + it + listOf(AddRetryInformationHeaderInterceptors(codegenContext)) + } +} + +private class AddRetryInformationHeaderInterceptors(codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val smithyRuntime = RuntimeType.smithyRuntime(runtimeConfig) + private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) + + override fun section(section: ServiceRuntimePluginSection): Writable = writable { + if (section is ServiceRuntimePluginSection.AdditionalConfig) { + // Track the latency between client and server. + section.registerInterceptor(runtimeConfig, this) { + rust("#T::new()", smithyRuntime.resolve("client::orchestrator::interceptors::ServiceClockSkewInterceptor")) + } + + // Track the number of request attempts made. + section.registerInterceptor(runtimeConfig, this) { + rust("#T::new()", smithyRuntime.resolve("client::orchestrator::interceptors::RequestAttemptsInterceptor")) + } + + // Add request metadata to outgoing requests. Sets a header. + section.registerInterceptor(runtimeConfig, this) { + rust("#T::new()", awsRuntime.resolve("request_info::RequestInfoInterceptor")) + } + } + } +} diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/Cargo.toml b/aws/sra-test/integration-tests/aws-sdk-s3/Cargo.toml index 27998067f1..d93edb0e84 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/Cargo.toml +++ b/aws/sra-test/integration-tests/aws-sdk-s3/Cargo.toml @@ -10,6 +10,7 @@ aws-http = { path = "../../../rust-runtime/aws-http" } aws-runtime = { path = "../../../rust-runtime/aws-runtime" } aws-sdk-s3 = { path = "../../build/sdk/aws-sdk-s3", features = ["test-util"] } aws-smithy-client = { path = "../../../../rust-runtime/aws-smithy-client", features = ["test-util", "rustls"] } +aws-smithy-runtime = { path = "../../../../rust-runtime/aws-smithy-runtime" } aws-smithy-runtime-api = { path = "../../../../rust-runtime/aws-smithy-runtime-api" } aws-types = { path = "../../../rust-runtime/aws-types" } criterion = { version = "0.4", features = ["async_tokio"] } diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs b/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs index c6659cb7c1..7c875e18ab 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs +++ b/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs @@ -6,7 +6,7 @@ #[macro_use] extern crate criterion; use aws_sdk_s3 as s3; -use aws_smithy_runtime_api::client::interceptors::Interceptors; +use aws_smithy_runtime_api::client::interceptors::InterceptorRegistrar; use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; use aws_smithy_runtime_api::config_bag::ConfigBag; use criterion::{BenchmarkId, Criterion}; @@ -88,6 +88,7 @@ macro_rules! middleware_bench_fn { } async fn orchestrator(client: &s3::Client) { + #[derive(Debug)] struct FixupPlugin { region: String, } @@ -95,7 +96,7 @@ async fn orchestrator(client: &s3::Client) { fn configure( &self, cfg: &mut ConfigBag, - _interceptors: &mut Interceptors, + _interceptors: &mut InterceptorRegistrar, ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> { let params_builder = s3::endpoint::Params::builder() .set_region(Some(self.region.clone())) diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/slow-network-and-late-client-clock.json b/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/slow-network-and-late-client-clock.json new file mode 100644 index 0000000000..692e5d23f2 --- /dev/null +++ b/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/slow-network-and-late-client-clock.json @@ -0,0 +1,105 @@ +{ + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~", + "headers": { + "x-amz-security-token": [ + "notarealsessiontoken" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=e7eccf4e792113f5f17a50bfd8f1719479e89ba0b476894e6f3dba030dc87f82" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0" + ], + "x-amz-date": [ + "20210618T170728Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "amz-sdk-invocation-id": [ + "00000000-0000-4000-8000-000000000000" + ], + "user-agent": [ + "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "x-amz-request-id": [ + "9X5E7C9EAB6AQEP2" + ], + "x-amz-id-2": [ + "gZsrBxajPyo1Q0DE2plGf7T6kAnxd4Xx7/S+8lq18GegL6kFbnVXLLh1LnBzpEpFiHN9XoNHkeA=" + ], + "content-type": [ + "application/xml" + ], + "transfer-encoding": [ + "chunked" + ], + "server": [ + "AmazonS3" + ], + "date": [ + "Wed, 26 Apr 2023 14:00:24 GMT" + ], + "x-amz-bucket-region": [ + "us-east-1" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "\n\n test-bucket\n prefix~\n 1\n 1000\n false\n \n some-file.file\n 2009-10-12T17:50:30.000Z\n 434234\n STANDARD\n \n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ], + "docs": "One SDK operation invocation. Client retries 3 times, successful response on 3rd attempt. Slow network, one way latency is 2 seconds. Server takes 1 second to generate response. Client clock is 10 minutes behind server clock. One second delay between retries.", + "version": "V0" +} diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/three-retries_and-then-success.json b/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/three-retries_and-then-success.json new file mode 100644 index 0000000000..b0aee24861 --- /dev/null +++ b/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/three-retries_and-then-success.json @@ -0,0 +1,312 @@ +{ + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~", + "headers": { + "x-amz-security-token": [ + "notarealsessiontoken" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=e7eccf4e792113f5f17a50bfd8f1719479e89ba0b476894e6f3dba030dc87f82" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0" + ], + "x-amz-date": [ + "20190601T000000Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "amz-sdk-invocation-id": [ + "00000000-0000-4000-8000-000000000000" + ], + "amz-sdk-request": [ + "attempt=1; max=3" + ], + "user-agent": [ + "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 500, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "x-amz-request-id": [ + "foo-id" + ], + "x-amz-id-2": [ + "foo-id" + ], + "content-type": [ + "application/xml" + ], + "transfer-encoding": [ + "chunked" + ], + "server": [ + "AmazonS3" + ], + "date": [ + "Sat, 01 Jun 2019 00:00:00 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "\n \"\n Server\n InternalError\n We encountered an internal error. Please try again.\n foo-id\n\"" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~", + "headers": { + "x-amz-security-token": [ + "notarealsessiontoken" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=e7eccf4e792113f5f17a50bfd8f1719479e89ba0b476894e6f3dba030dc87f82" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0" + ], + "x-amz-date": [ + "20190601T000001Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "amz-sdk-invocation-id": [ + "00000000-0000-4000-8000-000000000000" + ], + "amz-sdk-request": [ + "ttl=20190601T000011Z; attempt=2; max=3" + ], + "user-agent": [ + "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 500, + "version": "HTTP/1.1", + "headers": { + "server": [ + "AmazonS3" + ], + "x-amz-request-id": [ + "foo-id" + ], + "x-amz-id-2": [ + "foo-id" + ], + "content-type": [ + "application/xml" + ], + "transfer-encoding": [ + "chunked" + ], + "server": [ + "AmazonS3" + ], + "date": [ + "Sat, 01 Jun 2019 00:00:01 GMT" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "\n \"\n Server\n InternalError\n We encountered an internal error. Please try again.\n foo-id\n\"" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~", + "headers": { + "x-amz-security-token": [ + "notarealsessiontoken" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=e7eccf4e792113f5f17a50bfd8f1719479e89ba0b476894e6f3dba030dc87f82" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0" + ], + "x-amz-date": [ + "20190601T000002Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "amz-sdk-invocation-id": [ + "00000000-0000-4000-8000-000000000000" + ], + "amz-sdk-request": [ + "ttl=20190601T000012Z; attempt=3; max=3" + ], + "user-agent": [ + "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "x-amz-request-id": [ + "9X5E7C9EAB6AQEP2" + ], + "x-amz-id-2": [ + "gZsrBxajPyo1Q0DE2plGf7T6kAnxd4Xx7/S+8lq18GegL6kFbnVXLLh1LnBzpEpFiHN9XoNHkeA=" + ], + "content-type": [ + "application/xml" + ], + "transfer-encoding": [ + "chunked" + ], + "server": [ + "AmazonS3" + ], + "date": [ + "Sat, 01 Jun 2019 00:00:02 GMT" + ], + "x-amz-bucket-region": [ + "us-east-1" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "\n\n test-bucket\n prefix~\n 1\n 1000\n false\n \n some-file.file\n 2009-10-12T17:50:30.000Z\n 434234\n STANDARD\n \n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ], + "docs": "One SDK operation invocation. Client retries 3 times, successful response on 3rd attempt. Fast network, latency + server time is less than one second. No clock skew. Client waits 1 second between retry attempts.", + "version": "V0" +} diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/three-successful-attempts.json b/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/three-successful-attempts.json new file mode 100644 index 0000000000..180b54c5d7 --- /dev/null +++ b/aws/sra-test/integration-tests/aws-sdk-s3/test-data/request-information-headers/three-successful-attempts.json @@ -0,0 +1,105 @@ +{ + "events": [ + { + "connection_id": 0, + "action": { + "Request": { + "request": { + "uri": "https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~", + "headers": { + "x-amz-security-token": [ + "notarealsessiontoken" + ], + "authorization": [ + "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=e7eccf4e792113f5f17a50bfd8f1719479e89ba0b476894e6f3dba030dc87f82" + ], + "x-amz-user-agent": [ + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0" + ], + "x-amz-date": [ + "20210618T170728Z" + ], + "x-amz-content-sha256": [ + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "amz-sdk-invocation-id": [ + "00000000-0000-4000-8000-000000000000" + ], + "user-agent": [ + "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" + ] + }, + "method": "GET" + } + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Request" + } + } + }, + { + "connection_id": 0, + "action": { + "Response": { + "response": { + "Ok": { + "status": 200, + "version": "HTTP/1.1", + "headers": { + "x-amz-request-id": [ + "9X5E7C9EAB6AQEP2" + ], + "x-amz-id-2": [ + "gZsrBxajPyo1Q0DE2plGf7T6kAnxd4Xx7/S+8lq18GegL6kFbnVXLLh1LnBzpEpFiHN9XoNHkeA=" + ], + "content-type": [ + "application/xml" + ], + "transfer-encoding": [ + "chunked" + ], + "server": [ + "AmazonS3" + ], + "date": [ + "Wed, 26 Apr 2023 14:00:24 GMT" + ], + "x-amz-bucket-region": [ + "us-east-1" + ] + } + } + } + } + } + }, + { + "connection_id": 0, + "action": { + "Data": { + "data": { + "Utf8": "\n\n test-bucket\n prefix~\n 1\n 1000\n false\n \n some-file.file\n 2009-10-12T17:50:30.000Z\n 434234\n STANDARD\n \n" + }, + "direction": "Response" + } + } + }, + { + "connection_id": 0, + "action": { + "Eof": { + "ok": true, + "direction": "Response" + } + } + } + ], + "docs": "Client makes 3 separate SDK operation invocations; All succeed on first attempt. Fast network, latency + server time is less than one second.", + "version": "V0" +} diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/request_information_headers.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/request_information_headers.rs new file mode 100644 index 0000000000..af0ab416dd --- /dev/null +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/request_information_headers.rs @@ -0,0 +1,257 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_http::user_agent::AwsUserAgent; +use aws_runtime::invocation_id::InvocationId; +use aws_sdk_s3::config::{Credentials, Region}; +use aws_sdk_s3::endpoint::Params; +use aws_sdk_s3::Client; +use aws_smithy_client::dvr; +use aws_smithy_client::dvr::MediaType; +use aws_smithy_client::erase::DynConnector; +use aws_smithy_runtime::client::retries::strategy::FixedDelayRetryStrategy; +use aws_smithy_runtime_api::client::interceptors::InterceptorRegistrar; +use aws_smithy_runtime_api::client::orchestrator::{ConfigBagAccessors, RequestTime}; +use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +#[derive(Debug)] +struct FixupPlugin { + client: Client, + timestamp: SystemTime, +} + +// # One SDK operation invocation. +// # Client retries 3 times, successful response on 3rd attempt. +// # Fast network, latency + server time is less than one second. +// # No clock skew +// # Client waits 1 second between retry attempts. +#[tokio::test] +async fn three_retries_and_then_success() { + tracing_subscriber::fmt::init(); + + impl RuntimePlugin for FixupPlugin { + fn configure( + &self, + cfg: &mut ConfigBag, + _interceptors: &mut InterceptorRegistrar, + ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> { + let params_builder = Params::builder() + .set_region(self.client.conf().region().map(|c| c.as_ref().to_string())) + .bucket("test-bucket"); + + cfg.put(params_builder); + cfg.set_request_time(RequestTime::new(self.timestamp.clone())); + cfg.put(AwsUserAgent::for_tests()); + cfg.put(InvocationId::for_tests()); + cfg.set_retry_strategy(FixedDelayRetryStrategy::one_second_delay()); + Ok(()) + } + } + + let path = "test-data/request-information-headers/three-retries_and-then-success.json"; + let conn = dvr::ReplayingConnection::from_file(path).unwrap(); + let config = aws_sdk_s3::Config::builder() + .credentials_provider(Credentials::for_tests()) + .region(Region::new("us-east-1")) + .http_connector(DynConnector::new(conn.clone())) + .build(); + let client = Client::from_conf(config); + let fixup = FixupPlugin { + client: client.clone(), + timestamp: UNIX_EPOCH + Duration::from_secs(1624036048), + }; + + let resp = dbg!( + client + .list_objects_v2() + .config_override(aws_sdk_s3::Config::builder().force_path_style(false)) + .bucket("test-bucket") + .prefix("prefix~") + .send_orchestrator_with_plugin(Some(fixup)) + .await + ); + + let resp = resp.expect("valid e2e test"); + assert_eq!(resp.name(), Some("test-bucket")); + conn.full_validate(MediaType::Xml).await.expect("failed") +} +// +// // # Client makes 3 separate SDK operation invocations +// // # All succeed on first attempt. +// // # Fast network, latency + server time is less than one second. +// // - request: +// // time: 2019-06-01T00:00:00Z +// // headers: +// // amz-sdk-invocation-id: 3dfe4f26-c090-4887-8c14-7bac778bca07 +// // amz-sdk-request: attempt=1; max=3 +// // response: +// // status: 200 +// // time_received: 2019-06-01T00:00:00Z +// // headers: +// // Date: Sat, 01 Jun 2019 00:00:00 GMT +// // - request: +// // time: 2019-06-01T00:01:01Z +// // headers: +// // # Note the different invocation id because it's a new SDK +// // # invocation operation. +// // amz-sdk-invocation-id: 70370531-7b83-4b90-8b93-46975687ecf6 +// // amz-sdk-request: ttl=20190601T000011Z; attempt=1; max=3 +// // response: +// // status: 200 +// // time_received: 2019-06-01T00:00:01Z +// // headers: +// // Date: Sat, 01 Jun 2019 00:00:01 GMT +// // - request: +// // time: 2019-06-01T00:00:02Z +// // headers: +// // amz-sdk-invocation-id: 910bf450-6c90-43de-a508-3fa126a06b71 +// // amz-sdk-request: ttl=20190601T000012Z; attempt=1; max=3 +// // response: +// // status: 200 +// // time_received: 2019-06-01T00:00:02Z +// // headers: +// // Date: Sat, 01 Jun 2019 00:00:02 GMT +// const THREE_SUCCESSFUL_ATTEMPTS_PATH: &str = "test-data/request-information-headers/three-successful-attempts.json"; +// #[tokio::test] +// async fn three_successful_attempts() { +// tracing_subscriber::fmt::init(); +// +// impl RuntimePlugin for FixupPlugin { +// fn configure( +// &self, +// cfg: &mut ConfigBag, +// ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> { +// let params_builder = Params::builder() +// .set_region(self.client.conf().region().map(|c| c.as_ref().to_string())) +// .bucket("test-bucket"); +// +// cfg.put(params_builder); +// cfg.set_request_time(RequestTime::new(self.timestamp.clone())); +// cfg.put(AwsUserAgent::for_tests()); +// cfg.put(InvocationId::for_tests()); +// Ok(()) +// } +// } +// +// let conn = dvr::ReplayingConnection::from_file(THREE_SUCCESSFUL_ATTEMPTS_PATH).unwrap(); +// let config = aws_sdk_s3::Config::builder() +// .credentials_provider(Credentials::for_tests()) +// .region(Region::new("us-east-1")) +// .http_connector(DynConnector::new(conn.clone())) +// .build(); +// let client = Client::from_conf(config); +// let fixup = FixupPlugin { +// client: client.clone(), +// timestamp: UNIX_EPOCH + Duration::from_secs(1624036048), +// }; +// +// let resp = dbg!( +// client +// .list_objects_v2() +// .config_override(aws_sdk_s3::Config::builder().force_path_style(false)) +// .bucket("test-bucket") +// .prefix("prefix~") +// .send_v2_with_plugin(Some(fixup)) +// .await +// ); +// +// let resp = resp.expect("valid e2e test"); +// assert_eq!(resp.name(), Some("test-bucket")); +// conn.full_validate(MediaType::Xml).await.expect("failed") +// } +// +// // # One SDK operation invocation. +// // # Client retries 3 times, successful response on 3rd attempt. +// // # Slow network, one way latency is 2 seconds. +// // # Server takes 1 second to generate response. +// // # Client clock is 10 minutes behind server clock. +// // # One second delay between retries. +// // - request: +// // time: 2019-06-01T00:00:00Z +// // headers: +// // amz-sdk-invocation-id: 3dfe4f26-c090-4887-8c14-7bac778bca07 +// // amz-sdk-request: attempt=1; max=3 +// // response: +// // status: 500 +// // time_received: 2019-06-01T00:00:05Z +// // headers: +// // Date: Sat, 01 Jun 2019 00:10:03 GMT +// // - request: +// // time: 2019-06-01T00:00:06Z +// // # The ttl is 00:00:16 with the client clock, +// // # but accounting for skew we have +// // # 00:10:03 - 00:00:05 = 00:09:58 +// // # ttl = 00:00:16 + 00:09:58 = 00:10:14 +// // headers: +// // amz-sdk-invocation-id: 3dfe4f26-c090-4887-8c14-7bac778bca07 +// // amz-sdk-request: ttl=20190601T001014Z; attempt=2; max=3 +// // response: +// // status: 500 +// // time_received: 2019-06-01T00:00:11Z +// // headers: +// // Date: Sat, 01 Jun 2019 00:10:09 GMT +// // - request: +// // time: 2019-06-01T00:00:12Z +// // headers: +// // # ttl = 00:00:12 + 20 = 00:00:22 +// // # skew is: +// // # 00:10:09 - 00:00:11 +// // amz-sdk-invocation-id: 3dfe4f26-c090-4887-8c14-7bac778bca07 +// // amz-sdk-request: ttl=20190601T001020Z; attempt=3; max=3 +// // response: +// // status: 200 +// // time_received: 2019-06-01T00:00:17Z +// // headers: +// // Date: Sat, 01 Jun 2019 00:10:15 GMT +// const SLOW_NETWORK_AND_LATE_CLIENT_CLOCK_PATH: &str = "test-data/request-information-headers/slow-network-and-late-client-clock.json"; +// #[tokio::test] +// async fn slow_network_and_late_client_clock() { +// tracing_subscriber::fmt::init(); +// +// impl RuntimePlugin for FixupPlugin { +// fn configure( +// &self, +// cfg: &mut ConfigBag, +// ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> { +// let params_builder = Params::builder() +// .set_region(self.client.conf().region().map(|c| c.as_ref().to_string())) +// .bucket("test-bucket"); +// +// cfg.put(params_builder); +// cfg.set_request_time(RequestTime::new(self.timestamp.clone())); +// cfg.put(AwsUserAgent::for_tests()); +// cfg.put(InvocationId::for_tests()); +// Ok(()) +// } +// } +// +// let conn = dvr::ReplayingConnection::from_file(SLOW_NETWORK_AND_LATE_CLIENT_CLOCK_PATH).unwrap(); +// let config = aws_sdk_s3::Config::builder() +// .credentials_provider(Credentials::for_tests()) +// .region(Region::new("us-east-1")) +// .http_connector(DynConnector::new(conn.clone())) +// .build(); +// let client = Client::from_conf(config); +// let fixup = FixupPlugin { +// client: client.clone(), +// timestamp: UNIX_EPOCH + Duration::from_secs(1624036048), +// }; +// +// let resp = dbg!( +// client +// .list_objects_v2() +// .config_override(aws_sdk_s3::Config::builder().force_path_style(false)) +// .bucket("test-bucket") +// .prefix("prefix~") +// .send_v2_with_plugin(Some(fixup)) +// .await +// ); +// +// let resp = resp.expect("valid e2e test"); +// assert_eq!(resp.name(), Some("test-bucket")); +// conn.full_validate(MediaType::Xml).await.expect("failed") +// } diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index 68b7f66319..496d50d84b 100644 --- a/rust-runtime/aws-smithy-runtime/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime/Cargo.toml @@ -27,7 +27,7 @@ http-body = "0.4.5" pin-project-lite = "0.2.7" pin-utils = "0.1.0" tokio = { version = "1.25", features = [] } -tracing = "0.1" +tracing = "0.1.37" [dev-dependencies] aws-smithy-async = { path = "../aws-smithy-async", features = ["rt-tokio"] } diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs index 4b291a87d4..e3be9839c3 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs @@ -23,6 +23,7 @@ mod auth; /// Defines types that implement a trait for endpoint resolution pub mod endpoints; mod http; +pub mod interceptors; #[doc(hidden)] #[macro_export] diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors.rs new file mode 100644 index 0000000000..081b9b3bd7 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors.rs @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod request_attempts; +mod service_clock_skew; + +pub use request_attempts::{RequestAttempts, RequestAttemptsInterceptor}; +pub use service_clock_skew::{ServiceClockSkew, ServiceClockSkewInterceptor}; diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors/request_attempts.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors/request_attempts.rs new file mode 100644 index 0000000000..c69137b022 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors/request_attempts.rs @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeTransmit; +use aws_smithy_runtime_api::client::interceptors::{BoxError, Interceptor, InterceptorContext}; +use aws_smithy_runtime_api::config_bag::ConfigBag; + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct RequestAttempts { + attempts: u32, +} + +impl RequestAttempts { + pub fn new() -> Self { + Self::default() + } + + // There is no legitimate reason to set this unless you're testing things. + // Therefore, this is only available for tests. + #[cfg(test)] + pub fn new_with_attempts(attempts: u32) -> Self { + Self { attempts } + } + + pub fn attempts(&self) -> u32 { + self.attempts + } + + fn increment(mut self) -> Self { + self.attempts += 1; + self + } +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct RequestAttemptsInterceptor {} + +impl RequestAttemptsInterceptor { + pub fn new() -> Self { + Self::default() + } +} + +impl Interceptor for RequestAttemptsInterceptor { + fn modify_before_retry_loop( + &self, + _ctx: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + cfg.put(RequestAttempts::new()); + Ok(()) + } + + fn modify_before_transmit( + &self, + _ctx: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + if let Some(request_attempts) = cfg.get::().cloned() { + cfg.put(request_attempts.increment()); + } + Ok(()) + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors/service_clock_skew.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors/service_clock_skew.rs new file mode 100644 index 0000000000..579ecbb484 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/interceptors/service_clock_skew.rs @@ -0,0 +1,83 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime_api::client::interceptors::context::phase::BeforeDeserialization; +use aws_smithy_runtime_api::client::interceptors::{BoxError, Interceptor, InterceptorContext}; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use aws_smithy_types::date_time::Format; +use aws_smithy_types::DateTime; +use std::time::{Duration, SystemTime}; + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ServiceClockSkew { + inner: Duration, +} + +impl ServiceClockSkew { + fn new(inner: Duration) -> Self { + Self { inner } + } + + pub fn skew(&self) -> Duration { + self.inner + } +} + +impl From for Duration { + fn from(skew: ServiceClockSkew) -> Duration { + skew.inner + } +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct ServiceClockSkewInterceptor {} + +impl ServiceClockSkewInterceptor { + pub fn new() -> Self { + Self::default() + } +} + +fn calculate_skew(time_sent: DateTime, time_received: DateTime) -> Duration { + let skew = (time_sent.as_secs_f64() - time_received.as_secs_f64()).max(0.0); + Duration::from_secs_f64(skew) +} + +fn extract_time_sent_from_response( + ctx: &mut InterceptorContext, +) -> Result { + let date_header = ctx + .response() + .headers() + .get("date") + .ok_or("Response from server does not include a `date` header")? + .to_str()?; + DateTime::from_str(date_header, Format::HttpDate).map_err(Into::into) +} + +impl Interceptor for ServiceClockSkewInterceptor { + fn modify_before_deserialization( + &self, + ctx: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let time_received = DateTime::from(SystemTime::now()); + let time_sent = match extract_time_sent_from_response(ctx) { + Ok(time_sent) => time_sent, + Err(e) => { + // We don't want to fail a request for this because 1xx and 5xx responses and + // responses from servers with no clock may omit this header. We still log it at the + // trace level to aid in debugging. + tracing::trace!("failed to calculate clock skew of service from response: {e}. Ignoring this error...",); + return Ok(()); + } + }; + let skew = ServiceClockSkew::new(calculate_skew(time_sent, time_received)); + cfg.put(skew); + Ok(()) + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs index 6b7854fc2c..6f1e71d39e 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs @@ -3,6 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +#[cfg(feature = "test-util")] +mod fixed_delay; mod never; +#[cfg(feature = "test-util")] +pub use fixed_delay::FixedDelayRetryStrategy; pub use never::NeverRetryStrategy; diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/fixed_delay.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/fixed_delay.rs new file mode 100644 index 0000000000..1ee985c22e --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/fixed_delay.rs @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::client::orchestrator::interceptors::RequestAttempts; +use aws_smithy_runtime_api::client::interceptors::context::phase::AfterDeserialization; +use aws_smithy_runtime_api::client::interceptors::InterceptorContext; +use aws_smithy_runtime_api::client::orchestrator::BoxError; +use aws_smithy_runtime_api::client::retries::{ + ClassifyRetry, RetryClassifiers, RetryReason, RetryStrategy, ShouldAttempt, +}; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use std::time::Duration; + +// A retry policy used in tests. This relies on an error classifier already present in the config bag. +// If a server response is retryable, it will be retried after a fixed delay. +#[derive(Debug, Clone)] +pub struct FixedDelayRetryStrategy { + fixed_delay: Duration, + max_attempts: u32, +} + +impl FixedDelayRetryStrategy { + pub fn new(fixed_delay: Duration) -> Self { + Self { + fixed_delay, + max_attempts: 4, + } + } + + pub fn one_second_delay() -> Self { + Self::new(Duration::from_secs(1)) + } +} + +impl RetryStrategy for FixedDelayRetryStrategy { + fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result { + Ok(ShouldAttempt::Yes) + } + + fn should_attempt_retry( + &self, + ctx: &InterceptorContext, + cfg: &ConfigBag, + ) -> Result { + // Look a the result. If it's OK then we're done; No retry required. Otherwise, we need to inspect it + let error = match ctx.output_or_error() { + Ok(_) => { + tracing::trace!("request succeeded, no retry necessary"); + return Ok(ShouldAttempt::No); + } + Err(err) => err, + }; + + let request_attempts: &RequestAttempts = cfg + .get() + .expect("at least one request attempt is made before any retry is attempted"); + if request_attempts.attempts() == self.max_attempts { + tracing::trace!( + attempts = request_attempts.attempts(), + max_attempts = self.max_attempts, + "not retrying because we are out of attempts" + ); + return Ok(ShouldAttempt::No); + } + + let retry_classifiers = cfg + .get::() + .expect("a retry classifier is set"); + let retry_reason = retry_classifiers.classify_retry(error); + + let backoff = match retry_reason { + Some(RetryReason::Explicit(_)) => self.fixed_delay, + Some(RetryReason::Error(_)) => self.fixed_delay, + Some(_) => { + unreachable!("RetryReason is non-exhaustive. Therefore, we need to cover this unreachable case.") + } + None => { + tracing::trace!( + attempts = request_attempts.attempts(), + max_attempts = self.max_attempts, + "encountered unretryable error" + ); + return Ok(ShouldAttempt::No); + } + }; + + tracing::debug!( + "attempt {} failed with {:?}; retrying after {:?}", + request_attempts.attempts(), + retry_reason, + backoff + ); + + Ok(ShouldAttempt::YesAfterDelay(backoff)) + } +} diff --git a/rust-runtime/aws-smithy-types/src/date_time/mod.rs b/rust-runtime/aws-smithy-types/src/date_time/mod.rs index 6bc099d25a..fd9555b5e6 100644 --- a/rust-runtime/aws-smithy-types/src/date_time/mod.rs +++ b/rust-runtime/aws-smithy-types/src/date_time/mod.rs @@ -330,16 +330,20 @@ impl fmt::Display for ConversionError { /// Formats for representing a `DateTime` in the Smithy protocols. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Format { - /// RFC-3339 Date Time. If the date time has an offset, an error will be returned + /// RFC-3339 Date Time. If the date time has an offset, an error will be returned. + /// e.g. `2019-12-16T23:48:18Z` DateTime, - /// RFC-3339 Date Time. Offsets are supported + /// RFC-3339 Date Time. Offsets are supported. + /// e.g. `2019-12-16T23:48:18+01:00` DateTimeWithOffset, /// Date format used by the HTTP `Date` header, specified in RFC-7231. + /// e.g. `Mon, 16 Dec 2019 23:48:18 GMT` HttpDate, /// Number of seconds since the Unix epoch formatted as a floating point. + /// e.g. `1576540098.52` EpochSeconds, }