aws_smithy_types/
retry.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! This module defines types that describe when to retry given a response.
7
8use crate::config_bag::{Storable, StoreReplace};
9use std::fmt;
10use std::str::FromStr;
11use std::time::Duration;
12
13const VALID_RETRY_MODES: &[RetryMode] = &[RetryMode::Standard];
14
15/// Type of error that occurred when making a request.
16#[derive(Clone, Copy, Eq, PartialEq, Debug)]
17#[non_exhaustive]
18pub enum ErrorKind {
19    /// A connection-level error.
20    ///
21    /// A `TransientError` can represent conditions such as socket timeouts, socket connection errors, or TLS negotiation timeouts.
22    ///
23    /// `TransientError` is not modeled by Smithy and is instead determined through client-specific heuristics and response status codes.
24    ///
25    /// Typically these should never be applied for non-idempotent request types
26    /// since in this scenario, it's impossible to know whether the operation had
27    /// a side effect on the server.
28    ///
29    /// TransientErrors are not currently modeled. They are determined based on specific provider
30    /// level errors & response status code.
31    TransientError,
32
33    /// An error where the server explicitly told the client to back off, such as a 429 or 503 HTTP error.
34    ThrottlingError,
35
36    /// Server error that isn't explicitly throttling but is considered by the client
37    /// to be something that should be retried.
38    ServerError,
39
40    /// Doesn't count against any budgets. This could be something like a 401 challenge in Http.
41    ClientError,
42}
43
44impl fmt::Display for ErrorKind {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::TransientError => write!(f, "transient error"),
48            Self::ThrottlingError => write!(f, "throttling error"),
49            Self::ServerError => write!(f, "server error"),
50            Self::ClientError => write!(f, "client error"),
51        }
52    }
53}
54
55/// Trait that provides an `ErrorKind` and an error code.
56pub trait ProvideErrorKind {
57    /// Returns the `ErrorKind` when the error is modeled as retryable
58    ///
59    /// If the error kind cannot be determined (e.g. the error is unmodeled at the error kind depends
60    /// on an HTTP status code, return `None`.
61    fn retryable_error_kind(&self) -> Option<ErrorKind>;
62
63    /// Returns the `code` for this error if one exists
64    fn code(&self) -> Option<&str>;
65}
66
67/// `RetryKind` describes how a request MAY be retried for a given response
68///
69/// A `RetryKind` describes how a response MAY be retried; it does not mandate retry behavior.
70/// The actual retry behavior is at the sole discretion of the RetryStrategy in place.
71/// A RetryStrategy may ignore the suggestion for a number of reasons including but not limited to:
72/// - Number of retry attempts exceeded
73/// - The required retry delay exceeds the maximum backoff configured by the client
74/// - No retry tokens are available due to service health
75#[non_exhaustive]
76#[derive(Eq, PartialEq, Debug)]
77pub enum RetryKind {
78    /// Retry the associated request due to a known `ErrorKind`.
79    Error(ErrorKind),
80
81    /// An Explicit retry (e.g. from `x-amz-retry-after`).
82    ///
83    /// Note: The specified `Duration` is considered a suggestion and may be replaced or ignored.
84    Explicit(Duration),
85
86    /// The response was a failure that should _not_ be retried.
87    UnretryableFailure,
88
89    /// The response was successful, so no retry is necessary.
90    Unnecessary,
91}
92
93/// Specifies how failed requests should be retried.
94#[non_exhaustive]
95#[derive(Eq, PartialEq, Debug, Clone, Copy)]
96pub enum RetryMode {
97    /// The standard set of retry rules across AWS SDKs. This mode includes a standard set of errors
98    /// that are retried, and support for retry quotas. The default maximum number of attempts
99    /// with this mode is three, unless otherwise explicitly configured with [`RetryConfig`].
100    Standard,
101
102    /// An experimental retry mode that includes the functionality of standard mode but includes
103    /// automatic client-side throttling. Because this mode is experimental, it might change
104    /// behavior in the future.
105    Adaptive,
106}
107
108impl FromStr for RetryMode {
109    type Err = RetryModeParseError;
110
111    fn from_str(string: &str) -> Result<Self, Self::Err> {
112        let string = string.trim();
113
114        // eq_ignore_ascii_case is OK here because the only strings we need to check for are ASCII
115        if string.eq_ignore_ascii_case("standard") {
116            Ok(RetryMode::Standard)
117        } else if string.eq_ignore_ascii_case("adaptive") {
118            Ok(RetryMode::Adaptive)
119        } else {
120            Err(RetryModeParseError::new(string))
121        }
122    }
123}
124
125/// Failure to parse a `RetryMode` from string.
126#[derive(Debug)]
127pub struct RetryModeParseError {
128    message: String,
129}
130
131impl RetryModeParseError {
132    pub(super) fn new(message: impl Into<String>) -> Self {
133        Self {
134            message: message.into(),
135        }
136    }
137}
138
139impl fmt::Display for RetryModeParseError {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(
142            f,
143            "error parsing string '{}' as RetryMode, valid options are: {:#?}",
144            self.message, VALID_RETRY_MODES
145        )
146    }
147}
148
149impl std::error::Error for RetryModeParseError {}
150
151/// Builder for [`RetryConfig`].
152#[non_exhaustive]
153#[derive(Debug, Default, Clone, PartialEq)]
154pub struct RetryConfigBuilder {
155    mode: Option<RetryMode>,
156    max_attempts: Option<u32>,
157    initial_backoff: Option<Duration>,
158    max_backoff: Option<Duration>,
159    reconnect_mode: Option<ReconnectMode>,
160}
161
162impl RetryConfigBuilder {
163    /// Creates a new builder.
164    pub fn new() -> Self {
165        Default::default()
166    }
167
168    /// Sets the retry mode.
169    pub fn set_mode(&mut self, retry_mode: Option<RetryMode>) -> &mut Self {
170        self.mode = retry_mode;
171        self
172    }
173
174    /// Sets the retry mode.
175    pub fn mode(mut self, mode: RetryMode) -> Self {
176        self.set_mode(Some(mode));
177        self
178    }
179
180    /// Set the [`ReconnectMode`] for the retry strategy
181    ///
182    /// By default, when a transient error is encountered, the connection in use will be poisoned.
183    /// This prevents reusing a connection to a potentially bad host but may increase the load on
184    /// the server.
185    ///
186    /// This behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
187    pub fn reconnect_mode(mut self, reconnect_mode: ReconnectMode) -> Self {
188        self.set_reconnect_mode(Some(reconnect_mode));
189        self
190    }
191
192    /// Set the [`ReconnectMode`] for the retry strategy
193    ///
194    /// By default, when a transient error is encountered, the connection in use will be poisoned.
195    /// This prevents reusing a connection to a potentially bad host but may increase the load on
196    /// the server.
197    ///
198    /// This behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
199    pub fn set_reconnect_mode(&mut self, reconnect_mode: Option<ReconnectMode>) -> &mut Self {
200        self.reconnect_mode = reconnect_mode;
201        self
202    }
203
204    /// Sets the max attempts. This value must be greater than zero.
205    pub fn set_max_attempts(&mut self, max_attempts: Option<u32>) -> &mut Self {
206        self.max_attempts = max_attempts;
207        self
208    }
209
210    /// Sets the max attempts. This value must be greater than zero.
211    pub fn max_attempts(mut self, max_attempts: u32) -> Self {
212        self.set_max_attempts(Some(max_attempts));
213        self
214    }
215
216    /// Set the initial_backoff duration. This duration should be non-zero.
217    pub fn set_initial_backoff(&mut self, initial_backoff: Option<Duration>) -> &mut Self {
218        self.initial_backoff = initial_backoff;
219        self
220    }
221
222    /// Set the initial_backoff duration. This duration should be non-zero.
223    pub fn initial_backoff(mut self, initial_backoff: Duration) -> Self {
224        self.set_initial_backoff(Some(initial_backoff));
225        self
226    }
227
228    /// Set the max_backoff duration. This duration should be non-zero.
229    pub fn set_max_backoff(&mut self, max_backoff: Option<Duration>) -> &mut Self {
230        self.max_backoff = max_backoff;
231        self
232    }
233
234    /// Set the max_backoff duration. This duration should be non-zero.
235    pub fn max_backoff(mut self, max_backoff: Duration) -> Self {
236        self.set_max_backoff(Some(max_backoff));
237        self
238    }
239
240    /// Merge two builders together. Values from `other` will only be used as a fallback for values
241    /// from `self` Useful for merging configs from different sources together when you want to
242    /// handle "precedence" per value instead of at the config level
243    ///
244    /// # Example
245    ///
246    /// ```rust
247    /// # use aws_smithy_types::retry::{RetryMode, RetryConfigBuilder};
248    /// let a = RetryConfigBuilder::new().max_attempts(1);
249    /// let b = RetryConfigBuilder::new().max_attempts(5).mode(RetryMode::Adaptive);
250    /// let retry_config = a.take_unset_from(b).build();
251    /// // A's value take precedence over B's value
252    /// assert_eq!(retry_config.max_attempts(), 1);
253    /// // A never set a retry mode so B's value was used
254    /// assert_eq!(retry_config.mode(), RetryMode::Adaptive);
255    /// ```
256    pub fn take_unset_from(self, other: Self) -> Self {
257        Self {
258            mode: self.mode.or(other.mode),
259            max_attempts: self.max_attempts.or(other.max_attempts),
260            initial_backoff: self.initial_backoff.or(other.initial_backoff),
261            max_backoff: self.max_backoff.or(other.max_backoff),
262            reconnect_mode: self.reconnect_mode.or(other.reconnect_mode),
263        }
264    }
265
266    /// Builds a `RetryConfig`.
267    pub fn build(self) -> RetryConfig {
268        RetryConfig {
269            mode: self.mode.unwrap_or(RetryMode::Standard),
270            max_attempts: self.max_attempts.unwrap_or(3),
271            initial_backoff: self
272                .initial_backoff
273                .unwrap_or_else(|| Duration::from_secs(1)),
274            reconnect_mode: self
275                .reconnect_mode
276                .unwrap_or(ReconnectMode::ReconnectOnTransientError),
277            max_backoff: self.max_backoff.unwrap_or_else(|| Duration::from_secs(20)),
278            use_static_exponential_base: false,
279        }
280    }
281}
282
283/// Retry configuration for requests.
284#[non_exhaustive]
285#[derive(Debug, Clone, PartialEq)]
286pub struct RetryConfig {
287    mode: RetryMode,
288    max_attempts: u32,
289    initial_backoff: Duration,
290    max_backoff: Duration,
291    reconnect_mode: ReconnectMode,
292    use_static_exponential_base: bool,
293}
294
295impl Storable for RetryConfig {
296    type Storer = StoreReplace<RetryConfig>;
297}
298
299/// Mode for connection re-establishment
300///
301/// By default, when a transient error is encountered, the connection in use will be poisoned. This
302/// behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
303#[derive(Debug, Clone, PartialEq, Copy)]
304pub enum ReconnectMode {
305    /// Reconnect on [`ErrorKind::TransientError`]
306    ReconnectOnTransientError,
307
308    /// Disable reconnect on error
309    ///
310    /// When this setting is applied, 503s, timeouts, and other transient errors will _not_
311    /// lead to a new connection being established unless the connection is closed by the remote.
312    ReuseAllConnections,
313}
314
315impl Storable for ReconnectMode {
316    type Storer = StoreReplace<ReconnectMode>;
317}
318
319impl RetryConfig {
320    /// Creates a default `RetryConfig` with `RetryMode::Standard` and max attempts of three.
321    pub fn standard() -> Self {
322        Self {
323            mode: RetryMode::Standard,
324            max_attempts: 3,
325            initial_backoff: Duration::from_secs(1),
326            reconnect_mode: ReconnectMode::ReconnectOnTransientError,
327            max_backoff: Duration::from_secs(20),
328            use_static_exponential_base: false,
329        }
330    }
331
332    /// Creates a default `RetryConfig` with `RetryMode::Adaptive` and max attempts of three.
333    pub fn adaptive() -> Self {
334        Self {
335            mode: RetryMode::Adaptive,
336            max_attempts: 3,
337            initial_backoff: Duration::from_secs(1),
338            reconnect_mode: ReconnectMode::ReconnectOnTransientError,
339            max_backoff: Duration::from_secs(20),
340            use_static_exponential_base: false,
341        }
342    }
343
344    /// Creates a `RetryConfig` that has retries disabled.
345    pub fn disabled() -> Self {
346        Self::standard().with_max_attempts(1)
347    }
348
349    /// Set this config's [retry mode](RetryMode).
350    pub fn with_retry_mode(mut self, retry_mode: RetryMode) -> Self {
351        self.mode = retry_mode;
352        self
353    }
354
355    /// Set the maximum number of times a request should be tried, including the initial attempt.
356    /// This value must be greater than zero.
357    pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
358        self.max_attempts = max_attempts;
359        self
360    }
361
362    /// Set the [`ReconnectMode`] for the retry strategy
363    ///
364    /// By default, when a transient error is encountered, the connection in use will be poisoned.
365    /// This prevents reusing a connection to a potentially bad host but may increase the load on
366    /// the server.
367    ///
368    /// This behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
369    pub fn with_reconnect_mode(mut self, reconnect_mode: ReconnectMode) -> Self {
370        self.reconnect_mode = reconnect_mode;
371        self
372    }
373
374    /// Set the multiplier used when calculating backoff times as part of an
375    /// [exponential backoff with jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)
376    /// strategy. Most services should work fine with the default duration of 1 second, but if you
377    /// find that your requests are taking too long due to excessive retry backoff, try lowering
378    /// this value.
379    ///
380    /// ## Example
381    ///
382    /// *For a request that gets retried 3 times, when initial_backoff is 1 seconds:*
383    /// - the first retry will occur after 0 to 1 seconds
384    /// - the second retry will occur after 0 to 2 seconds
385    /// - the third retry will occur after 0 to 4 seconds
386    ///
387    /// *For a request that gets retried 3 times, when initial_backoff is 30 milliseconds:*
388    /// - the first retry will occur after 0 to 30 milliseconds
389    /// - the second retry will occur after 0 to 60 milliseconds
390    /// - the third retry will occur after 0 to 120 milliseconds
391    pub fn with_initial_backoff(mut self, initial_backoff: Duration) -> Self {
392        self.initial_backoff = initial_backoff;
393        self
394    }
395
396    /// Set the maximum backoff time.
397    pub fn with_max_backoff(mut self, max_backoff: Duration) -> Self {
398        self.max_backoff = max_backoff;
399        self
400    }
401
402    /// Hint to the retry strategy whether to use a static exponential base.
403    ///
404    /// When a retry strategy uses exponential backoff, it calculates a random base. This causes the
405    /// retry delay to be slightly random, and helps prevent "thundering herd" scenarios. However,
406    /// it's often useful during testing to know exactly how long the delay will be.
407    ///
408    /// Therefore, if you're writing a test and asserting an expected retry delay,
409    /// set this to `true`.
410    #[cfg(feature = "test-util")]
411    pub fn with_use_static_exponential_base(mut self, use_static_exponential_base: bool) -> Self {
412        self.use_static_exponential_base = use_static_exponential_base;
413        self
414    }
415
416    /// Returns the retry mode.
417    pub fn mode(&self) -> RetryMode {
418        self.mode
419    }
420
421    /// Returns the [`ReconnectMode`]
422    pub fn reconnect_mode(&self) -> ReconnectMode {
423        self.reconnect_mode
424    }
425
426    /// Returns the max attempts.
427    pub fn max_attempts(&self) -> u32 {
428        self.max_attempts
429    }
430
431    /// Returns the backoff multiplier duration.
432    pub fn initial_backoff(&self) -> Duration {
433        self.initial_backoff
434    }
435
436    /// Returns the max backoff duration.
437    pub fn max_backoff(&self) -> Duration {
438        self.max_backoff
439    }
440
441    /// Returns true if retry is enabled with this config
442    pub fn has_retry(&self) -> bool {
443        self.max_attempts > 1
444    }
445
446    /// Returns `true` if retry strategies should use a static exponential base instead of the
447    /// default random base.
448    ///
449    /// To set this value, the `test-util` feature must be enabled.
450    pub fn use_static_exponential_base(&self) -> bool {
451        self.use_static_exponential_base
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use crate::retry::{RetryConfigBuilder, RetryMode};
458    use std::str::FromStr;
459
460    #[test]
461    fn retry_config_builder_merge_with_favors_self_values_over_other_values() {
462        let self_builder = RetryConfigBuilder::new()
463            .max_attempts(1)
464            .mode(RetryMode::Adaptive);
465        let other_builder = RetryConfigBuilder::new()
466            .max_attempts(5)
467            .mode(RetryMode::Standard);
468        let retry_config = self_builder.take_unset_from(other_builder).build();
469
470        assert_eq!(retry_config.max_attempts, 1);
471        assert_eq!(retry_config.mode, RetryMode::Adaptive);
472    }
473
474    #[test]
475    fn retry_mode_from_str_parses_valid_strings_regardless_of_casing() {
476        assert_eq!(
477            RetryMode::from_str("standard").ok(),
478            Some(RetryMode::Standard)
479        );
480        assert_eq!(
481            RetryMode::from_str("STANDARD").ok(),
482            Some(RetryMode::Standard)
483        );
484        assert_eq!(
485            RetryMode::from_str("StAnDaRd").ok(),
486            Some(RetryMode::Standard)
487        );
488        // assert_eq!(
489        //     RetryMode::from_str("adaptive").ok(),
490        //     Some(RetryMode::Adaptive)
491        // );
492        // assert_eq!(
493        //     RetryMode::from_str("ADAPTIVE").ok(),
494        //     Some(RetryMode::Adaptive)
495        // );
496        // assert_eq!(
497        //     RetryMode::from_str("aDaPtIvE").ok(),
498        //     Some(RetryMode::Adaptive)
499        // );
500    }
501
502    #[test]
503    fn retry_mode_from_str_ignores_whitespace_before_and_after() {
504        assert_eq!(
505            RetryMode::from_str("  standard ").ok(),
506            Some(RetryMode::Standard)
507        );
508        assert_eq!(
509            RetryMode::from_str("   STANDARD  ").ok(),
510            Some(RetryMode::Standard)
511        );
512        assert_eq!(
513            RetryMode::from_str("  StAnDaRd   ").ok(),
514            Some(RetryMode::Standard)
515        );
516        // assert_eq!(
517        //     RetryMode::from_str("  adaptive  ").ok(),
518        //     Some(RetryMode::Adaptive)
519        // );
520        // assert_eq!(
521        //     RetryMode::from_str("   ADAPTIVE ").ok(),
522        //     Some(RetryMode::Adaptive)
523        // );
524        // assert_eq!(
525        //     RetryMode::from_str("  aDaPtIvE    ").ok(),
526        //     Some(RetryMode::Adaptive)
527        // );
528    }
529
530    #[test]
531    fn retry_mode_from_str_wont_parse_invalid_strings() {
532        assert_eq!(RetryMode::from_str("std").ok(), None);
533        assert_eq!(RetryMode::from_str("aws").ok(), None);
534        assert_eq!(RetryMode::from_str("s t a n d a r d").ok(), None);
535        assert_eq!(RetryMode::from_str("a d a p t i v e").ok(), None);
536    }
537}