aws_config/default_provider/
retry_config.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::provider_config::ProviderConfig;
7use crate::retry::error::{RetryConfigError, RetryConfigErrorKind};
8use aws_runtime::env_config::{EnvConfigError, EnvConfigValue};
9use aws_smithy_types::error::display::DisplayErrorContext;
10use aws_smithy_types::retry::{RetryConfig, RetryMode};
11use std::str::FromStr;
12
13/// Default RetryConfig Provider chain
14///
15/// Unlike other "providers" `RetryConfig` has no related `RetryConfigProvider` trait. Instead,
16/// a builder struct is returned which has a similar API.
17///
18/// This provider will check the following sources in order:
19/// 1. Environment variables: `AWS_MAX_ATTEMPTS` & `AWS_RETRY_MODE`
20/// 2. Profile file: `max_attempts` and `retry_mode`
21///
22/// # Example
23///
24/// When running [`aws_config::from_env()`](crate::from_env()), a [`ConfigLoader`](crate::ConfigLoader)
25/// is created that will then create a [`RetryConfig`] from the default_provider. There is no
26/// need to call `default_provider` and the example below is only for illustration purposes.
27///
28/// ```no_run
29/// # use std::error::Error;
30/// # #[tokio::main]
31/// # async fn main() -> Result<(), Box<dyn Error>> {
32/// use aws_config::default_provider::retry_config;
33///
34/// // Load a retry config from a specific profile
35/// let retry_config = retry_config::default_provider()
36///     .profile_name("other_profile")
37///     .retry_config()
38///     .await;
39/// let config = aws_config::from_env()
40///     // Override the retry config set by the default profile
41///     .retry_config(retry_config)
42///     .load()
43///     .await;
44/// // instantiate a service client:
45/// // <my_aws_service>::Client::new(&config);
46/// #     Ok(())
47/// # }
48/// ```
49pub fn default_provider() -> Builder {
50    Builder::default()
51}
52
53mod env {
54    pub(super) const MAX_ATTEMPTS: &str = "AWS_MAX_ATTEMPTS";
55    pub(super) const RETRY_MODE: &str = "AWS_RETRY_MODE";
56}
57
58mod profile_keys {
59    pub(super) const MAX_ATTEMPTS: &str = "max_attempts";
60    pub(super) const RETRY_MODE: &str = "retry_mode";
61}
62
63/// Builder for RetryConfig that checks the environment and aws profile for configuration
64#[derive(Debug, Default)]
65pub struct Builder {
66    provider_config: ProviderConfig,
67}
68
69impl Builder {
70    /// Configure the default chain
71    ///
72    /// Exposed for overriding the environment when unit-testing providers
73    pub fn configure(mut self, configuration: &ProviderConfig) -> Self {
74        self.provider_config = configuration.clone();
75        self
76    }
77
78    /// Override the profile name used by this provider
79    pub fn profile_name(mut self, name: &str) -> Self {
80        self.provider_config = self.provider_config.with_profile_name(name.to_string());
81        self
82    }
83
84    /// Attempt to create a [`RetryConfig`] from following sources in order:
85    /// 1. Environment variables: `AWS_MAX_ATTEMPTS` & `AWS_RETRY_MODE`
86    /// 2. Profile file: `max_attempts` and `retry_mode`
87    /// 3. [RetryConfig::standard()](aws_smithy_types::retry::RetryConfig::standard)
88    ///
89    /// Precedence is considered on a per-field basis
90    ///
91    /// # Panics
92    ///
93    /// - Panics if the `AWS_MAX_ATTEMPTS` env var or `max_attempts` profile var is set to 0
94    /// - Panics if the `AWS_RETRY_MODE` env var or `retry_mode` profile var is set to "adaptive" (it's not yet supported)
95    pub async fn retry_config(self) -> RetryConfig {
96        match self.try_retry_config().await {
97            Ok(conf) => conf,
98            Err(e) => panic!("{}", DisplayErrorContext(e)),
99        }
100    }
101
102    pub(crate) async fn try_retry_config(
103        self,
104    ) -> Result<RetryConfig, EnvConfigError<RetryConfigError>> {
105        let env = self.provider_config.env();
106        let profiles = self.provider_config.profile().await;
107        // Both of these can return errors due to invalid config settings, and we want to surface those as early as possible
108        // hence, we'll panic if any config values are invalid (missing values are OK though)
109        // We match this instead of unwrapping, so we can print the error with the `Display` impl instead of the `Debug` impl that unwrap uses
110        let mut retry_config = RetryConfig::standard();
111        let max_attempts = EnvConfigValue::new()
112            .env(env::MAX_ATTEMPTS)
113            .profile(profile_keys::MAX_ATTEMPTS)
114            .validate(&env, profiles, validate_max_attempts);
115
116        let retry_mode = EnvConfigValue::new()
117            .env(env::RETRY_MODE)
118            .profile(profile_keys::RETRY_MODE)
119            .validate(&env, profiles, |s| {
120                RetryMode::from_str(s)
121                    .map_err(|err| RetryConfigErrorKind::InvalidRetryMode { source: err }.into())
122            });
123
124        if let Some(max_attempts) = max_attempts? {
125            retry_config = retry_config.with_max_attempts(max_attempts);
126        }
127
128        if let Some(retry_mode) = retry_mode? {
129            retry_config = retry_config.with_retry_mode(retry_mode);
130        }
131
132        Ok(retry_config)
133    }
134}
135
136fn validate_max_attempts(max_attempts: &str) -> Result<u32, RetryConfigError> {
137    match max_attempts.parse::<u32>() {
138        Ok(0) => Err(RetryConfigErrorKind::MaxAttemptsMustNotBeZero.into()),
139        Ok(max_attempts) => Ok(max_attempts),
140        Err(source) => Err(RetryConfigErrorKind::FailedToParseMaxAttempts { source }.into()),
141    }
142}
143
144#[cfg(test)]
145mod test {
146    use crate::default_provider::retry_config::env;
147    use crate::provider_config::ProviderConfig;
148    use crate::retry::{
149        error::RetryConfigError, error::RetryConfigErrorKind, RetryConfig, RetryMode,
150    };
151    use aws_runtime::env_config::EnvConfigError;
152    use aws_types::os_shim_internal::{Env, Fs};
153
154    async fn test_provider(
155        vars: &[(&str, &str)],
156    ) -> Result<RetryConfig, EnvConfigError<RetryConfigError>> {
157        super::Builder::default()
158            .configure(&ProviderConfig::no_configuration().with_env(Env::from_slice(vars)))
159            .try_retry_config()
160            .await
161    }
162
163    #[tokio::test]
164    async fn test_returns_default_retry_config_from_empty_profile() {
165        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
166        let fs = Fs::from_slice(&[("config", "[default]\n")]);
167
168        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
169
170        let actual_retry_config = super::default_provider()
171            .configure(&provider_config)
172            .retry_config()
173            .await;
174
175        let expected_retry_config = RetryConfig::standard();
176
177        assert_eq!(actual_retry_config, expected_retry_config);
178        // This is redundant, but it's really important to make sure that
179        // we're setting these exact values by default, so we check twice
180        assert_eq!(actual_retry_config.max_attempts(), 3);
181        assert_eq!(actual_retry_config.mode(), RetryMode::Standard);
182    }
183
184    #[tokio::test]
185    async fn test_no_retry_config_in_empty_profile() {
186        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
187        let fs = Fs::from_slice(&[("config", "[default]\n")]);
188
189        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
190
191        let actual_retry_config = super::default_provider()
192            .configure(&provider_config)
193            .retry_config()
194            .await;
195
196        let expected_retry_config = RetryConfig::standard();
197
198        assert_eq!(actual_retry_config, expected_retry_config)
199    }
200
201    #[tokio::test]
202    async fn test_creation_of_retry_config_from_profile() {
203        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
204        // TODO(https://github.com/awslabs/aws-sdk-rust/issues/247): standard is the default mode;
205        // this test would be better if it was setting it to adaptive mode
206        // adaptive mode is currently unsupported so that would panic
207        let fs = Fs::from_slice(&[(
208            "config",
209            // If the lines with the vars have preceding spaces, they don't get read
210            r#"[default]
211max_attempts = 1
212retry_mode = standard
213            "#,
214        )]);
215
216        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
217
218        let actual_retry_config = super::default_provider()
219            .configure(&provider_config)
220            .retry_config()
221            .await;
222
223        let expected_retry_config = RetryConfig::standard().with_max_attempts(1);
224
225        assert_eq!(actual_retry_config, expected_retry_config)
226    }
227
228    #[tokio::test]
229    async fn test_env_retry_config_takes_precedence_over_profile_retry_config() {
230        let env = Env::from_slice(&[
231            ("AWS_CONFIG_FILE", "config"),
232            ("AWS_MAX_ATTEMPTS", "42"),
233            ("AWS_RETRY_MODE", "standard"),
234        ]);
235        // TODO(https://github.com/awslabs/aws-sdk-rust/issues/247) standard is the default mode;
236        // this test would be better if it was setting it to adaptive mode
237        // adaptive mode is currently unsupported so that would panic
238        let fs = Fs::from_slice(&[(
239            "config",
240            // If the lines with the vars have preceding spaces, they don't get read
241            r#"[default]
242max_attempts = 88
243retry_mode = standard
244            "#,
245        )]);
246
247        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
248
249        let actual_retry_config = super::default_provider()
250            .configure(&provider_config)
251            .retry_config()
252            .await;
253
254        let expected_retry_config = RetryConfig::standard().with_max_attempts(42);
255
256        assert_eq!(actual_retry_config, expected_retry_config)
257    }
258
259    #[tokio::test]
260    #[should_panic = "failed to parse max attempts. source: global profile (`default`) key: `max_attempts`: invalid digit found in string"]
261    async fn test_invalid_profile_retry_config_panics() {
262        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
263        let fs = Fs::from_slice(&[(
264            "config",
265            // If the lines with the vars have preceding spaces, they don't get read
266            r#"[default]
267max_attempts = potato
268            "#,
269        )]);
270
271        let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
272
273        let _ = super::default_provider()
274            .configure(&provider_config)
275            .retry_config()
276            .await;
277    }
278
279    #[tokio::test]
280    async fn defaults() {
281        let built = test_provider(&[]).await.unwrap();
282
283        assert_eq!(built.mode(), RetryMode::Standard);
284        assert_eq!(built.max_attempts(), 3);
285    }
286
287    #[tokio::test]
288    async fn max_attempts_is_read_correctly() {
289        assert_eq!(
290            test_provider(&[(env::MAX_ATTEMPTS, "88")]).await.unwrap(),
291            RetryConfig::standard().with_max_attempts(88)
292        );
293    }
294
295    #[tokio::test]
296    async fn max_attempts_errors_when_it_cant_be_parsed_as_an_integer() {
297        assert!(matches!(
298            test_provider(&[(env::MAX_ATTEMPTS, "not an integer")])
299                .await
300                .unwrap_err()
301                .err(),
302            RetryConfigError {
303                kind: RetryConfigErrorKind::FailedToParseMaxAttempts { .. }
304            }
305        ));
306    }
307
308    #[tokio::test]
309    async fn retry_mode_is_read_correctly() {
310        assert_eq!(
311            test_provider(&[(env::RETRY_MODE, "standard")])
312                .await
313                .unwrap(),
314            RetryConfig::standard()
315        );
316    }
317
318    #[tokio::test]
319    async fn both_fields_can_be_set_at_once() {
320        assert_eq!(
321            test_provider(&[(env::RETRY_MODE, "standard"), (env::MAX_ATTEMPTS, "13")])
322                .await
323                .unwrap(),
324            RetryConfig::standard().with_max_attempts(13)
325        );
326    }
327
328    #[tokio::test]
329    async fn disallow_zero_max_attempts() {
330        let err = test_provider(&[(env::MAX_ATTEMPTS, "0")])
331            .await
332            .unwrap_err();
333        let err = err.err();
334        assert!(matches!(
335            err,
336            RetryConfigError {
337                kind: RetryConfigErrorKind::MaxAttemptsMustNotBeZero { .. }
338            }
339        ));
340    }
341}