mirror of https://github.com/smithy-lang/smithy-rs
Feature: Fine-grained Timeout Configuration (#831)
* add: TimeoutConfig add: provider to fetch timeout from profile add: provider to fetch timeout from environment add: default provider for TimeoutConfigs update: aws_config::Config to support timeout conf update: rustdoc lint * add: TimeoutConfigBuilder::merge_with test * update: changelogs * add: timeout_config to client and builder add: generic timeout service add: non-working timeout layer * add: timeout layer/service with configurable duration add: test that ensures timeout service works * fix: eliminate useless clones * feature: Kotlin decorator for TimeoutConfig add: tests for timeout-related codegen update: incorrect package path in RetryConfigDecorator.kt fix: outdated aws-config timeout config code * remove: link to struct in external crate fix: outdated doc test fix: add missing import to doc test fix: copypaste error in doc test update: outdated lint name * update: comment on Builder::timeout_config panics add: test ensuring timeouts can't be infinite update: use a floating point number in a timeout doc update: message for failed profile load to mention that profile will be skipped remove: commented out code update: attempt to make difference between api_call_timeout and api_call_attempt_timeout clearer update: outdated doc comments update: TimeoutConfigError descriptions * formatting: arrange imports in aws_smithy_client lib.rs add: todo for improving timeout error categorization * update: var parsers to work without needless allocations update: improve example for default timeout config provider update: improve example for default retry config provider fix: unhelpful doc comment for TimeoutLayerFuture<T> remove: outdated TODO * fix: various typos in docs add: PR links to changelogs format: doc references to structs to look nicer add: note about expected form and unit of timeout config data update: expand ProfileFileTimeoutConfigProvider example * fix: relocate provider config tests to the correct package/directory remove: unimplemented timeouts from `Settings` * add: S3 integration test for timeouts update: re-enable TimeoutLayers add: "list_of_set_timeouts" logging helper to TimeoutConfig refactor: the way TimeoutService handles futures so that it can work better with no timeout set add: helper structs to make creating timeout services easier * update: split timeout_config example into multiple lines * fix: Clippy lints * fix: outdated test * fix: more clippy lints * fix: typo add: TimeoutConfig example * Update CHANGELOG.md Co-authored-by: Russell Cohen <rcoh@amazon.com> * Update aws/rust-runtime/aws-config/src/default_provider.rs Co-authored-by: Russell Cohen <rcoh@amazon.com> * Update aws/rust-runtime/aws-config/src/default_provider.rs Co-authored-by: Russell Cohen <rcoh@amazon.com> * Update aws/rust-runtime/aws-config/src/default_provider.rs Co-authored-by: Russell Cohen <rcoh@amazon.com> * Update aws/rust-runtime/aws-config/src/default_provider.rs Co-authored-by: Russell Cohen <rcoh@amazon.com> * Update aws/rust-runtime/aws-config/src/default_provider.rs Co-authored-by: Russell Cohen <rcoh@amazon.com> * Update aws/rust-runtime/aws-config/src/default_provider.rs Co-authored-by: Russell Cohen <rcoh@amazon.com> * feature: user-configurable AsyncSleep impls update: fallback to sleep impl that sleeps forever instead of sleep impl being optional * Update rust-runtime/aws-smithy-types/src/timeout.rs Co-authored-by: John DiSanti <jdisanti@amazon.com> * update: changelogs format: SleepImplDecorator.kt update: TimeoutConfig doc remove: list_of_set_timeouts in favor of Debug impl for TimeoutConfig * Apply suggestions from code review Co-authored-by: John DiSanti <jdisanti@amazon.com> * fix: broken macro doc test by ignoring it fix: outdated struct ref in doc fix: outdated generated doc * fix: mode broken doc tests * fix: broken doc test * attempt to fix CI-only doc test error * add: moduleUseName method to CodegenContext remove: pub use reexports from timeout and sleep impl decorators add: pub use reexports to aws_config for timeout and retry configs undo: default_sleep_impl changes attempt to add tokio time feature to S3 integration test * Update timeout.rs * Apply suggestions from code review Co-authored-by: John DiSanti <jdisanti@amazon.com> * refactor: consolidate timeout parsing logic rename: `RetryConfigBuilder::merge_with` to `RetryConfigBuilder::take_unset_from` refactor: move timeout parsing tests to timeouts.rs add: entry to SDK changelog noting the renaming remove: redundant feature from s3 integration test Cargo.toml update: various setters added by this PR to have the same form as our preexisting setters add: extra info to the warning emitted when ConfigLoader calls default_async_sleep and gets None * fix: tests broken when implementing suggestions * update: doc hide sleep_impl for aws_types::Config remove: leftover comment Co-authored-by: Russell Cohen <rcoh@amazon.com> Co-authored-by: John DiSanti <jdisanti@amazon.com>
This commit is contained in:
parent
cab7de3faf
commit
a9a691b6b4
|
@ -7,13 +7,17 @@ vNext (Month Day, Year)
|
|||
**Breaking Changes**
|
||||
- (aws-smithy-client): Extraneous `pub use SdkSuccess` removed from `aws_smithy_client::hyper_ext`. (smithy-rs#855)
|
||||
|
||||
**New this week**
|
||||
|
||||
- Timeouts for requests are now configurable. You can set separate timeouts for each individual request attempt and all attempts made for a request. (smithy-rs#831)
|
||||
|
||||
v0.29.0-alpha (November 11th, 2021)
|
||||
===================================
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
Several breaking changes around `aws_smithy_types::Instant` were introduced by smithy-rs#849:
|
||||
- `aws_smithy_types::Instant` from was renamed to `DateTime` to avoid confusion with the standard library's monotonically nondecreasing `Instant` type.
|
||||
- `aws_smithy_types::Instant` from was renamed to `DateTime` to avoid confusion with the standard library's monotonically non-decreasing `Instant` type.
|
||||
- `DateParseError` in `aws_smithy_types` has been renamed to `DateTimeParseError` to match the type that's being parsed.
|
||||
- The `chrono-conversions` feature and associated functions have been moved to the `aws-smithy-types-convert` crate.
|
||||
- Calls to `Instant::from_chrono` should be changed to:
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
vNext (Month Day, Year)
|
||||
=======================
|
||||
|
||||
**TODO Upon release**
|
||||
- Update README & aws-sdk-rust CI for MSRV upgrade to 1.54
|
||||
|
||||
**New this week**
|
||||
|
||||
- :tada: Timeouts for requests are now configurable. You can set a timeout for each individual request attempt or for all attempts made for a request. (smithy-rs#831)
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
- `RetryConfigBuilder::merge_with` has been renamed to `RetryConfigBuilder::take_unset_from`
|
||||
|
||||
v0.0.26-alpha (TBD)
|
||||
===================================
|
||||
**New this release**
|
||||
- Improve docs on `aws-smithy-client` (smithy-rs#855)
|
||||
|
||||
**Breaking Changes**
|
||||
- (aws-smithy-client): Extraneous `pub use SdkSuccess` removed from `aws_smithy_client::hyper_ext`. (smithy-rs#855)
|
||||
|
||||
|
||||
v0.0.26-alpha (TBD)
|
||||
=======================
|
||||
|
||||
**TODO Upon release**
|
||||
- Update README & aws-sdk-rust CI for MSRV upgrade to 1.54
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
- The `add_metadata` function was removed from `AwsUserAgent` in `aws-http`.
|
||||
|
|
|
@ -104,15 +104,26 @@ pub mod retry_config {
|
|||
///
|
||||
/// # Example
|
||||
///
|
||||
/// When running [`aws_config::from_env()`](crate::from_env()), a [`ConfigLoader`](crate::ConfigLoader)
|
||||
/// is created that will then create a [`RetryConfig`] from the default_provider. There is no
|
||||
/// need to call `default_provider` and the example below is only for illustration purposes.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::error::Error;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// use aws_config::default_provider::retry_config;
|
||||
/// // Creating a RetryConfig from the default_provider already happens when loading a config from_env
|
||||
/// // This is only for illustration purposes
|
||||
/// let retry_config = retry_config::default_provider().retry_config().await;
|
||||
/// let config = aws_config::from_env().retry_config(retry_config).load().await;
|
||||
///
|
||||
/// // Load a retry config from a specific profile
|
||||
/// let retry_config = retry_config::default_provider()
|
||||
/// .profile_name("other_profile")
|
||||
/// .retry_config()
|
||||
/// .await;
|
||||
/// let config = aws_config::from_env()
|
||||
/// // Override the retry config set by the default profile
|
||||
/// .retry_config(retry_config)
|
||||
/// .load()
|
||||
/// .await;
|
||||
/// // instantiate a service client:
|
||||
/// // <my_aws_service>::Client::new(&config);
|
||||
/// # Ok(())
|
||||
|
@ -130,7 +141,6 @@ pub mod retry_config {
|
|||
}
|
||||
|
||||
impl Builder {
|
||||
#[doc(hidden)]
|
||||
/// Configure the default chain
|
||||
///
|
||||
/// Exposed for overriding the environment when unit-testing providers
|
||||
|
@ -172,7 +182,9 @@ pub mod retry_config {
|
|||
Err(err) => panic!("{}", err),
|
||||
};
|
||||
|
||||
builder_from_env.merge_with(builder_from_profile).build()
|
||||
builder_from_env
|
||||
.take_unset_from(builder_from_profile)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -272,6 +284,122 @@ pub mod app_name {
|
|||
}
|
||||
}
|
||||
|
||||
/// Default timeout configuration provider chain
|
||||
pub mod timeout_config {
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
|
||||
use crate::environment::timeout_config::EnvironmentVariableTimeoutConfigProvider;
|
||||
use crate::profile;
|
||||
use crate::provider_config::ProviderConfig;
|
||||
|
||||
/// Default [`TimeoutConfig`] Provider chain
|
||||
///
|
||||
/// Unlike other credentials and region, [`TimeoutConfig`] has no related `TimeoutConfigProvider` trait. Instead,
|
||||
/// a builder struct is returned which has a similar API.
|
||||
///
|
||||
/// This provider will check the following sources in order:
|
||||
/// 1. [Environment variables](EnvironmentVariableTimeoutConfigProvider)
|
||||
/// 2. [Profile file](crate::profile::timeout_config::ProfileFileTimeoutConfigProvider) (`~/.aws/config`)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::error::Error;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// use aws_config::default_provider::timeout_config;
|
||||
///
|
||||
/// // Load a timeout config from a specific profile
|
||||
/// let timeout_config = timeout_config::default_provider()
|
||||
/// .profile_name("other_profile")
|
||||
/// .timeout_config()
|
||||
/// .await;
|
||||
/// let config = aws_config::from_env()
|
||||
/// // Override the timeout config set by the default profile
|
||||
/// .timeout_config(timeout_config)
|
||||
/// .load()
|
||||
/// .await;
|
||||
/// // instantiate a service client:
|
||||
/// // <my_aws_service>::Client::new(&config);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn default_provider() -> Builder {
|
||||
Builder::default()
|
||||
}
|
||||
|
||||
/// Builder for [`TimeoutConfig`] that checks the environment variables and AWS profile files for configuration
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
env_provider: EnvironmentVariableTimeoutConfigProvider,
|
||||
profile_file: profile::timeout_config::Builder,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Configure the default chain
|
||||
///
|
||||
/// Exposed for overriding the environment when unit-testing providers
|
||||
pub fn configure(mut self, configuration: &ProviderConfig) -> Self {
|
||||
self.env_provider =
|
||||
EnvironmentVariableTimeoutConfigProvider::new_with_env(configuration.env());
|
||||
self.profile_file = self.profile_file.configure(configuration);
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the profile name used by this provider
|
||||
pub fn profile_name(mut self, name: &str) -> Self {
|
||||
self.profile_file = self.profile_file.profile_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attempt to create a [`TimeoutConfig`](aws_smithy_types::timeout::TimeoutConfig) from following sources in order:
|
||||
/// 1. [Environment variables](crate::environment::timeout_config::EnvironmentVariableTimeoutConfigProvider)
|
||||
/// 2. [Profile file](crate::profile::timeout_config::ProfileFileTimeoutConfigProvider)
|
||||
///
|
||||
/// Precedence is considered on a per-field basis. If no timeout is specified, requests will never time out.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if:
|
||||
/// - a timeout is set to `NaN`, a negative number, or infinity
|
||||
/// - a timeout can't be parsed as a floating point number
|
||||
pub async fn timeout_config(self) -> TimeoutConfig {
|
||||
// Both of these can return errors due to invalid config settings and we want to surface those as early as possible
|
||||
// hence, we'll panic if any config values are invalid (missing values are OK though)
|
||||
// We match this instead of unwrapping so we can print the error with the `Display` impl instead of the `Debug` impl that unwrap uses
|
||||
let builder_from_env = match self.env_provider.timeout_config() {
|
||||
Ok(timeout_config_builder) => timeout_config_builder,
|
||||
Err(err) => panic!("{}", err),
|
||||
};
|
||||
let builder_from_profile = match self.profile_file.build().timeout_config().await {
|
||||
Ok(timeout_config_builder) => timeout_config_builder,
|
||||
Err(err) => panic!("{}", err),
|
||||
};
|
||||
|
||||
let conf = builder_from_env.take_unset_from(builder_from_profile);
|
||||
|
||||
if conf.tls_negotiation_timeout().is_some() {
|
||||
tracing::warn!(
|
||||
"A TLS negotiation timeout was set but that feature is currently unimplemented so the setting will be ignored. \
|
||||
To help us prioritize support for this feature, please upvote aws-sdk-rust#151 (https://github.com/awslabs/aws-sdk-rust/issues/151)")
|
||||
}
|
||||
|
||||
if conf.connect_timeout().is_some() {
|
||||
tracing::warn!(
|
||||
"A connect timeout was set but that feature is currently unimplemented so the setting will be ignored. \
|
||||
To help us prioritize support for this feature, please upvote aws-sdk-rust#151 (https://github.com/awslabs/aws-sdk-rust/issues/151)")
|
||||
}
|
||||
|
||||
if conf.read_timeout().is_some() {
|
||||
tracing::warn!(
|
||||
"A read timeout was set but that feature is currently unimplemented so the setting will be ignored. \
|
||||
To help us prioritize support for this feature, please upvote aws-sdk-rust#151 (https://github.com/awslabs/aws-sdk-rust/issues/151)")
|
||||
}
|
||||
|
||||
conf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default credentials provider chain
|
||||
pub mod credentials {
|
||||
use std::borrow::Cow;
|
||||
|
|
|
@ -9,6 +9,7 @@ pub use app_name::EnvironmentVariableAppNameProvider;
|
|||
/// Load credentials from the environment
|
||||
pub mod credentials;
|
||||
pub use credentials::EnvironmentVariableCredentialsProvider;
|
||||
|
||||
/// Load regions from the environment
|
||||
pub mod region;
|
||||
pub use region::EnvironmentVariableRegionProvider;
|
||||
|
@ -16,3 +17,7 @@ pub use region::EnvironmentVariableRegionProvider;
|
|||
/// Load retry behavior configuration from the environment
|
||||
pub mod retry_config;
|
||||
pub use retry_config::EnvironmentVariableRetryConfigProvider;
|
||||
|
||||
/// Load timeout configuration from the environment
|
||||
pub mod timeout_config;
|
||||
pub use timeout_config::EnvironmentVariableTimeoutConfigProvider;
|
||||
|
|
|
@ -14,15 +14,14 @@ const ENV_VAR_RETRY_MODE: &str = "AWS_RETRY_MODE";
|
|||
/// Load a retry_config from environment variables
|
||||
///
|
||||
/// This provider will check the values of `AWS_RETRY_MODE` and `AWS_MAX_ATTEMPTS`
|
||||
/// in order to build a retry config. If at least one is set to a valid value,
|
||||
/// construction will succeed
|
||||
/// in order to build a retry config.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvironmentVariableRetryConfigProvider {
|
||||
env: Env,
|
||||
}
|
||||
|
||||
impl EnvironmentVariableRetryConfigProvider {
|
||||
/// Create a new `EnvironmentVariableRetryConfigProvider`
|
||||
/// Create a new [`EnvironmentVariableRetryConfigProvider`]
|
||||
pub fn new() -> Self {
|
||||
EnvironmentVariableRetryConfigProvider { env: Env::real() }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
//! Load timeout configuration properties from environment variables
|
||||
|
||||
use aws_smithy_types::timeout::{parse_str_as_timeout, TimeoutConfig, TimeoutConfigError};
|
||||
use aws_types::os_shim_internal::Env;
|
||||
use std::time::Duration;
|
||||
|
||||
const ENV_VAR_CONNECT_TIMEOUT: &str = "AWS_CONNECT_TIMEOUT";
|
||||
const ENV_VAR_TLS_NEGOTIATION_TIMEOUT: &str = "AWS_TLS_NEGOTIATION_TIMEOUT";
|
||||
const ENV_VAR_READ_TIMEOUT: &str = "AWS_READ_TIMEOUT";
|
||||
const ENV_VAR_API_CALL_ATTEMPT_TIMEOUT: &str = "AWS_API_CALL_ATTEMPT_TIMEOUT";
|
||||
const ENV_VAR_API_CALL_TIMEOUT: &str = "AWS_API_CALL_TIMEOUT";
|
||||
|
||||
/// Load a timeout_config from environment variables
|
||||
///
|
||||
/// This provider will check the values of the following variables in order to build a `TimeoutConfig`
|
||||
///
|
||||
/// - `AWS_CONNECT_TIMEOUT`
|
||||
/// - `AWS_TLS_NEGOTIATION_TIMEOUT`
|
||||
/// - `AWS_READ_TIMEOUT`
|
||||
/// - `AWS_API_CALL_ATTEMPT_TIMEOUT`
|
||||
/// - `AWS_API_CALL_TIMEOUT`
|
||||
///
|
||||
/// Timeout values represent the number of seconds before timing out and must be non-negative floats
|
||||
/// or integers. NaN and infinity are also invalid.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvironmentVariableTimeoutConfigProvider {
|
||||
env: Env,
|
||||
}
|
||||
|
||||
impl EnvironmentVariableTimeoutConfigProvider {
|
||||
/// Create a new [`EnvironmentVariableTimeoutConfigProvider`]
|
||||
pub fn new() -> Self {
|
||||
EnvironmentVariableTimeoutConfigProvider { env: Env::real() }
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Create a timeout config provider from a given [`Env`]
|
||||
///
|
||||
/// This method is used for tests that need to override environment variables.
|
||||
pub fn new_with_env(env: Env) -> Self {
|
||||
EnvironmentVariableTimeoutConfigProvider { env }
|
||||
}
|
||||
|
||||
/// Attempt to create a new [`TimeoutConfig`] from environment variables
|
||||
pub fn timeout_config(&self) -> Result<TimeoutConfig, TimeoutConfigError> {
|
||||
let connect_timeout = construct_timeout_from_env_var(&self.env, ENV_VAR_CONNECT_TIMEOUT)?;
|
||||
let tls_negotiation_timeout =
|
||||
construct_timeout_from_env_var(&self.env, ENV_VAR_TLS_NEGOTIATION_TIMEOUT)?;
|
||||
let read_timeout = construct_timeout_from_env_var(&self.env, ENV_VAR_READ_TIMEOUT)?;
|
||||
let api_call_attempt_timeout =
|
||||
construct_timeout_from_env_var(&self.env, ENV_VAR_API_CALL_ATTEMPT_TIMEOUT)?;
|
||||
let api_call_timeout = construct_timeout_from_env_var(&self.env, ENV_VAR_API_CALL_TIMEOUT)?;
|
||||
|
||||
Ok(TimeoutConfig::new()
|
||||
.with_connect_timeout(connect_timeout)
|
||||
.with_tls_negotiation_timeout(tls_negotiation_timeout)
|
||||
.with_read_timeout(read_timeout)
|
||||
.with_api_call_attempt_timeout(api_call_attempt_timeout)
|
||||
.with_api_call_timeout(api_call_timeout))
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_timeout_from_env_var(
|
||||
env: &Env,
|
||||
var: &'static str,
|
||||
) -> Result<Option<Duration>, TimeoutConfigError> {
|
||||
match env.get(var).ok() {
|
||||
Some(timeout) => {
|
||||
parse_str_as_timeout(&timeout, var.into(), "environment variable".into()).map(Some)
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{
|
||||
EnvironmentVariableTimeoutConfigProvider, ENV_VAR_API_CALL_ATTEMPT_TIMEOUT,
|
||||
ENV_VAR_API_CALL_TIMEOUT, ENV_VAR_CONNECT_TIMEOUT, ENV_VAR_READ_TIMEOUT,
|
||||
ENV_VAR_TLS_NEGOTIATION_TIMEOUT,
|
||||
};
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use aws_types::os_shim_internal::Env;
|
||||
use std::time::Duration;
|
||||
|
||||
fn test_provider(vars: &[(&str, &str)]) -> EnvironmentVariableTimeoutConfigProvider {
|
||||
EnvironmentVariableTimeoutConfigProvider::new_with_env(Env::from_slice(vars))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_defaults() {
|
||||
let built = test_provider(&[]).timeout_config().unwrap();
|
||||
|
||||
assert_eq!(built.read_timeout(), None);
|
||||
assert_eq!(built.connect_timeout(), None);
|
||||
assert_eq!(built.tls_negotiation_timeout(), None);
|
||||
assert_eq!(built.api_call_attempt_timeout(), None);
|
||||
assert_eq!(built.api_call_timeout(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_fields_can_be_set_at_once() {
|
||||
assert_eq!(
|
||||
test_provider(&[
|
||||
(ENV_VAR_READ_TIMEOUT, "1.0"),
|
||||
(ENV_VAR_CONNECT_TIMEOUT, "2"),
|
||||
(ENV_VAR_TLS_NEGOTIATION_TIMEOUT, "3.0000"),
|
||||
(ENV_VAR_API_CALL_ATTEMPT_TIMEOUT, "04.000"),
|
||||
(ENV_VAR_API_CALL_TIMEOUT, "900012345.0")
|
||||
])
|
||||
.timeout_config()
|
||||
.unwrap(),
|
||||
TimeoutConfig::new()
|
||||
.with_read_timeout(Some(Duration::from_secs_f32(1.0)))
|
||||
.with_connect_timeout(Some(Duration::from_secs_f32(2.0)))
|
||||
.with_tls_negotiation_timeout(Some(Duration::from_secs_f32(3.0)))
|
||||
.with_api_call_attempt_timeout(Some(Duration::from_secs_f32(4.0)))
|
||||
// Some floats can't be represented as f32 so this duration will be equal to the
|
||||
// duration from the env.
|
||||
.with_api_call_timeout(Some(Duration::from_secs_f32(900012350.0)))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ use aws_smithy_http_tower::map_request::{
|
|||
AsyncMapRequestLayer, AsyncMapRequestService, MapRequestLayer, MapRequestService,
|
||||
};
|
||||
use aws_smithy_types::retry::{ErrorKind, RetryKind};
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use aws_types::os_shim_internal::{Env, Fs};
|
||||
use bytes::Bytes;
|
||||
use http::uri::InvalidUri;
|
||||
|
@ -543,19 +544,22 @@ impl Builder {
|
|||
let endpoint = Endpoint::immutable(endpoint);
|
||||
let retry_config = retry::Config::default()
|
||||
.with_max_attempts(self.max_attempts.unwrap_or(DEFAULT_ATTEMPTS));
|
||||
let timeout_config = TimeoutConfig::default();
|
||||
let token_loader = token::TokenMiddleware::new(
|
||||
connector.clone(),
|
||||
config.time_source(),
|
||||
endpoint.clone(),
|
||||
self.token_ttl.unwrap_or(DEFAULT_TOKEN_TTL),
|
||||
retry_config.clone(),
|
||||
timeout_config.clone(),
|
||||
);
|
||||
let middleware = ImdsMiddleware { token_loader };
|
||||
let inner_client = aws_smithy_client::Builder::new()
|
||||
.connector(connector.clone())
|
||||
.middleware(middleware)
|
||||
.build()
|
||||
.with_retry_config(retry_config);
|
||||
.with_retry_config(retry_config)
|
||||
.with_timeout_config(timeout_config);
|
||||
let client = Client {
|
||||
endpoint,
|
||||
inner: inner_client,
|
||||
|
|
|
@ -35,6 +35,7 @@ use http::{HeaderValue, Uri};
|
|||
use crate::cache::ExpiringCache;
|
||||
use crate::imds::client::{ImdsError, ImdsErrorPolicy, TokenError};
|
||||
use aws_smithy_client::retry;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
/// Token Refresh Buffer
|
||||
|
@ -82,9 +83,11 @@ impl TokenMiddleware {
|
|||
endpoint: Endpoint,
|
||||
token_ttl: Duration,
|
||||
retry_config: retry::Config,
|
||||
timeout_config: TimeoutConfig,
|
||||
) -> Self {
|
||||
let inner_client =
|
||||
aws_smithy_client::Client::new(connector).with_retry_config(retry_config);
|
||||
let inner_client = aws_smithy_client::Client::new(connector)
|
||||
.with_retry_config(retry_config)
|
||||
.with_timeout_config(timeout_config);
|
||||
let client = Arc::new(inner_client);
|
||||
Self {
|
||||
client,
|
||||
|
|
|
@ -89,6 +89,10 @@ mod json_credentials;
|
|||
#[cfg(feature = "http-provider")]
|
||||
mod http_provider;
|
||||
|
||||
// Re-export types from smithy-types
|
||||
pub use aws_smithy_types::retry::RetryConfig;
|
||||
pub use aws_smithy_types::timeout::TimeoutConfig;
|
||||
|
||||
// Re-export types from aws-types
|
||||
pub use aws_types::app_name::{AppName, InvalidAppName};
|
||||
pub use aws_types::config::Config;
|
||||
|
@ -121,13 +125,18 @@ pub use loader::ConfigLoader;
|
|||
|
||||
#[cfg(feature = "default-provider")]
|
||||
mod loader {
|
||||
use crate::default_provider::{app_name, credentials, region, retry_config};
|
||||
use crate::meta::region::ProvideRegion;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep};
|
||||
use aws_smithy_types::retry::RetryConfig;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use aws_types::app_name::AppName;
|
||||
use aws_types::config::Config;
|
||||
use aws_types::credentials::{ProvideCredentials, SharedCredentialsProvider};
|
||||
|
||||
use crate::default_provider::{app_name, credentials, region, retry_config, timeout_config};
|
||||
use crate::meta::region::ProvideRegion;
|
||||
|
||||
/// Load a cross-service [`Config`](aws_types::config::Config) from the environment
|
||||
///
|
||||
/// This builder supports overriding individual components of the generated config. Overriding a component
|
||||
|
@ -136,10 +145,12 @@ mod loader {
|
|||
/// chain will not be used.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ConfigLoader {
|
||||
region: Option<Box<dyn ProvideRegion>>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
app_name: Option<AppName>,
|
||||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
region: Option<Box<dyn ProvideRegion>>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
sleep: Option<Arc<dyn AsyncSleep>>,
|
||||
timeout_config: Option<TimeoutConfig>,
|
||||
}
|
||||
|
||||
impl ConfigLoader {
|
||||
|
@ -175,6 +186,35 @@ mod loader {
|
|||
self
|
||||
}
|
||||
|
||||
/// Override the timeout config used to build [`Config`](aws_types::config::Config).
|
||||
/// **Note: This only sets timeouts for calls to AWS services.** Timeouts for the credentials
|
||||
/// provider chain are configured separately.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use std::time::Duration;
|
||||
/// # use aws_smithy_types::timeout::TimeoutConfig;
|
||||
/// # async fn create_config() {
|
||||
/// let timeout_config = TimeoutConfig::new().with_api_call_timeout(Some(Duration::from_secs(1)));
|
||||
/// let config = aws_config::from_env()
|
||||
/// .timeout_config(timeout_config)
|
||||
/// .load()
|
||||
/// .await;
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn timeout_config(mut self, timeout_config: TimeoutConfig) -> Self {
|
||||
self.timeout_config = Some(timeout_config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the sleep implementation for this [`ConfigLoader`]. The sleep implementation
|
||||
/// is used to create timeout futures.
|
||||
pub fn sleep_impl(mut self, sleep: impl AsyncSleep + 'static) -> Self {
|
||||
// it's possible that we could wrapping an `Arc in an `Arc` and that's OK
|
||||
self.sleep = Some(Arc::new(sleep));
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the credentials provider used to build [`Config`](aws_types::config::Config).
|
||||
/// # Examples
|
||||
/// Override the credentials provider but load the default value for region:
|
||||
|
@ -222,6 +262,27 @@ mod loader {
|
|||
app_name::default_provider().app_name().await
|
||||
};
|
||||
|
||||
let timeout_config = if let Some(timeout_config) = self.timeout_config {
|
||||
timeout_config
|
||||
} else {
|
||||
timeout_config::default_provider().timeout_config().await
|
||||
};
|
||||
|
||||
let sleep_impl = if self.sleep.is_none() {
|
||||
if default_async_sleep().is_none() {
|
||||
tracing::warn!(
|
||||
"An implementation of AsyncSleep was requested by calling default_async_sleep \
|
||||
but no default was set.
|
||||
This happened when ConfigLoader::load was called during Config construction. \
|
||||
You can fix this by setting a sleep_impl on the ConfigLoader before calling \
|
||||
load or by enabling the rt-tokio feature"
|
||||
);
|
||||
}
|
||||
default_async_sleep()
|
||||
} else {
|
||||
self.sleep
|
||||
};
|
||||
|
||||
let credentials_provider = if let Some(provider) = self.credentials_provider {
|
||||
provider
|
||||
} else {
|
||||
|
@ -233,8 +294,11 @@ mod loader {
|
|||
let mut builder = Config::builder()
|
||||
.region(region)
|
||||
.retry_config(retry_config)
|
||||
.timeout_config(timeout_config)
|
||||
.credentials_provider(credentials_provider);
|
||||
|
||||
builder.set_app_name(app_name);
|
||||
builder.set_sleep_impl(sleep_impl);
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ pub mod app_name;
|
|||
pub mod credentials;
|
||||
pub mod region;
|
||||
pub mod retry_config;
|
||||
pub mod timeout_config;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use credentials::ProfileFileCredentialsProvider;
|
||||
|
|
|
@ -19,13 +19,13 @@ use crate::provider_config::ProviderConfig;
|
|||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// **Loads 2 as the `max_attempts` to make when sending a request
|
||||
/// **Loads 2 as the `max_attempts` to make when sending a request**
|
||||
/// ```ini
|
||||
/// [default]
|
||||
/// max_attempts = 2
|
||||
/// ```
|
||||
///
|
||||
/// **Loads `standard` as the `retry_mode` _if and only if_ the `other` profile is selected.
|
||||
/// **Loads `standard` as the `retry_mode` _if and only if_ the `other` profile is selected.**
|
||||
///
|
||||
/// ```ini
|
||||
/// [profile other]
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
//! Load timeout configuration properties from an AWS profile
|
||||
|
||||
use crate::profile::Profile;
|
||||
use crate::provider_config::ProviderConfig;
|
||||
use aws_smithy_types::timeout::{parse_str_as_timeout, TimeoutConfig, TimeoutConfigError};
|
||||
use aws_types::os_shim_internal::{Env, Fs};
|
||||
use std::time::Duration;
|
||||
|
||||
const PROFILE_VAR_CONNECT_TIMEOUT: &str = "connect_timeout";
|
||||
const PROFILE_VAR_TLS_NEGOTIATION_TIMEOUT: &str = "tls_negotiation_timeout";
|
||||
const PROFILE_VAR_READ_TIMEOUT: &str = "read_timeout";
|
||||
const PROFILE_VAR_API_CALL_ATTEMPT_TIMEOUT: &str = "api_call_attempt_timeout";
|
||||
const PROFILE_VAR_API_CALL_TIMEOUT: &str = "api_call_timeout";
|
||||
|
||||
/// Load timeout configuration properties from a profile file
|
||||
///
|
||||
/// This provider will attempt to load AWS shared configuration, then read timeout configuration
|
||||
/// properties from the active profile. Timeout values represent the number of seconds before timing
|
||||
/// out and must be non-negative floats or integers. NaN and infinity are also invalid. If at least
|
||||
/// one of these values is valid, construction will succeed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// **Sets timeouts for the `default` profile**
|
||||
/// ```ini
|
||||
/// [default]
|
||||
/// connect_timeout = 1.0
|
||||
/// read_timeout = 1.0
|
||||
/// tls_negotiation_timeout = 0.5
|
||||
/// api_call_attempt_timeout = 2
|
||||
/// api_call_timeout = 3
|
||||
/// ```
|
||||
///
|
||||
/// **Sets the `connect_timeout` to 0.5 seconds _if and only if_ the `other` profile is selected.**
|
||||
///
|
||||
/// ```ini
|
||||
/// [profile other]
|
||||
/// connect_timeout = 0.5
|
||||
/// ```
|
||||
///
|
||||
/// This provider is part of the [default timeout config provider chain](crate::default_provider::timeout_config).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProfileFileTimeoutConfigProvider {
|
||||
fs: Fs,
|
||||
env: Env,
|
||||
profile_override: Option<String>,
|
||||
}
|
||||
|
||||
/// Builder for [`ProfileFileTimeoutConfigProvider`]
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
config: Option<ProviderConfig>,
|
||||
profile_override: Option<String>,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Override the configuration for this provider
|
||||
pub fn configure(mut self, config: &ProviderConfig) -> Self {
|
||||
self.config = Some(config.clone());
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the profile name used by the [`ProfileFileTimeoutConfigProvider`]
|
||||
pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
|
||||
self.profile_override = Some(profile_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build a [`ProfileFileTimeoutConfigProvider`] from this builder
|
||||
pub fn build(self) -> ProfileFileTimeoutConfigProvider {
|
||||
let conf = self.config.unwrap_or_default();
|
||||
ProfileFileTimeoutConfigProvider {
|
||||
env: conf.env(),
|
||||
fs: conf.fs(),
|
||||
profile_override: self.profile_override,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileFileTimeoutConfigProvider {
|
||||
/// Create a new [`ProfileFileTimeoutConfigProvider`]
|
||||
///
|
||||
/// To override the selected profile, set the `AWS_PROFILE` environment variable or use the [`Builder`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
fs: Fs::real(),
|
||||
env: Env::real(),
|
||||
profile_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Builder`] to construct a [`ProfileFileTimeoutConfigProvider`]
|
||||
pub fn builder() -> Builder {
|
||||
Builder::default()
|
||||
}
|
||||
|
||||
/// Attempt to create a new [`TimeoutConfig`] from a profile file.
|
||||
pub async fn timeout_config(&self) -> Result<TimeoutConfig, TimeoutConfigError> {
|
||||
let profile = match super::parser::load(&self.fs, &self.env).await {
|
||||
Ok(profile) => profile,
|
||||
Err(err) => {
|
||||
tracing::warn!(err = %err, "failed to parse profile, skipping it");
|
||||
// return an empty builder
|
||||
return Ok(Default::default());
|
||||
}
|
||||
};
|
||||
|
||||
let selected_profile = self
|
||||
.profile_override
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| profile.selected_profile());
|
||||
let selected_profile = match profile.get_profile(selected_profile) {
|
||||
Some(profile) => profile,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"failed to get selected '{}' profile, skipping it",
|
||||
selected_profile
|
||||
);
|
||||
// return an empty config
|
||||
return Ok(TimeoutConfig::new());
|
||||
}
|
||||
};
|
||||
|
||||
let connect_timeout =
|
||||
construct_timeout_from_profile_var(selected_profile, PROFILE_VAR_CONNECT_TIMEOUT)?;
|
||||
let tls_negotiation_timeout = construct_timeout_from_profile_var(
|
||||
selected_profile,
|
||||
PROFILE_VAR_TLS_NEGOTIATION_TIMEOUT,
|
||||
)?;
|
||||
let read_timeout =
|
||||
construct_timeout_from_profile_var(selected_profile, PROFILE_VAR_READ_TIMEOUT)?;
|
||||
let api_call_attempt_timeout = construct_timeout_from_profile_var(
|
||||
selected_profile,
|
||||
PROFILE_VAR_API_CALL_ATTEMPT_TIMEOUT,
|
||||
)?;
|
||||
let api_call_timeout =
|
||||
construct_timeout_from_profile_var(selected_profile, PROFILE_VAR_API_CALL_TIMEOUT)?;
|
||||
|
||||
Ok(TimeoutConfig::new()
|
||||
.with_connect_timeout(connect_timeout)
|
||||
.with_tls_negotiation_timeout(tls_negotiation_timeout)
|
||||
.with_read_timeout(read_timeout)
|
||||
.with_api_call_attempt_timeout(api_call_attempt_timeout)
|
||||
.with_api_call_timeout(api_call_timeout))
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_timeout_from_profile_var(
|
||||
profile: &Profile,
|
||||
var: &'static str,
|
||||
) -> Result<Option<Duration>, TimeoutConfigError> {
|
||||
let profile_name = format!("aws profile [{}]", profile.name());
|
||||
match profile.get(var) {
|
||||
Some(timeout) => parse_str_as_timeout(timeout, var.into(), profile_name.into()).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
#![warn(
|
||||
missing_docs,
|
||||
missing_crate_level_docs,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
missing_debug_implementations,
|
||||
rust_2018_idioms,
|
||||
unreachable_pub
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
#![warn(
|
||||
missing_docs,
|
||||
missing_crate_level_docs,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
missing_debug_implementations,
|
||||
rust_2018_idioms,
|
||||
unreachable_pub
|
||||
|
|
|
@ -9,26 +9,35 @@
|
|||
//!
|
||||
//! This module contains an shared configuration representation that is agnostic from a specific service.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use aws_smithy_async::rt::sleep::AsyncSleep;
|
||||
use aws_smithy_types::retry::RetryConfig;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
|
||||
use crate::app_name::AppName;
|
||||
use crate::credentials::SharedCredentialsProvider;
|
||||
use crate::region::Region;
|
||||
use aws_smithy_types::retry::RetryConfig;
|
||||
|
||||
/// AWS Shared Configuration
|
||||
pub struct Config {
|
||||
app_name: Option<AppName>,
|
||||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
region: Option<Region>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
app_name: Option<AppName>,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
timeout_config: Option<TimeoutConfig>,
|
||||
}
|
||||
|
||||
/// Builder for AWS Shared Configuration
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
app_name: Option<AppName>,
|
||||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
region: Option<Region>,
|
||||
retry_config: Option<RetryConfig>,
|
||||
credentials_provider: Option<SharedCredentialsProvider>,
|
||||
app_name: Option<AppName>,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
timeout_config: Option<TimeoutConfig>,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
|
@ -102,6 +111,107 @@ impl Builder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the [`TimeoutConfig`] for the builder
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::time::Duration;
|
||||
/// use aws_types::config::Config;
|
||||
/// use aws_smithy_types::timeout::TimeoutConfig;
|
||||
///
|
||||
/// let timeout_config = TimeoutConfig::new()
|
||||
/// .with_api_call_attempt_timeout(Some(Duration::from_secs(1)));
|
||||
/// let config = Config::builder().timeout_config(timeout_config).build();
|
||||
/// ```
|
||||
pub fn timeout_config(mut self, timeout_config: TimeoutConfig) -> Self {
|
||||
self.set_timeout_config(Some(timeout_config));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`TimeoutConfig`] for the builder
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use std::time::Duration;
|
||||
/// use aws_types::config::{Config, Builder};
|
||||
/// use aws_smithy_types::timeout::TimeoutConfig;
|
||||
///
|
||||
/// fn set_preferred_timeouts(builder: &mut Builder) {
|
||||
/// let timeout_config = TimeoutConfig::new()
|
||||
/// .with_api_call_attempt_timeout(Some(Duration::from_secs(2)))
|
||||
/// .with_api_call_timeout(Some(Duration::from_secs(5)));
|
||||
/// builder.set_timeout_config(Some(timeout_config));
|
||||
/// }
|
||||
///
|
||||
/// let mut builder = Config::builder();
|
||||
/// set_preferred_timeouts(&mut builder);
|
||||
/// let config = builder.build();
|
||||
/// ```
|
||||
pub fn set_timeout_config(&mut self, timeout_config: Option<TimeoutConfig>) -> &mut Self {
|
||||
self.timeout_config = timeout_config;
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Set the sleep implementation for the builder. The sleep implementation is used to create
|
||||
/// timeout futures.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::sync::Arc;
|
||||
/// use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep};
|
||||
/// use aws_types::config::Config;
|
||||
///
|
||||
/// ##[derive(Debug)]
|
||||
/// pub struct ForeverSleep;
|
||||
///
|
||||
/// impl AsyncSleep for ForeverSleep {
|
||||
/// fn sleep(&self, duration: std::time::Duration) -> Sleep {
|
||||
/// Sleep::new(std::future::pending())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let sleep_impl = Arc::new(ForeverSleep);
|
||||
/// let config = Config::builder().sleep_impl(sleep_impl).build();
|
||||
/// ```
|
||||
pub fn sleep_impl(mut self, sleep_impl: Arc<dyn AsyncSleep>) -> Self {
|
||||
self.set_sleep_impl(Some(sleep_impl));
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Set the sleep implementation for the builder. The sleep implementation is used to create
|
||||
/// timeout futures.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep};
|
||||
/// # use aws_types::config::{Builder, Config};
|
||||
/// #[derive(Debug)]
|
||||
/// pub struct ForeverSleep;
|
||||
///
|
||||
/// impl AsyncSleep for ForeverSleep {
|
||||
/// fn sleep(&self, duration: std::time::Duration) -> Sleep {
|
||||
/// Sleep::new(std::future::pending())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn set_never_ending_sleep_impl(builder: &mut Builder) {
|
||||
/// let sleep_impl = std::sync::Arc::new(ForeverSleep);
|
||||
/// builder.set_sleep_impl(Some(sleep_impl));
|
||||
/// }
|
||||
///
|
||||
/// let mut builder = Config::builder();
|
||||
/// set_never_ending_sleep_impl(&mut builder);
|
||||
/// let config = builder.build();
|
||||
/// ```
|
||||
pub fn set_sleep_impl(&mut self, sleep_impl: Option<Arc<dyn AsyncSleep>>) -> &mut Self {
|
||||
self.sleep_impl = sleep_impl;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the credentials provider for the builder
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -175,10 +285,12 @@ impl Builder {
|
|||
/// Build a [`Config`](Config) from this builder
|
||||
pub fn build(self) -> Config {
|
||||
Config {
|
||||
app_name: self.app_name,
|
||||
credentials_provider: self.credentials_provider,
|
||||
region: self.region,
|
||||
retry_config: self.retry_config,
|
||||
credentials_provider: self.credentials_provider,
|
||||
app_name: self.app_name,
|
||||
sleep_impl: self.sleep_impl,
|
||||
timeout_config: self.timeout_config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +306,17 @@ impl Config {
|
|||
self.retry_config.as_ref()
|
||||
}
|
||||
|
||||
/// Configured timeout config
|
||||
pub fn timeout_config(&self) -> Option<&TimeoutConfig> {
|
||||
self.timeout_config.as_ref()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Configured sleep implementation
|
||||
pub fn sleep_impl(&self) -> Option<Arc<dyn AsyncSleep>> {
|
||||
self.sleep_impl.clone()
|
||||
}
|
||||
|
||||
/// Configured credentials provider
|
||||
pub fn credentials_provider(&self) -> Option<&SharedCredentialsProvider> {
|
||||
self.credentials_provider.as_ref()
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
|
||||
package software.amazon.smithy.rustsdk
|
||||
|
||||
import software.amazon.smithy.rust.codegen.smithy.RetryConfigDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.DocsRsMetadataDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.DocsRsMetadataSettings
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.RetryConfigDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.SleepImplDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.TimeoutConfigDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecorator
|
||||
import software.amazon.smithy.rustsdk.customize.apigateway.ApiGatewayDecorator
|
||||
import software.amazon.smithy.rustsdk.customize.auth.DisabledAuthDecorator
|
||||
|
@ -33,6 +35,8 @@ val DECORATORS = listOf(
|
|||
|
||||
// Smithy specific decorators
|
||||
RetryConfigDecorator(),
|
||||
SleepImplDecorator(),
|
||||
TimeoutConfigDecorator(),
|
||||
|
||||
// Service specific decorators
|
||||
DisabledAuthDecorator(),
|
||||
|
|
|
@ -101,7 +101,13 @@ private class AwsFluentClientExtensions(private val types: Types) {
|
|||
/// Creates a client with the given service config and connector override.
|
||||
pub fn from_conf_conn(conf: crate::Config, conn: C) -> Self {
|
||||
let retry_config = conf.retry_config.as_ref().cloned().unwrap_or_default();
|
||||
let client = #{aws_hyper}::Client::new(conn).with_retry_config(retry_config.into());
|
||||
let timeout_config = conf.timeout_config.as_ref().cloned().unwrap_or_default();
|
||||
let sleep_impl = conf.sleep_impl.clone();
|
||||
let mut client = #{aws_hyper}::Client::new(conn)
|
||||
.with_retry_config(retry_config.into())
|
||||
.with_timeout_config(timeout_config);
|
||||
|
||||
client.set_sleep_impl(sleep_impl);
|
||||
Self { handle: std::sync::Arc::new(Handle { client, conf }) }
|
||||
}
|
||||
""",
|
||||
|
@ -121,7 +127,13 @@ private class AwsFluentClientExtensions(private val types: Types) {
|
|||
##[cfg(any(feature = "rustls", feature = "native-tls"))]
|
||||
pub fn from_conf(conf: crate::Config) -> Self {
|
||||
let retry_config = conf.retry_config.as_ref().cloned().unwrap_or_default();
|
||||
let client = #{aws_hyper}::Client::https().with_retry_config(retry_config.into());
|
||||
let timeout_config = conf.timeout_config.as_ref().cloned().unwrap_or_default();
|
||||
let sleep_impl = conf.sleep_impl.clone();
|
||||
let mut client = #{aws_hyper}::Client::https()
|
||||
.with_retry_config(retry_config.into())
|
||||
.with_timeout_config(timeout_config);
|
||||
|
||||
client.set_sleep_impl(sleep_impl);
|
||||
Self { handle: std::sync::Arc::new(Handle { client, conf }) }
|
||||
}
|
||||
""",
|
||||
|
|
|
@ -47,6 +47,8 @@ class SharedConfigDecorator : RustCodegenDecorator {
|
|||
let mut builder = Builder::default();
|
||||
builder = builder.region(input.region().cloned());
|
||||
builder.set_retry_config(input.retry_config().cloned());
|
||||
builder.set_timeout_config(input.timeout_config().cloned());
|
||||
builder.set_sleep_impl(input.sleep_impl().clone());
|
||||
builder.set_credentials_provider(input.credentials_provider().cloned());
|
||||
builder.set_app_name(input.app_name().cloned());
|
||||
builder
|
||||
|
|
|
@ -11,7 +11,10 @@ edition = "2018"
|
|||
aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3" }
|
||||
aws-smithy-client = { path = "../../build/aws-sdk/sdk/aws-smithy-client", features = ["test-util"] }
|
||||
aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" }
|
||||
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" }
|
||||
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }
|
||||
tracing-subscriber = "0.2.18"
|
||||
tokio = { version = "1", features = ["full"]}
|
||||
|
||||
[dev-dependencies]
|
||||
aws-http = { path = "../../build/aws-sdk/sdk/aws-http"}
|
||||
|
@ -19,4 +22,3 @@ aws-hyper = { path = "../../build/aws-sdk/sdk/aws-hyper"}
|
|||
bytes = "1"
|
||||
http = "0.2.3"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"]}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
use aws_sdk_s3::model::{
|
||||
CompressionType, CsvInput, CsvOutput, ExpressionType, FileHeaderInfo, InputSerialization,
|
||||
OutputSerialization,
|
||||
};
|
||||
use aws_sdk_s3::{Client, Config, Credentials, Region};
|
||||
use aws_smithy_async::assert_elapsed;
|
||||
use aws_smithy_async::rt::sleep::{AsyncSleep, TokioSleep};
|
||||
use aws_smithy_client::never::NeverService;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::result::ConnectorError;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timeout_service_ends_request_that_never_completes() {
|
||||
let conn: NeverService<http::Request<SdkBody>, http::Response<SdkBody>, ConnectorError> =
|
||||
NeverService::new();
|
||||
let region = Region::from_static("us-east-2");
|
||||
let credentials = Credentials::from_keys("test", "test", None);
|
||||
let timeout_config =
|
||||
TimeoutConfig::new().with_api_call_timeout(Some(Duration::from_secs_f32(0.5)));
|
||||
let sleep_impl: Arc<dyn AsyncSleep> = Arc::new(TokioSleep::new());
|
||||
let config = Config::builder()
|
||||
.region(region)
|
||||
.credentials_provider(credentials)
|
||||
.timeout_config(timeout_config)
|
||||
.sleep_impl(sleep_impl)
|
||||
.build();
|
||||
let client = Client::from_conf_conn(config, conn.clone());
|
||||
|
||||
let now = tokio::time::Instant::now();
|
||||
tokio::time::pause();
|
||||
|
||||
let err = client
|
||||
.select_object_content()
|
||||
.bucket("aws-rust-sdk")
|
||||
.key("sample_data.csv")
|
||||
.expression_type(ExpressionType::Sql)
|
||||
.expression("SELECT * FROM s3object s WHERE s.\"Name\" = 'Jane'")
|
||||
.input_serialization(
|
||||
InputSerialization::builder()
|
||||
.csv(
|
||||
CsvInput::builder()
|
||||
.file_header_info(FileHeaderInfo::Use)
|
||||
.build(),
|
||||
)
|
||||
.compression_type(CompressionType::None)
|
||||
.build(),
|
||||
)
|
||||
.output_serialization(
|
||||
OutputSerialization::builder()
|
||||
.csv(CsvOutput::builder().build())
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(format!("{:?}", err), "ConstructionFailure(TimedOutError)");
|
||||
assert_elapsed!(now, std::time::Duration::from_secs_f32(0.5));
|
||||
}
|
|
@ -64,4 +64,12 @@ data class CodegenContext(
|
|||
settings: RustSettings,
|
||||
mode: CodegenMode,
|
||||
) : this(model, symbolProvider, settings.runtimeConfig, serviceShape, protocol, settings.moduleName, settings, mode)
|
||||
|
||||
/**
|
||||
* A moduleName for a crate uses kebab-case. When you want to `use` a crate in Rust code,
|
||||
* it must be in snake-case. Call this method to get this crate's name in snake-case.
|
||||
*/
|
||||
fun moduleUseName(): String {
|
||||
return this.moduleName.replace("-", "_")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,15 @@
|
|||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.smithy
|
||||
package software.amazon.smithy.rust.codegen.smithy.customizations
|
||||
|
||||
import software.amazon.smithy.rust.codegen.rustlang.Writable
|
||||
import software.amazon.smithy.rust.codegen.rustlang.rust
|
||||
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
|
||||
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
|
||||
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
|
||||
|
@ -61,7 +64,6 @@ fn test_1() {
|
|||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Config>();
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
class RetryConfigDecorator : RustCodegenDecorator {
|
||||
|
@ -85,8 +87,7 @@ class RetryConfigDecorator : RustCodegenDecorator {
|
|||
|
||||
class RetryConfigProviderConfig(codegenContext: CodegenContext) : ConfigCustomization() {
|
||||
private val retryConfig = smithyTypesRetry(codegenContext.runtimeConfig)
|
||||
private val moduleName = codegenContext.moduleName
|
||||
private val moduleUseName = moduleName.replace("-", "_")
|
||||
private val moduleUseName = codegenContext.moduleUseName()
|
||||
private val codegenScope = arrayOf("RetryConfig" to retryConfig.member("RetryConfig"))
|
||||
override fun section(section: ServiceConfig) = writable {
|
||||
when (section) {
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.smithy.customizations
|
||||
|
||||
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
|
||||
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.generators.config.ConfigCustomization
|
||||
import software.amazon.smithy.rust.codegen.smithy.generators.config.ServiceConfig
|
||||
|
||||
/* Example Generated Code */
|
||||
/*
|
||||
pub struct Config {
|
||||
pub(crate) sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
}
|
||||
impl std::fmt::Debug for Config {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut config = f.debug_struct("Config");
|
||||
config.finish()
|
||||
}
|
||||
}
|
||||
impl Config {
|
||||
pub fn builder() -> Builder {
|
||||
Builder::default()
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
}
|
||||
impl Builder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn sleep_impl(mut self, sleep_impl: Arc<dyn AsyncSleep>) -> Self {
|
||||
self.set_sleep_impl(Some(sleep_impl));
|
||||
self
|
||||
}
|
||||
pub fn set_sleep_impl(
|
||||
&mut self,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
) -> &mut Self {
|
||||
self.sleep_impl = sleep_impl;
|
||||
self
|
||||
}
|
||||
pub fn build(self) -> Config {
|
||||
Config {
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_1() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Config>();
|
||||
}
|
||||
*/
|
||||
|
||||
class SleepImplDecorator : RustCodegenDecorator {
|
||||
override val name: String = "AsyncSleep"
|
||||
override val order: Byte = 0
|
||||
|
||||
override fun configCustomizations(
|
||||
codegenContext: CodegenContext,
|
||||
baseCustomizations: List<ConfigCustomization>
|
||||
): List<ConfigCustomization> {
|
||||
return baseCustomizations + SleepImplProviderConfig(codegenContext)
|
||||
}
|
||||
}
|
||||
|
||||
class SleepImplProviderConfig(codegenContext: CodegenContext) : ConfigCustomization() {
|
||||
private val sleepModule = smithyAsyncRtSleep(codegenContext.runtimeConfig)
|
||||
private val moduleUseName = codegenContext.moduleUseName()
|
||||
private val codegenScope =
|
||||
arrayOf("AsyncSleep" to sleepModule.member("AsyncSleep"), "Sleep" to sleepModule.member("Sleep"))
|
||||
|
||||
override fun section(section: ServiceConfig) = writable {
|
||||
when (section) {
|
||||
is ServiceConfig.ConfigStruct -> rustTemplate(
|
||||
"pub(crate) sleep_impl: Option<std::sync::Arc<dyn #{AsyncSleep}>>,",
|
||||
*codegenScope
|
||||
)
|
||||
is ServiceConfig.ConfigImpl -> emptySection
|
||||
is ServiceConfig.BuilderStruct ->
|
||||
rustTemplate("sleep_impl: Option<std::sync::Arc<dyn #{AsyncSleep}>>,", *codegenScope)
|
||||
ServiceConfig.BuilderImpl ->
|
||||
rustTemplate(
|
||||
"""
|
||||
/// Set the sleep_impl for the builder
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// use $moduleUseName::config::Config;
|
||||
/// use #{AsyncSleep};
|
||||
/// use #{Sleep};
|
||||
///
|
||||
/// ##[derive(Debug)]
|
||||
/// pub struct ForeverSleep;
|
||||
///
|
||||
/// impl AsyncSleep for ForeverSleep {
|
||||
/// fn sleep(&self, duration: std::time::Duration) -> Sleep {
|
||||
/// Sleep::new(std::future::pending())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let sleep_impl = std::sync::Arc::new(ForeverSleep);
|
||||
/// let config = Config::builder().sleep_impl(sleep_impl).build();
|
||||
/// ```
|
||||
pub fn sleep_impl(mut self, sleep_impl: std::sync::Arc<dyn #{AsyncSleep}>) -> Self {
|
||||
self.set_sleep_impl(Some(sleep_impl));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sleep_impl for the builder
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// use $moduleUseName::config::{Builder, Config};
|
||||
/// use #{AsyncSleep};
|
||||
/// use #{Sleep};
|
||||
///
|
||||
/// ##[derive(Debug)]
|
||||
/// pub struct ForeverSleep;
|
||||
///
|
||||
/// impl AsyncSleep for ForeverSleep {
|
||||
/// fn sleep(&self, duration: std::time::Duration) -> Sleep {
|
||||
/// Sleep::new(std::future::pending())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn set_never_ending_sleep_impl(builder: &mut Builder) {
|
||||
/// let sleep_impl = std::sync::Arc::new(ForeverSleep);
|
||||
/// builder.set_sleep_impl(Some(sleep_impl));
|
||||
/// }
|
||||
///
|
||||
/// let mut builder = Config::builder();
|
||||
/// set_never_ending_sleep_impl(&mut builder);
|
||||
/// let config = builder.build();
|
||||
/// ```
|
||||
pub fn set_sleep_impl(&mut self, sleep_impl: Option<std::sync::Arc<dyn #{AsyncSleep}>>) -> &mut Self {
|
||||
self.sleep_impl = sleep_impl;
|
||||
self
|
||||
}
|
||||
""",
|
||||
*codegenScope
|
||||
)
|
||||
ServiceConfig.BuilderBuild -> rustTemplate(
|
||||
"""sleep_impl: self.sleep_impl,""",
|
||||
*codegenScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate path to the root module in aws_smithy_async
|
||||
fun smithyAsyncRtSleep(runtimeConfig: RuntimeConfig) =
|
||||
RuntimeType("sleep", runtimeConfig.runtimeCrate("async"), "aws_smithy_async::rt")
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.smithy.customizations
|
||||
|
||||
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
|
||||
import software.amazon.smithy.rust.codegen.rustlang.writable
|
||||
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
|
||||
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
|
||||
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
|
||||
import software.amazon.smithy.rust.codegen.smithy.generators.config.ConfigCustomization
|
||||
import software.amazon.smithy.rust.codegen.smithy.generators.config.ServiceConfig
|
||||
|
||||
/* Example Generated Code */
|
||||
/*
|
||||
pub struct Config {
|
||||
pub(crate) timeout_config: Option<aws_smithy_types::timeout::TimeoutConfig>,
|
||||
}
|
||||
impl std::fmt::Debug for Config {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut config = f.debug_struct("Config");
|
||||
config.finish()
|
||||
}
|
||||
}
|
||||
impl Config {
|
||||
pub fn builder() -> Builder {
|
||||
Builder::default()
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
timeout_config: Option<aws_smithy_types::timeout::TimeoutConfig>,
|
||||
}
|
||||
impl Builder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn timeout_config(mut self, timeout_config: aws_smithy_types::timeout::TimeoutConfig) -> Self {
|
||||
self.set_timeout_config(Some(timeout_config));
|
||||
self
|
||||
}
|
||||
pub fn set_timeout_config(
|
||||
&mut self,
|
||||
timeout_config: Option<aws_smithy_types::timeout::TimeoutConfig>,
|
||||
) -> &mut Self {
|
||||
self.timeout_config = timeout_config;
|
||||
self
|
||||
}
|
||||
pub fn build(self) -> Config {
|
||||
Config {
|
||||
timeout_config: self.timeout_config,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_1() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Config>();
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
class TimeoutConfigDecorator : RustCodegenDecorator {
|
||||
override val name: String = "TimeoutConfig"
|
||||
override val order: Byte = 0
|
||||
|
||||
override fun configCustomizations(
|
||||
codegenContext: CodegenContext,
|
||||
baseCustomizations: List<ConfigCustomization>
|
||||
): List<ConfigCustomization> {
|
||||
return baseCustomizations + TimeoutConfigProviderConfig(codegenContext)
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutConfigProviderConfig(codegenContext: CodegenContext) : ConfigCustomization() {
|
||||
private val timeoutConfig = smithyTypesTimeout(codegenContext.runtimeConfig)
|
||||
private val moduleUseName = codegenContext.moduleUseName()
|
||||
private val codegenScope = arrayOf("TimeoutConfig" to timeoutConfig.member("TimeoutConfig"))
|
||||
override fun section(section: ServiceConfig) = writable {
|
||||
when (section) {
|
||||
is ServiceConfig.ConfigStruct -> rustTemplate(
|
||||
"pub(crate) timeout_config: Option<#{TimeoutConfig}>,",
|
||||
*codegenScope
|
||||
)
|
||||
is ServiceConfig.ConfigImpl -> emptySection
|
||||
is ServiceConfig.BuilderStruct ->
|
||||
rustTemplate("timeout_config: Option<#{TimeoutConfig}>,", *codegenScope)
|
||||
ServiceConfig.BuilderImpl ->
|
||||
rustTemplate(
|
||||
"""
|
||||
/// Set the timeout_config for the builder
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// ## use std::time::Duration;
|
||||
/// use $moduleUseName::config::Config;
|
||||
/// use #{TimeoutConfig};
|
||||
///
|
||||
/// let timeout_config = TimeoutConfig::new()
|
||||
/// .with_api_call_attempt_timeout(Some(Duration::from_secs(1)));
|
||||
/// let config = Config::builder().timeout_config(timeout_config).build();
|
||||
/// ```
|
||||
pub fn timeout_config(mut self, timeout_config: #{TimeoutConfig}) -> Self {
|
||||
self.set_timeout_config(Some(timeout_config));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout_config for the builder
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// ## use std::time::Duration;
|
||||
/// use $moduleUseName::config::{Builder, Config};
|
||||
/// use #{TimeoutConfig};
|
||||
///
|
||||
/// fn set_request_timeout(builder: &mut Builder) {
|
||||
/// let timeout_config = TimeoutConfig::new()
|
||||
/// .with_api_call_timeout(Some(Duration::from_secs(3)));
|
||||
/// builder.set_timeout_config(Some(timeout_config));
|
||||
/// }
|
||||
///
|
||||
/// let mut builder = Config::builder();
|
||||
/// set_request_timeout(&mut builder);
|
||||
/// let config = builder.build();
|
||||
/// ```
|
||||
pub fn set_timeout_config(&mut self, timeout_config: Option<#{TimeoutConfig}>) -> &mut Self {
|
||||
self.timeout_config = timeout_config;
|
||||
self
|
||||
}
|
||||
""",
|
||||
*codegenScope
|
||||
)
|
||||
ServiceConfig.BuilderBuild -> rustTemplate(
|
||||
"""timeout_config: self.timeout_config,""",
|
||||
*codegenScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate path to the timeout module in aws_smithy_types
|
||||
fun smithyTypesTimeout(runtimeConfig: RuntimeConfig) =
|
||||
RuntimeType("timeout", runtimeConfig.runtimeCrate("types"), "aws_smithy_types")
|
|
@ -148,8 +148,7 @@ class FluentClientGenerator(
|
|||
private val model = codegenContext.model
|
||||
private val clientDep = CargoDependency.SmithyClient(codegenContext.runtimeConfig).copy(optional = true)
|
||||
private val runtimeConfig = codegenContext.runtimeConfig
|
||||
private val moduleName = codegenContext.moduleName
|
||||
private val moduleUseName = moduleName.replace("-", "_")
|
||||
private val moduleUseName = codegenContext.moduleUseName()
|
||||
private val humanName = serviceShape.id.name
|
||||
private val core = FluentClientCore(model)
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust
|
||||
package software.amazon.smithy.rust.codegen.customizations
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import software.amazon.smithy.rust.codegen.smithy.RetryConfigProviderConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.RetryConfigProviderConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer
|
||||
import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer
|
||||
import software.amazon.smithy.rust.codegen.testutil.TestWorkspace
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.customizations
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.SleepImplProviderConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer
|
||||
import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer
|
||||
import software.amazon.smithy.rust.codegen.testutil.TestWorkspace
|
||||
import software.amazon.smithy.rust.codegen.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.testutil.rustSettings
|
||||
import software.amazon.smithy.rust.codegen.testutil.testCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.testutil.validateConfigCustomizations
|
||||
|
||||
internal class SleepImplProviderConfigTest {
|
||||
private val baseModel = """
|
||||
namespace test
|
||||
use aws.protocols#awsQuery
|
||||
|
||||
structure SomeOutput {
|
||||
@xmlAttribute
|
||||
someAttribute: Long,
|
||||
|
||||
someVal: String
|
||||
}
|
||||
|
||||
operation SomeOperation {
|
||||
output: SomeOutput
|
||||
}
|
||||
""".asSmithyModel()
|
||||
|
||||
@Test
|
||||
fun `generates a valid config`() {
|
||||
val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel))
|
||||
val project = TestWorkspace.testProject()
|
||||
val codegenContext = testCodegenContext(model, settings = project.rustSettings(model))
|
||||
|
||||
validateConfigCustomizations(SleepImplProviderConfig(codegenContext), project)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
package software.amazon.smithy.rust.codegen.customizations
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import software.amazon.smithy.rust.codegen.smithy.customizations.TimeoutConfigProviderConfig
|
||||
import software.amazon.smithy.rust.codegen.smithy.transformers.OperationNormalizer
|
||||
import software.amazon.smithy.rust.codegen.smithy.transformers.RecursiveShapeBoxer
|
||||
import software.amazon.smithy.rust.codegen.testutil.TestWorkspace
|
||||
import software.amazon.smithy.rust.codegen.testutil.asSmithyModel
|
||||
import software.amazon.smithy.rust.codegen.testutil.rustSettings
|
||||
import software.amazon.smithy.rust.codegen.testutil.testCodegenContext
|
||||
import software.amazon.smithy.rust.codegen.testutil.validateConfigCustomizations
|
||||
|
||||
internal class TimeoutConfigProviderConfigTest {
|
||||
private val baseModel = """
|
||||
namespace test
|
||||
use aws.protocols#awsQuery
|
||||
|
||||
structure SomeOutput {
|
||||
@xmlAttribute
|
||||
someAttribute: Long,
|
||||
|
||||
someVal: String
|
||||
}
|
||||
|
||||
operation SomeOperation {
|
||||
output: SomeOutput
|
||||
}
|
||||
""".asSmithyModel()
|
||||
|
||||
@Test
|
||||
fun `generates a valid config`() {
|
||||
val model = RecursiveShapeBoxer.transform(OperationNormalizer.transform(baseModel))
|
||||
val project = TestWorkspace.testProject()
|
||||
val codegenContext = testCodegenContext(model, settings = project.rustSettings(model))
|
||||
|
||||
validateConfigCustomizations(TimeoutConfigProviderConfig(codegenContext), project)
|
||||
}
|
||||
}
|
|
@ -10,3 +10,30 @@
|
|||
|
||||
pub mod future;
|
||||
pub mod rt;
|
||||
|
||||
/// Given an `Instant` and a `Duration`, assert time elapsed since `Instant` is equal to `Duration`.
|
||||
/// This macro allows for a 5ms margin of error.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let now = std::time::Instant::now();
|
||||
/// let _ = some_function_that_always_takes_five_seconds_to_run().await;
|
||||
/// assert_elapsed!(now, std::time::Duration::from_secs(5));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! assert_elapsed {
|
||||
($start:expr, $dur:expr) => {{
|
||||
let elapsed = $start.elapsed();
|
||||
// type ascription improves compiler error when wrong type is passed
|
||||
let lower: std::time::Duration = $dur;
|
||||
|
||||
// Handles ms rounding
|
||||
assert!(
|
||||
elapsed >= lower && elapsed <= lower + std::time::Duration::from_millis(5),
|
||||
"actual = {:?}, expected = {:?}",
|
||||
elapsed,
|
||||
lower
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -38,10 +38,15 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a default sleep implementation based on the features enabled, or `None` if
|
||||
/// there isn't one available from this crate.
|
||||
#[cfg(feature = "rt-tokio")]
|
||||
/// Returns a default sleep implementation based on the features enabled
|
||||
pub fn default_async_sleep() -> Option<Arc<dyn AsyncSleep>> {
|
||||
sleep_tokio()
|
||||
Some(sleep_tokio())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rt-tokio"))]
|
||||
pub fn default_async_sleep() -> Option<Arc<dyn AsyncSleep>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Future returned by [`AsyncSleep`].
|
||||
|
@ -83,11 +88,6 @@ impl AsyncSleep for TokioSleep {
|
|||
}
|
||||
|
||||
#[cfg(feature = "rt-tokio")]
|
||||
fn sleep_tokio() -> Option<Arc<dyn AsyncSleep>> {
|
||||
Some(Arc::new(TokioSleep::new()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rt-tokio"))]
|
||||
fn sleep_tokio() -> Option<Arc<dyn AsyncSleep>> {
|
||||
None
|
||||
fn sleep_tokio() -> Arc<dyn AsyncSleep> {
|
||||
Arc::new(TokioSleep::new())
|
||||
}
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{bounds, erase, retry, Client};
|
||||
use aws_smithy_async::rt::sleep::AsyncSleep;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::result::ConnectorError;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
|
||||
/// A builder that provides more customization options when constructing a [`Client`].
|
||||
///
|
||||
|
@ -17,6 +21,8 @@ pub struct Builder<C = (), M = (), R = retry::Standard> {
|
|||
connector: C,
|
||||
middleware: M,
|
||||
retry_policy: R,
|
||||
timeout_config: TimeoutConfig,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
}
|
||||
|
||||
// It'd be nice to include R where R: Default here, but then the caller ends up always having to
|
||||
|
@ -32,13 +38,9 @@ where
|
|||
C: Default,
|
||||
M: Default,
|
||||
{
|
||||
/// Construct a new builder.
|
||||
///
|
||||
/// This will
|
||||
///
|
||||
/// You will likely want to , as it does not specify a [connector](Builder::connector)
|
||||
/// or [middleware](Builder::middleware). It uses the [standard retry
|
||||
/// mechanism](retry::Standard).
|
||||
/// Construct a new builder. This does not specify a [connector](Builder::connector)
|
||||
/// or [middleware](Builder::middleware).
|
||||
/// It uses the [standard retry mechanism](retry::Standard).
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
@ -58,6 +60,8 @@ impl<M, R> Builder<(), M, R> {
|
|||
connector,
|
||||
retry_policy: self.retry_policy,
|
||||
middleware: self.middleware,
|
||||
timeout_config: self.timeout_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,7 +115,9 @@ impl<C, R> Builder<C, (), R> {
|
|||
Builder {
|
||||
connector: self.connector,
|
||||
retry_policy: self.retry_policy,
|
||||
timeout_config: self.timeout_config,
|
||||
middleware,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,7 +160,9 @@ impl<C, M> Builder<C, M, retry::Standard> {
|
|||
Builder {
|
||||
connector: self.connector,
|
||||
retry_policy,
|
||||
timeout_config: self.timeout_config,
|
||||
middleware: self.middleware,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,6 +172,16 @@ impl<C, M> Builder<C, M> {
|
|||
pub fn set_retry_config(&mut self, config: retry::Config) {
|
||||
self.retry_policy.with_config(config);
|
||||
}
|
||||
|
||||
/// Set a timeout config for the builder
|
||||
pub fn set_timeout_config(&mut self, timeout_config: TimeoutConfig) {
|
||||
self.timeout_config = timeout_config;
|
||||
}
|
||||
|
||||
/// Set the [`AsyncSleep`] function that the [`Client`] will use to create things like timeout futures.
|
||||
pub fn set_sleep_impl(&mut self, async_sleep: Option<Arc<dyn AsyncSleep>>) {
|
||||
self.sleep_impl = async_sleep;
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, M, R> Builder<C, M, R> {
|
||||
|
@ -176,6 +194,8 @@ impl<C, M, R> Builder<C, M, R> {
|
|||
connector: map(self.connector),
|
||||
middleware: self.middleware,
|
||||
retry_policy: self.retry_policy,
|
||||
timeout_config: self.timeout_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,6 +208,8 @@ impl<C, M, R> Builder<C, M, R> {
|
|||
connector: self.connector,
|
||||
middleware: map(self.middleware),
|
||||
retry_policy: self.retry_policy,
|
||||
timeout_config: self.timeout_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,6 +219,8 @@ impl<C, M, R> Builder<C, M, R> {
|
|||
connector: self.connector,
|
||||
retry_policy: self.retry_policy,
|
||||
middleware: self.middleware,
|
||||
timeout_config: self.timeout_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ where
|
|||
connector: self.connector,
|
||||
middleware: DynMiddleware::new(self.middleware),
|
||||
retry_policy: self.retry_policy,
|
||||
timeout_config: self.timeout_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +96,8 @@ where
|
|||
connector: DynConnector::new(self.connector),
|
||||
middleware: self.middleware,
|
||||
retry_policy: self.retry_policy,
|
||||
timeout_config: self.timeout_config,
|
||||
sleep_impl: self.sleep_impl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,24 +38,25 @@
|
|||
//! let client = Client::<DynConnector, MyMiddleware>::new(DynConnector::new(connector));
|
||||
//! ```
|
||||
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
|
||||
use http::Uri;
|
||||
use hyper::client::connect::Connection;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tower::{BoxError, Service};
|
||||
|
||||
use aws_smithy_async::future::timeout::TimedOutError;
|
||||
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep};
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::result::ConnectorError;
|
||||
use std::error::Error;
|
||||
|
||||
use crate::hyper_ext::timeout_middleware::{ConnectTimeout, HttpReadTimeout, TimeoutError};
|
||||
use crate::{timeout, Builder as ClientBuilder};
|
||||
use aws_smithy_async::future::timeout::TimedOutError;
|
||||
pub use aws_smithy_http::result::{SdkError, SdkSuccess};
|
||||
use aws_smithy_types::retry::ErrorKind;
|
||||
|
||||
use crate::{timeout, Builder as ClientBuilder};
|
||||
|
||||
use self::timeout_middleware::{ConnectTimeout, HttpReadTimeout, TimeoutError};
|
||||
|
||||
/// Adapter from a [`hyper::Client`](hyper::Client) to a connector usable by a Smithy [`Client`](crate::Client).
|
||||
///
|
||||
/// This adapter also enables TCP `CONNECT` and HTTP `READ` timeouts via [`Adapter::builder`]. For examples
|
||||
|
@ -303,6 +304,8 @@ impl<M, R> ClientBuilder<(), M, R> {
|
|||
}
|
||||
|
||||
mod timeout_middleware {
|
||||
use std::error::Error;
|
||||
use std::fmt::Formatter;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
@ -310,16 +313,13 @@ mod timeout_middleware {
|
|||
use std::time::Duration;
|
||||
|
||||
use http::Uri;
|
||||
|
||||
use pin_project_lite::pin_project;
|
||||
use tower::BoxError;
|
||||
|
||||
use aws_smithy_async::future;
|
||||
use aws_smithy_async::future::timeout::{TimedOutError, Timeout};
|
||||
use aws_smithy_async::rt::sleep::AsyncSleep;
|
||||
use aws_smithy_async::rt::sleep::Sleep;
|
||||
use std::error::Error;
|
||||
use std::fmt::Formatter;
|
||||
use tower::BoxError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TimeoutError {
|
||||
|
@ -507,29 +507,17 @@ mod timeout_middleware {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Duration;
|
||||
|
||||
use tower::Service;
|
||||
|
||||
use aws_smithy_async::assert_elapsed;
|
||||
use aws_smithy_async::rt::sleep::TokioSleep;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
|
||||
use crate::hyper_ext::Adapter;
|
||||
use crate::never::{NeverConnected, NeverReplies};
|
||||
use crate::timeout;
|
||||
use aws_smithy_async::rt::sleep::TokioSleep;
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use std::time::Duration;
|
||||
use tower::Service;
|
||||
|
||||
macro_rules! assert_elapsed {
|
||||
($start:expr, $dur:expr) => {{
|
||||
let elapsed = $start.elapsed();
|
||||
// type ascription improves compiler error when wrong type is passed
|
||||
let lower: std::time::Duration = $dur;
|
||||
|
||||
// Handles ms rounding
|
||||
assert!(
|
||||
elapsed >= lower && elapsed <= lower + std::time::Duration::from_millis(5),
|
||||
"actual = {:?}, expected = {:?}",
|
||||
elapsed,
|
||||
lower
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn connect_timeout_is_correct<T: Send + Sync + Clone + 'static>() {
|
||||
|
@ -595,18 +583,19 @@ mod timeout_middleware {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::hyper_ext::Adapter;
|
||||
use http::Uri;
|
||||
use hyper::client::connect::{Connected, Connection};
|
||||
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
use http::Uri;
|
||||
use hyper::client::connect::{Connected, Connection};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tower::BoxError;
|
||||
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
|
||||
use crate::hyper_ext::Adapter;
|
||||
|
||||
#[tokio::test]
|
||||
async fn hyper_io_error() {
|
||||
let connector = TestConnection {
|
||||
|
|
|
@ -36,6 +36,7 @@ pub mod static_tests;
|
|||
#[cfg(feature = "hyper")]
|
||||
pub mod never;
|
||||
pub mod timeout;
|
||||
pub use timeout::TimeoutLayer;
|
||||
|
||||
/// Type aliases for standard connection types.
|
||||
#[cfg(feature = "hyper")]
|
||||
|
@ -72,6 +73,12 @@ pub mod conns {
|
|||
crate::hyper_ext::Adapter<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>;
|
||||
}
|
||||
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tower::{Layer, Service, ServiceBuilder, ServiceExt};
|
||||
|
||||
use crate::timeout::generate_timeout_service_params_from_timeout_config;
|
||||
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep};
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::operation::Operation;
|
||||
use aws_smithy_http::response::ParseHttpResponse;
|
||||
|
@ -80,13 +87,11 @@ use aws_smithy_http::retry::ClassifyResponse;
|
|||
use aws_smithy_http_tower::dispatch::DispatchLayer;
|
||||
use aws_smithy_http_tower::parse_response::ParseResponseLayer;
|
||||
use aws_smithy_types::retry::ProvideErrorKind;
|
||||
use std::error::Error;
|
||||
|
||||
use tower::{Layer, Service, ServiceBuilder, ServiceExt};
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
|
||||
/// Smithy service client.
|
||||
///
|
||||
/// The service client is customizeable in a number of ways (see [`Builder`]), but most customers
|
||||
/// The service client is customizable in a number of ways (see [`Builder`]), but most customers
|
||||
/// can stick with the standard constructor provided by [`Client::new`]. It takes only a single
|
||||
/// argument, which is the middleware that fills out the [`http::Request`] for each higher-level
|
||||
/// operation so that it can ultimately be sent to the remote host. The middleware is responsible
|
||||
|
@ -112,6 +117,8 @@ pub struct Client<
|
|||
connector: Connector,
|
||||
middleware: Middleware,
|
||||
retry_policy: RetryPolicy,
|
||||
timeout_config: TimeoutConfig,
|
||||
sleep_impl: Option<Arc<dyn AsyncSleep>>,
|
||||
}
|
||||
|
||||
// Quick-create for people who just want "the default".
|
||||
|
@ -119,13 +126,17 @@ impl<C, M> Client<C, M>
|
|||
where
|
||||
M: Default,
|
||||
{
|
||||
/// Create a Smithy client that the given connector, a middleware default, and the [standard
|
||||
/// retry policy](crate::retry::Standard).
|
||||
/// Create a Smithy client that the given connector, a middleware default, the [standard
|
||||
/// retry policy](crate::retry::Standard), and the [`default_async_sleep`] sleep implementation.
|
||||
pub fn new(connector: C) -> Self {
|
||||
Builder::new()
|
||||
let mut client = Builder::new()
|
||||
.connector(connector)
|
||||
.middleware(M::default())
|
||||
.build()
|
||||
.build();
|
||||
|
||||
client.set_sleep_impl(default_async_sleep());
|
||||
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,6 +151,28 @@ impl<C, M> Client<C, M> {
|
|||
self.set_retry_config(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client's timeout configuration.
|
||||
pub fn set_timeout_config(&mut self, config: TimeoutConfig) {
|
||||
self.timeout_config = config;
|
||||
}
|
||||
|
||||
/// Set the client's timeout configuration.
|
||||
pub fn with_timeout_config(mut self, config: TimeoutConfig) -> Self {
|
||||
self.set_timeout_config(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`AsyncSleep`] function that the client will use to create things like timeout futures.
|
||||
pub fn set_sleep_impl(&mut self, sleep_impl: Option<Arc<dyn AsyncSleep>>) {
|
||||
self.sleep_impl = sleep_impl;
|
||||
}
|
||||
|
||||
/// Set the [`AsyncSleep`] function that the client will use to create things like timeout futures.
|
||||
pub fn with_sleep_impl(mut self, sleep_impl: Arc<dyn AsyncSleep>) -> Self {
|
||||
self.set_sleep_impl(Some(sleep_impl));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn check_send_sync<T: Send + Sync>(t: T) -> T {
|
||||
|
@ -189,9 +222,16 @@ where
|
|||
Service<Operation<O, Retry>, Response = SdkSuccess<T>, Error = SdkError<E>> + Clone,
|
||||
{
|
||||
let connector = self.connector.clone();
|
||||
|
||||
let timeout_servic_params = generate_timeout_service_params_from_timeout_config(
|
||||
&self.timeout_config,
|
||||
self.sleep_impl.clone(),
|
||||
);
|
||||
|
||||
let svc = ServiceBuilder::new()
|
||||
// Create a new request-scoped policy
|
||||
.layer(TimeoutLayer::new(timeout_servic_params.api_call))
|
||||
.retry(self.retry_policy.new_request_policy())
|
||||
.layer(TimeoutLayer::new(timeout_servic_params.api_call_attempt))
|
||||
.layer(ParseResponseLayer::<O, Retry>::new())
|
||||
// These layers can be considered as occurring in order. That is, first invoke the
|
||||
// customer-provided middleware, then dispatch dispatch over the wire.
|
||||
|
|
|
@ -14,17 +14,18 @@
|
|||
//! Its sole purpose in life is to create a [`RetryHandler`] for individual requests.
|
||||
//! - [`RetryHandler`]: A request-scoped retry policy, backed by request-local state and shared
|
||||
//! state contained within [`Standard`].
|
||||
//! - [`Config`]: Static configuration (max retries, max backoff etc.)
|
||||
//! - [`Config`]: Static configuration (max attempts, max backoff etc.)
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{SdkError, SdkSuccess};
|
||||
use aws_smithy_http::operation;
|
||||
use aws_smithy_http::operation::Operation;
|
||||
use aws_smithy_http::retry::ClassifyResponse;
|
||||
use aws_smithy_types::retry::{ErrorKind, RetryKind};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tracing::Instrument;
|
||||
|
||||
/// A policy instantiator.
|
||||
|
@ -334,15 +335,15 @@ mod test {
|
|||
use aws_smithy_types::retry::ErrorKind;
|
||||
use std::time::Duration;
|
||||
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
|
||||
fn test_config() -> Config {
|
||||
Config::default().with_base(|| 1_f64)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_handler_send_sync() {
|
||||
assert_send_sync::<RetryHandler>()
|
||||
fn must_be_send_sync<T: Send + Sync>() {}
|
||||
|
||||
must_be_send_sync::<RetryHandler>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -9,25 +9,33 @@
|
|||
//!
|
||||
//! As timeout and HTTP configuration stabilizes, this will move to aws-types and become a part of
|
||||
//! HttpSettings.
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::SdkError;
|
||||
use aws_smithy_async::future::timeout::{TimedOutError, Timeout};
|
||||
use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep};
|
||||
use aws_smithy_http::operation::Operation;
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use pin_project_lite::pin_project;
|
||||
use tower::Layer;
|
||||
|
||||
/// Timeout Configuration
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct Settings {
|
||||
connect_timeout: Option<Duration>,
|
||||
http_read_timeout: Option<Duration>,
|
||||
read_timeout: Option<Duration>,
|
||||
tls_negotiation_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Create a new timeout configuration
|
||||
///
|
||||
/// The current default (subject to change) is no timeouts
|
||||
/// Create a new timeout configuration with no timeouts set
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connect_timeout: None,
|
||||
http_read_timeout: None,
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// The configured TCP-connect timeout
|
||||
|
@ -37,7 +45,7 @@ impl Settings {
|
|||
|
||||
/// The configured HTTP-read timeout
|
||||
pub fn read(&self) -> Option<Duration> {
|
||||
self.http_read_timeout
|
||||
self.read_timeout
|
||||
}
|
||||
|
||||
/// Sets the connect timeout
|
||||
|
@ -51,8 +59,243 @@ impl Settings {
|
|||
/// Sets the read timeout
|
||||
pub fn with_read_timeout(self, read_timeout: Duration) -> Self {
|
||||
Self {
|
||||
http_read_timeout: Some(read_timeout),
|
||||
read_timeout: Some(read_timeout),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A struct containing everything needed to create a new [`TimeoutService`]
|
||||
pub struct TimeoutServiceParams {
|
||||
/// The duration of timeouts created from these params
|
||||
duration: Duration,
|
||||
/// The kind of timeouts created from these params
|
||||
kind: &'static str,
|
||||
/// The AsyncSleep impl that will be used to create time-limited futures
|
||||
async_sleep: Arc<dyn AsyncSleep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// A struct of structs containing everything needed to create new [`TimeoutService`]s
|
||||
pub struct ClientTimeoutParams {
|
||||
/// Params used to create a new API call [`TimeoutService`]
|
||||
pub(crate) api_call: Option<TimeoutServiceParams>,
|
||||
/// Params used to create a new API call attempt [`TimeoutService`]
|
||||
pub(crate) api_call_attempt: Option<TimeoutServiceParams>,
|
||||
}
|
||||
|
||||
/// Convert a [`TimeoutConfig`] into an [`ClientTimeoutParams`] in order to create the set of
|
||||
/// [`TimeoutService`]s needed by a [`crate::Client`]
|
||||
pub fn generate_timeout_service_params_from_timeout_config(
|
||||
timeout_config: &TimeoutConfig,
|
||||
async_sleep: Option<Arc<dyn AsyncSleep>>,
|
||||
) -> ClientTimeoutParams {
|
||||
if let Some(async_sleep) = async_sleep {
|
||||
ClientTimeoutParams {
|
||||
api_call: timeout_config
|
||||
.api_call_timeout()
|
||||
.map(|duration| TimeoutServiceParams {
|
||||
duration,
|
||||
kind: "API call (all attempts including retries)",
|
||||
async_sleep: async_sleep.clone(),
|
||||
}),
|
||||
api_call_attempt: timeout_config.api_call_attempt_timeout().map(|duration| {
|
||||
TimeoutServiceParams {
|
||||
duration,
|
||||
kind: "API call (single attempt)",
|
||||
async_sleep: async_sleep.clone(),
|
||||
}
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"One or more timeouts were set but no async_sleep fn was passed. No timeouts will occur.\n{:?}",
|
||||
timeout_config
|
||||
);
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A service that wraps another service, adding the ability to set a timeout for requests
|
||||
/// handled by the inner service.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TimeoutService<S> {
|
||||
inner: S,
|
||||
params: Option<TimeoutServiceParams>,
|
||||
}
|
||||
|
||||
impl<S> TimeoutService<S> {
|
||||
/// Create a new `TimeoutService` that will timeout after the duration specified in `params` elapses
|
||||
pub fn new(inner: S, params: Option<TimeoutServiceParams>) -> Self {
|
||||
Self { inner, params }
|
||||
}
|
||||
|
||||
/// Create a new `TimeoutService` that will never timeout
|
||||
pub fn no_timeout(inner: S) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
params: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer that wraps services in a timeout service
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub struct TimeoutLayer(Option<TimeoutServiceParams>);
|
||||
|
||||
impl TimeoutLayer {
|
||||
/// Create a new `TimeoutLayer`
|
||||
pub fn new(params: Option<TimeoutServiceParams>) -> Self {
|
||||
TimeoutLayer(params)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for TimeoutLayer {
|
||||
type Service = TimeoutService<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
TimeoutService {
|
||||
inner,
|
||||
params: self.0.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[non_exhaustive]
|
||||
#[must_use = "futures do nothing unless you `.await` or poll them"]
|
||||
// This allow is needed because otherwise Clippy will get mad we didn't document the
|
||||
// generated TimeoutServiceFutureProj
|
||||
#[allow(missing_docs)]
|
||||
#[project = TimeoutServiceFutureProj]
|
||||
/// A future generated by a [`TimeoutService`] that may or may not have a timeout depending on
|
||||
/// whether or not one was set. Because `TimeoutService` can be used at multiple levels of the
|
||||
/// service stack, a `kind` can be set so that when a timeout occurs, you can know which kind of
|
||||
/// timeout it was.
|
||||
pub enum TimeoutServiceFuture<F> {
|
||||
/// A wrapper around an inner future that will output an [`SdkError`] if it runs longer than
|
||||
/// the given duration
|
||||
Timeout {
|
||||
#[pin]
|
||||
future: Timeout<F, Sleep>,
|
||||
kind: &'static str,
|
||||
duration: Duration,
|
||||
},
|
||||
/// A thin wrapper around an inner future that will never time out
|
||||
NoTimeout {
|
||||
#[pin]
|
||||
future: F
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> TimeoutServiceFuture<F> {
|
||||
/// Given a `future`, an implementor of `AsyncSleep`, a `kind` for this timeout, and a `duration`,
|
||||
/// wrap the `future` inside a [`Timeout`] future and create a new [`TimeoutServiceFuture`] that
|
||||
/// will output an [`SdkError`] if `future` doesn't complete before `duration` has elapsed.
|
||||
pub fn new(future: F, params: &TimeoutServiceParams) -> Self {
|
||||
Self::Timeout {
|
||||
future: Timeout::new(future, params.async_sleep.sleep(params.duration)),
|
||||
kind: params.kind,
|
||||
duration: params.duration,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`TimeoutServiceFuture`] that will never time out.
|
||||
pub fn no_timeout(future: F) -> Self {
|
||||
Self::NoTimeout { future }
|
||||
}
|
||||
}
|
||||
|
||||
impl<InnerFuture, T, E> Future for TimeoutServiceFuture<InnerFuture>
|
||||
where
|
||||
InnerFuture: Future<Output = Result<T, SdkError<E>>>,
|
||||
{
|
||||
type Output = Result<T, SdkError<E>>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// TODO duration will be used in the error message once a Timeout variant is added to SdkError
|
||||
let (future, _kind, _duration) = match self.project() {
|
||||
TimeoutServiceFutureProj::NoTimeout { future } => return future.poll(cx),
|
||||
TimeoutServiceFutureProj::Timeout {
|
||||
future,
|
||||
kind,
|
||||
duration,
|
||||
} => (future, kind, duration),
|
||||
};
|
||||
match future.poll(cx) {
|
||||
Poll::Ready(Ok(response)) => Poll::Ready(response),
|
||||
Poll::Ready(Err(_timeout)) => {
|
||||
// TODO update SdkError to include a variant specifically for timeouts
|
||||
Poll::Ready(Err(SdkError::ConstructionFailure(Box::new(TimedOutError))))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, R, InnerService, E> tower::Service<Operation<H, R>> for TimeoutService<InnerService>
|
||||
where
|
||||
InnerService: tower::Service<Operation<H, R>, Error = SdkError<E>>,
|
||||
{
|
||||
type Response = InnerService::Response;
|
||||
type Error = aws_smithy_http::result::SdkError<E>;
|
||||
type Future = TimeoutServiceFuture<InnerService::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Operation<H, R>) -> Self::Future {
|
||||
let future = self.inner.call(req);
|
||||
|
||||
if let Some(params) = &self.params {
|
||||
Self::Future::new(future, params)
|
||||
} else {
|
||||
Self::Future::no_timeout(future)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::never::NeverService;
|
||||
use crate::timeout::generate_timeout_service_params_from_timeout_config;
|
||||
use crate::{SdkError, TimeoutLayer};
|
||||
use aws_smithy_async::assert_elapsed;
|
||||
use aws_smithy_async::rt::sleep::{AsyncSleep, TokioSleep};
|
||||
use aws_smithy_http::body::SdkBody;
|
||||
use aws_smithy_http::operation::{Operation, Request};
|
||||
use aws_smithy_types::timeout::TimeoutConfig;
|
||||
use tower::{Service, ServiceBuilder, ServiceExt};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timeout_service_ends_request_that_never_completes() {
|
||||
let req = Request::new(http::Request::new(SdkBody::empty()));
|
||||
let op = Operation::new(req, ());
|
||||
let never_service: NeverService<_, (), _> = NeverService::new();
|
||||
let timeout_config =
|
||||
TimeoutConfig::new().with_api_call_timeout(Some(Duration::from_secs_f32(0.25)));
|
||||
let sleep_impl: Option<Arc<dyn AsyncSleep>> = Some(Arc::new(TokioSleep::new()));
|
||||
let timeout_service_params =
|
||||
generate_timeout_service_params_from_timeout_config(&timeout_config, sleep_impl);
|
||||
let mut svc = ServiceBuilder::new()
|
||||
.layer(TimeoutLayer::new(timeout_service_params.api_call))
|
||||
.service(never_service);
|
||||
|
||||
let now = tokio::time::Instant::now();
|
||||
tokio::time::pause();
|
||||
|
||||
let err: SdkError<Box<dyn std::error::Error + 'static>> =
|
||||
svc.ready().await.unwrap().call(op).await.unwrap_err();
|
||||
|
||||
assert_eq!(format!("{:?}", err), "ConstructionFailure(TimedOutError)");
|
||||
assert_elapsed!(now, Duration::from_secs_f32(0.25));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ use aws_smithy_http::operation;
|
|||
use aws_smithy_http::operation::Operation;
|
||||
use aws_smithy_http::response::ParseHttpResponse;
|
||||
use aws_smithy_http::result::SdkError;
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
@ -67,22 +66,28 @@ type BoxedResultFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>
|
|||
/// `T`: The happy path return of the response parser
|
||||
/// `E`: The error path return of the response parser
|
||||
/// `R`: The type of the retry policy
|
||||
impl<S, O, T, E, R> tower::Service<operation::Operation<O, R>> for ParseResponseService<S, O, R>
|
||||
impl<InnerService, ResponseHandler, SuccessResponse, FailureResponse, RetryPolicy>
|
||||
tower::Service<operation::Operation<ResponseHandler, RetryPolicy>>
|
||||
for ParseResponseService<InnerService, ResponseHandler, RetryPolicy>
|
||||
where
|
||||
S: Service<operation::Request, Response = operation::Response, Error = SendOperationError>,
|
||||
S::Future: Send + 'static,
|
||||
O: ParseHttpResponse<Output = Result<T, E>> + Send + Sync + 'static,
|
||||
E: Error,
|
||||
InnerService:
|
||||
Service<operation::Request, Response = operation::Response, Error = SendOperationError>,
|
||||
InnerService::Future: Send + 'static,
|
||||
ResponseHandler: ParseHttpResponse<Output = Result<SuccessResponse, FailureResponse>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
FailureResponse: std::error::Error,
|
||||
{
|
||||
type Response = aws_smithy_http::result::SdkSuccess<T>;
|
||||
type Error = aws_smithy_http::result::SdkError<E>;
|
||||
type Response = aws_smithy_http::result::SdkSuccess<SuccessResponse>;
|
||||
type Error = aws_smithy_http::result::SdkError<FailureResponse>;
|
||||
type Future = BoxedResultFuture<Self::Response, Self::Error>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Operation<O, R>) -> Self::Future {
|
||||
fn call(&mut self, req: Operation<ResponseHandler, RetryPolicy>) -> Self::Future {
|
||||
let (req, parts) = req.into_request_response();
|
||||
let handler = parts.response_handler;
|
||||
// send_operation records the full request-response lifecycle.
|
||||
|
|
|
@ -33,6 +33,9 @@ pub struct SdkSuccess<O> {
|
|||
/// Failed SDK Result
|
||||
#[derive(Debug)]
|
||||
pub enum SdkError<E, R = operation::Response> {
|
||||
// TODO Request failures due to a timeout currently report this error type even though
|
||||
// they're not really a construction failure. Add a new variant for timeouts or update
|
||||
// DispatchFailure to accept more than just ConnectorErrors
|
||||
/// The request failed during construction. It was not dispatched over the network.
|
||||
ConstructionFailure(BoxError),
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
#![warn(
|
||||
missing_docs,
|
||||
missing_crate_level_docs,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
missing_debug_implementations,
|
||||
rust_2018_idioms,
|
||||
unreachable_pub
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
#![warn(
|
||||
missing_docs,
|
||||
missing_crate_level_docs,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
missing_debug_implementations,
|
||||
rust_2018_idioms,
|
||||
unreachable_pub
|
||||
|
@ -19,6 +19,7 @@ pub mod base64;
|
|||
pub mod date_time;
|
||||
pub mod primitive;
|
||||
pub mod retry;
|
||||
pub mod timeout;
|
||||
|
||||
pub use crate::date_time::DateTime;
|
||||
|
||||
|
|
|
@ -173,13 +173,13 @@ impl RetryConfigBuilder {
|
|||
/// # use aws_smithy_types::retry::{RetryMode, RetryConfigBuilder};
|
||||
/// let a = RetryConfigBuilder::new().max_attempts(1);
|
||||
/// let b = RetryConfigBuilder::new().max_attempts(5).mode(RetryMode::Adaptive);
|
||||
/// let retry_config = a.merge_with(b).build();
|
||||
/// let retry_config = a.take_unset_from(b).build();
|
||||
/// // A's value take precedence over B's value
|
||||
/// assert_eq!(retry_config.max_attempts(), 1);
|
||||
/// // A never set a retry mode so B's value was used
|
||||
/// assert_eq!(retry_config.mode(), RetryMode::Adaptive);
|
||||
/// ```
|
||||
pub fn merge_with(self, other: Self) -> Self {
|
||||
pub fn take_unset_from(self, other: Self) -> Self {
|
||||
Self {
|
||||
mode: self.mode.or(other.mode),
|
||||
max_attempts: self.max_attempts.or(other.max_attempts),
|
||||
|
@ -324,7 +324,7 @@ mod tests {
|
|||
let other_builder = RetryConfigBuilder::new()
|
||||
.max_attempts(5)
|
||||
.mode(RetryMode::Standard);
|
||||
let retry_config = self_builder.merge_with(other_builder).build();
|
||||
let retry_config = self_builder.take_unset_from(other_builder).build();
|
||||
|
||||
assert_eq!(retry_config.max_attempts, 1);
|
||||
assert_eq!(retry_config.mode, RetryMode::Adaptive);
|
||||
|
|
|
@ -0,0 +1,350 @@
|
|||
/*
|
||||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*/
|
||||
|
||||
//! This module defines types that describe timeouts for the various stages of an HTTP request.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration for timeouts
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::time::Duration;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// use aws_smithy_types::timeout::TimeoutConfig;
|
||||
/// let timeout_config = TimeoutConfig::new()
|
||||
/// .with_api_call_timeout(Some(Duration::from_secs(2)))
|
||||
/// .with_api_call_attempt_timeout(Some(Duration::from_secs_f32(0.5)));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// timeout_config.api_call_timeout(),
|
||||
/// Some(Duration::from_secs(2))
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// timeout_config.api_call_attempt_timeout(),
|
||||
/// Some(Duration::from_secs_f32(0.5))
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, PartialEq, Default)]
|
||||
pub struct TimeoutConfig {
|
||||
connect_timeout: Option<Duration>,
|
||||
tls_negotiation_timeout: Option<Duration>,
|
||||
read_timeout: Option<Duration>,
|
||||
api_call_attempt_timeout: Option<Duration>,
|
||||
api_call_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Debug for TimeoutConfig {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"Timeouts:
|
||||
Connect (time to first byte):{}
|
||||
TLS negotiation:{}
|
||||
HTTP read:{}
|
||||
API requests:{}
|
||||
HTTP requests:{}
|
||||
"#,
|
||||
format_timeout(self.connect_timeout),
|
||||
format_timeout(self.tls_negotiation_timeout),
|
||||
format_timeout(self.read_timeout),
|
||||
format_timeout(self.api_call_timeout),
|
||||
format_timeout(self.api_call_attempt_timeout),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_timeout(timeout: Option<Duration>) -> String {
|
||||
timeout
|
||||
.map(|d| format!("\t{}s", d.as_secs_f32()))
|
||||
.unwrap_or_else(|| "(unset)".to_owned())
|
||||
}
|
||||
|
||||
/// Parse a given string as a [`Duration`] that will be used to set a timeout. This will return an
|
||||
/// error result if the given string is negative, infinite, equal to zero, NaN, or if the string
|
||||
/// can't be parsed as an `f32`. The `name` and `set_by` fields are used to provide context when an
|
||||
/// error occurs
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use std::time::Duration;
|
||||
/// # use aws_smithy_types::timeout::parse_str_as_timeout;
|
||||
/// let duration = parse_str_as_timeout("8", "timeout".into(), "test_success".into()).unwrap();
|
||||
/// assert_eq!(duration, Duration::from_secs_f32(8.0));
|
||||
///
|
||||
/// // This line will panic with "InvalidTimeout { name: "timeout", reason: "timeout must not be less than or equal to zero", set_by: "test_error" }"
|
||||
/// let _ = parse_str_as_timeout("-1.0", "timeout".into(), "test_error".into()).unwrap();
|
||||
/// ```
|
||||
pub fn parse_str_as_timeout(
|
||||
timeout: &str,
|
||||
name: Cow<'static, str>,
|
||||
set_by: Cow<'static, str>,
|
||||
) -> Result<Duration, TimeoutConfigError> {
|
||||
match timeout.parse::<f32>() {
|
||||
Ok(timeout) if timeout <= 0.0 => Err(TimeoutConfigError::InvalidTimeout {
|
||||
set_by,
|
||||
name,
|
||||
reason: "timeout must not be less than or equal to zero".into(),
|
||||
}),
|
||||
Ok(timeout) if timeout.is_nan() => Err(TimeoutConfigError::InvalidTimeout {
|
||||
set_by,
|
||||
name,
|
||||
reason: "timeout must not be NaN".into(),
|
||||
}),
|
||||
Ok(timeout) if timeout.is_infinite() => Err(TimeoutConfigError::InvalidTimeout {
|
||||
set_by,
|
||||
name,
|
||||
reason: "timeout must not be infinite".into(),
|
||||
}),
|
||||
Ok(timeout) => Ok(Duration::from_secs_f32(timeout)),
|
||||
Err(err) => Err(TimeoutConfigError::ParseError {
|
||||
set_by,
|
||||
name,
|
||||
source: Box::new(err),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeoutConfig {
|
||||
/// Create a new `TimeoutConfig` with no timeouts set
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// A limit on the amount of time after making an initial connect attempt on a socket to complete the connect-handshake.
|
||||
pub fn connect_timeout(&self) -> Option<Duration> {
|
||||
self.connect_timeout
|
||||
}
|
||||
|
||||
/// A limit on the amount of time a TLS handshake takes from when the `CLIENT HELLO` message is
|
||||
/// sent to the time the client and server have fully negotiated ciphers and exchanged keys.
|
||||
pub fn tls_negotiation_timeout(&self) -> Option<Duration> {
|
||||
self.tls_negotiation_timeout
|
||||
}
|
||||
|
||||
/// A limit on the amount of time an application takes to attempt to read the first byte over an
|
||||
/// established, open connection after write request. This is also known as the
|
||||
/// "time to first byte" timeout.
|
||||
pub fn read_timeout(&self) -> Option<Duration> {
|
||||
self.read_timeout
|
||||
}
|
||||
|
||||
/// A limit on the amount of time it takes for the first byte to be sent over an established,
|
||||
/// open connection and when the last byte is received from the service for a single attempt.
|
||||
/// If you want to set a timeout for an entire request including retry attempts,
|
||||
/// use [`TimeoutConfig::api_call_timeout`] instead.
|
||||
pub fn api_call_attempt_timeout(&self) -> Option<Duration> {
|
||||
self.api_call_attempt_timeout
|
||||
}
|
||||
|
||||
/// A limit on the amount of time it takes for request to complete. A single request may be
|
||||
/// comprised of several attemps depending on an app's [`RetryConfig`](super::retry::RetryConfig). If you want
|
||||
/// to control timeouts for a single attempt, use [`TimeoutConfig::api_call_attempt_timeout`].
|
||||
pub fn api_call_timeout(&self) -> Option<Duration> {
|
||||
self.api_call_timeout
|
||||
}
|
||||
|
||||
/// Consume a `TimeoutConfig` to create a new one, setting the connect timeout
|
||||
pub fn with_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.connect_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consume a `TimeoutConfig` to create a new one, setting the TLS negotiation timeout
|
||||
pub fn with_tls_negotiation_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.tls_negotiation_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consume a `TimeoutConfig` to create a new one, setting the read timeout
|
||||
pub fn with_read_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.read_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consume a `TimeoutConfig` to create a new one, setting the API call attempt timeout
|
||||
pub fn with_api_call_attempt_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.api_call_attempt_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consume a `TimeoutConfig` to create a new one, setting the API call timeout
|
||||
pub fn with_api_call_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.api_call_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Merges two builders together.
|
||||
///
|
||||
/// Values from `other` will only be used as a fallback for values
|
||||
/// from `self`. Useful for merging configs from different sources together when you want to
|
||||
/// handle "precedence" per value instead of at the config level
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::time::Duration;
|
||||
/// # use aws_smithy_types::timeout::TimeoutConfig;
|
||||
/// let a = TimeoutConfig::new().with_read_timeout(Some(Duration::from_secs(2)));
|
||||
/// let b = TimeoutConfig::new()
|
||||
/// .with_read_timeout(Some(Duration::from_secs(10)))
|
||||
/// .with_connect_timeout(Some(Duration::from_secs(3)));
|
||||
/// let timeout_config = a.take_unset_from(b);
|
||||
/// // A's value take precedence over B's value
|
||||
/// assert_eq!(timeout_config.read_timeout(), Some(Duration::from_secs(2)));
|
||||
/// // A never set a connect timeout so B's value was used
|
||||
/// assert_eq!(timeout_config.connect_timeout(), Some(Duration::from_secs(3)));
|
||||
/// ```
|
||||
pub fn take_unset_from(self, other: Self) -> Self {
|
||||
Self {
|
||||
connect_timeout: self.connect_timeout.or(other.connect_timeout),
|
||||
tls_negotiation_timeout: self
|
||||
.tls_negotiation_timeout
|
||||
.or(other.tls_negotiation_timeout),
|
||||
read_timeout: self.read_timeout.or(other.read_timeout),
|
||||
api_call_attempt_timeout: self
|
||||
.api_call_attempt_timeout
|
||||
.or(other.api_call_attempt_timeout),
|
||||
api_call_timeout: self.api_call_timeout.or(other.api_call_timeout),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
/// An error that occurs during construction of a `TimeoutConfig`
|
||||
pub enum TimeoutConfigError {
|
||||
/// A timeout value was set to an invalid value:
|
||||
/// - Any number less than 0
|
||||
/// - Infinity or negative infinity
|
||||
/// - `NaN`
|
||||
InvalidTimeout {
|
||||
/// The name of the invalid value
|
||||
name: Cow<'static, str>,
|
||||
/// The reason that why the timeout was considered invalid
|
||||
reason: Cow<'static, str>,
|
||||
/// Where the invalid value originated from
|
||||
set_by: Cow<'static, str>,
|
||||
},
|
||||
/// The timeout value couln't be parsed as an `f32`
|
||||
ParseError {
|
||||
/// The name of the invalid value
|
||||
name: Cow<'static, str>,
|
||||
/// Where the invalid value originated from
|
||||
set_by: Cow<'static, str>,
|
||||
/// The source of this error
|
||||
source: Box<dyn std::error::Error>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for TimeoutConfigError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
use TimeoutConfigError::*;
|
||||
match self {
|
||||
InvalidTimeout {
|
||||
name,
|
||||
set_by,
|
||||
reason,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"invalid timeout '{}' set by {} is invalid: {}",
|
||||
name, set_by, reason
|
||||
)
|
||||
}
|
||||
ParseError {
|
||||
name,
|
||||
set_by,
|
||||
source,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"timeout '{}' set by {} could not be parsed as an f32: {}",
|
||||
name, set_by, source
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_str_as_timeout, TimeoutConfig};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn retry_config_builder_merge_with_favors_self_values_over_other_values() {
|
||||
let one_second = Some(Duration::from_secs(1));
|
||||
let two_seconds = Some(Duration::from_secs(2));
|
||||
|
||||
let self_config = TimeoutConfig::new()
|
||||
.with_connect_timeout(one_second)
|
||||
.with_read_timeout(one_second)
|
||||
.with_tls_negotiation_timeout(one_second)
|
||||
.with_api_call_timeout(one_second)
|
||||
.with_api_call_attempt_timeout(one_second);
|
||||
let other_config = TimeoutConfig::new()
|
||||
.with_connect_timeout(two_seconds)
|
||||
.with_read_timeout(two_seconds)
|
||||
.with_tls_negotiation_timeout(two_seconds)
|
||||
.with_api_call_timeout(two_seconds)
|
||||
.with_api_call_attempt_timeout(two_seconds);
|
||||
let timeout_config = self_config.take_unset_from(other_config);
|
||||
|
||||
assert_eq!(timeout_config.connect_timeout(), one_second);
|
||||
assert_eq!(timeout_config.read_timeout(), one_second);
|
||||
assert_eq!(timeout_config.tls_negotiation_timeout(), one_second);
|
||||
assert_eq!(timeout_config.api_call_timeout(), one_second);
|
||||
assert_eq!(timeout_config.api_call_attempt_timeout(), one_second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integer_timeouts_are_parseable() {
|
||||
let duration = parse_str_as_timeout("8", "timeout".into(), "test".into()).unwrap();
|
||||
assert_eq!(duration, Duration::from_secs_f32(8.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"ParseError { name: "timeout", set_by: "test", source: ParseFloatError { kind: Invalid } }"#]
|
||||
fn test_unparseable_timeouts_produce_an_error() {
|
||||
let _ = parse_str_as_timeout(
|
||||
"this sentence cant be parsed as a number",
|
||||
"timeout".into(),
|
||||
"test".into(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"InvalidTimeout { name: "timeout", reason: "timeout must not be less than or equal to zero", set_by: "test" }"#]
|
||||
fn test_negative_timeouts_are_invalid() {
|
||||
let _ = parse_str_as_timeout("-1.0", "timeout".into(), "test".into()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"InvalidTimeout { name: "timeout", reason: "timeout must not be less than or equal to zero", set_by: "test" }"#]
|
||||
fn test_setting_timeout_to_zero_is_invalid() {
|
||||
let _ = parse_str_as_timeout("0", "timeout".into(), "test".into()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"InvalidTimeout { name: "timeout", reason: "timeout must not be NaN", set_by: "test" }"#]
|
||||
fn test_nan_timeouts_are_invalid() {
|
||||
let _ = parse_str_as_timeout("NaN", "timeout".into(), "test".into()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = r#"InvalidTimeout { name: "timeout", reason: "timeout must not be infinite", set_by: "test" }"#]
|
||||
fn test_infinite_timeouts_are_invalid() {
|
||||
let _ = parse_str_as_timeout("inf", "timeout".into(), "test".into()).unwrap();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue