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::value::Value;
9use crate::config_bag::{ItemIter, Storable, Store, StoreReplace};
10use std::fmt;
11use std::str::FromStr;
12use std::time::Duration;
13
14const VALID_RETRY_MODES: &[RetryMode] = &[RetryMode::Standard];
15
16/// Type of error that occurred when making a request.
17#[derive(Clone, Copy, Eq, PartialEq, Debug)]
18#[non_exhaustive]
19pub enum ErrorKind {
20    /// A connection-level error.
21    ///
22    /// A `TransientError` can represent conditions such as socket timeouts, socket connection errors, or TLS negotiation timeouts.
23    ///
24    /// `TransientError` is not modeled by Smithy and is instead determined through client-specific heuristics and response status codes.
25    ///
26    /// Typically these should never be applied for non-idempotent request types
27    /// since in this scenario, it's impossible to know whether the operation had
28    /// a side effect on the server.
29    ///
30    /// TransientErrors are not currently modeled. They are determined based on specific provider
31    /// level errors & response status code.
32    TransientError,
33
34    /// An error where the server explicitly told the client to back off, such as a 429 or 503 HTTP error.
35    ThrottlingError,
36
37    /// Server error that isn't explicitly throttling but is considered by the client
38    /// to be something that should be retried.
39    ServerError,
40
41    /// Doesn't count against any budgets. This could be something like a 401 challenge in Http.
42    ClientError,
43}
44
45impl fmt::Display for ErrorKind {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::TransientError => write!(f, "transient error"),
49            Self::ThrottlingError => write!(f, "throttling error"),
50            Self::ServerError => write!(f, "server error"),
51            Self::ClientError => write!(f, "client error"),
52        }
53    }
54}
55
56/// Trait that provides an `ErrorKind` and an error code.
57pub trait ProvideErrorKind {
58    /// Returns the `ErrorKind` when the error is modeled as retryable
59    ///
60    /// If the error kind cannot be determined (e.g. the error is unmodeled at the error kind depends
61    /// on an HTTP status code, return `None`.
62    fn retryable_error_kind(&self) -> Option<ErrorKind>;
63
64    /// Returns the `code` for this error if one exists
65    fn code(&self) -> Option<&str>;
66}
67
68/// `RetryKind` describes how a request MAY be retried for a given response
69///
70/// A `RetryKind` describes how a response MAY be retried; it does not mandate retry behavior.
71/// The actual retry behavior is at the sole discretion of the RetryStrategy in place.
72/// A RetryStrategy may ignore the suggestion for a number of reasons including but not limited to:
73/// - Number of retry attempts exceeded
74/// - The required retry delay exceeds the maximum backoff configured by the client
75/// - No retry tokens are available due to service health
76#[non_exhaustive]
77#[derive(Eq, PartialEq, Debug)]
78pub enum RetryKind {
79    /// Retry the associated request due to a known `ErrorKind`.
80    Error(ErrorKind),
81
82    /// An Explicit retry (e.g. from `x-amz-retry-after`).
83    ///
84    /// Note: The specified `Duration` is considered a suggestion and may be replaced or ignored.
85    Explicit(Duration),
86
87    /// The response was a failure that should _not_ be retried.
88    UnretryableFailure,
89
90    /// The response was successful, so no retry is necessary.
91    Unnecessary,
92}
93
94/// Specifies how failed requests should be retried.
95#[non_exhaustive]
96#[derive(Eq, PartialEq, Debug, Clone, Copy)]
97pub enum RetryMode {
98    /// The standard set of retry rules across AWS SDKs. This mode includes a standard set of errors
99    /// that are retried, and support for retry quotas. The default maximum number of attempts
100    /// with this mode is three, unless otherwise explicitly configured with [`RetryConfig`].
101    Standard,
102
103    /// An experimental retry mode that includes the functionality of standard mode but includes
104    /// automatic client-side throttling. Because this mode is experimental, it might change
105    /// behavior in the future.
106    Adaptive,
107}
108
109impl FromStr for RetryMode {
110    type Err = RetryModeParseError;
111
112    fn from_str(string: &str) -> Result<Self, Self::Err> {
113        let string = string.trim();
114
115        // eq_ignore_ascii_case is OK here because the only strings we need to check for are ASCII
116        if string.eq_ignore_ascii_case("standard") {
117            Ok(RetryMode::Standard)
118        } else if string.eq_ignore_ascii_case("adaptive") {
119            Ok(RetryMode::Adaptive)
120        } else {
121            Err(RetryModeParseError::new(string))
122        }
123    }
124}
125
126/// Failure to parse a `RetryMode` from string.
127#[derive(Debug)]
128pub struct RetryModeParseError {
129    message: String,
130}
131
132impl RetryModeParseError {
133    pub(super) fn new(message: impl Into<String>) -> Self {
134        Self {
135            message: message.into(),
136        }
137    }
138}
139
140impl fmt::Display for RetryModeParseError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(
143            f,
144            "error parsing string '{}' as RetryMode, valid options are: {:#?}",
145            self.message, VALID_RETRY_MODES
146        )
147    }
148}
149
150impl std::error::Error for RetryModeParseError {}
151
152/// Builder for [`RetryConfig`].
153#[non_exhaustive]
154#[derive(Debug, Default, Clone, PartialEq)]
155pub struct RetryConfigBuilder {
156    mode: Option<RetryMode>,
157    max_attempts: Option<u32>,
158    initial_backoff: Option<Duration>,
159    max_backoff: Option<Duration>,
160    reconnect_mode: Option<ReconnectMode>,
161}
162
163impl RetryConfigBuilder {
164    /// Creates a new builder.
165    pub fn new() -> Self {
166        Default::default()
167    }
168
169    /// Sets the retry mode.
170    pub fn set_mode(&mut self, retry_mode: Option<RetryMode>) -> &mut Self {
171        self.mode = retry_mode;
172        self
173    }
174
175    /// Sets the retry mode.
176    pub fn mode(mut self, mode: RetryMode) -> Self {
177        self.set_mode(Some(mode));
178        self
179    }
180
181    /// Set the [`ReconnectMode`] for the retry strategy
182    ///
183    /// By default, when a transient error is encountered, the connection in use will be poisoned.
184    /// This prevents reusing a connection to a potentially bad host but may increase the load on
185    /// the server.
186    ///
187    /// This behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
188    pub fn reconnect_mode(mut self, reconnect_mode: ReconnectMode) -> Self {
189        self.set_reconnect_mode(Some(reconnect_mode));
190        self
191    }
192
193    /// Set the [`ReconnectMode`] for the retry strategy
194    ///
195    /// By default, when a transient error is encountered, the connection in use will be poisoned.
196    /// This prevents reusing a connection to a potentially bad host but may increase the load on
197    /// the server.
198    ///
199    /// This behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
200    pub fn set_reconnect_mode(&mut self, reconnect_mode: Option<ReconnectMode>) -> &mut Self {
201        self.reconnect_mode = reconnect_mode;
202        self
203    }
204
205    /// Sets the max attempts. This value must be greater than zero.
206    pub fn set_max_attempts(&mut self, max_attempts: Option<u32>) -> &mut Self {
207        self.max_attempts = max_attempts;
208        self
209    }
210
211    /// Sets the max attempts. This value must be greater than zero.
212    pub fn max_attempts(mut self, max_attempts: u32) -> Self {
213        self.set_max_attempts(Some(max_attempts));
214        self
215    }
216
217    /// Set the initial_backoff duration. This duration should be non-zero.
218    pub fn set_initial_backoff(&mut self, initial_backoff: Option<Duration>) -> &mut Self {
219        self.initial_backoff = initial_backoff;
220        self
221    }
222
223    /// Set the initial_backoff duration. This duration should be non-zero.
224    pub fn initial_backoff(mut self, initial_backoff: Duration) -> Self {
225        self.set_initial_backoff(Some(initial_backoff));
226        self
227    }
228
229    /// Set the max_backoff duration. This duration should be non-zero.
230    pub fn set_max_backoff(&mut self, max_backoff: Option<Duration>) -> &mut Self {
231        self.max_backoff = max_backoff;
232        self
233    }
234
235    /// Set the max_backoff duration. This duration should be non-zero.
236    pub fn max_backoff(mut self, max_backoff: Duration) -> Self {
237        self.set_max_backoff(Some(max_backoff));
238        self
239    }
240
241    /// Merge two builders together. Values from `other` will only be used as a fallback for values
242    /// from `self` Useful for merging configs from different sources together when you want to
243    /// handle "precedence" per value instead of at the config level
244    ///
245    /// # Example
246    ///
247    /// ```rust
248    /// # use aws_smithy_types::retry::{RetryMode, RetryConfigBuilder};
249    /// let a = RetryConfigBuilder::new().max_attempts(1);
250    /// let b = RetryConfigBuilder::new().max_attempts(5).mode(RetryMode::Adaptive);
251    /// let retry_config = a.take_unset_from(b).build();
252    /// // A's value take precedence over B's value
253    /// assert_eq!(retry_config.max_attempts(), 1);
254    /// // A never set a retry mode so B's value was used
255    /// assert_eq!(retry_config.mode(), RetryMode::Adaptive);
256    /// ```
257    pub fn take_unset_from(self, other: Self) -> Self {
258        Self {
259            mode: self.mode.or(other.mode),
260            max_attempts: self.max_attempts.or(other.max_attempts),
261            initial_backoff: self.initial_backoff.or(other.initial_backoff),
262            max_backoff: self.max_backoff.or(other.max_backoff),
263            reconnect_mode: self.reconnect_mode.or(other.reconnect_mode),
264        }
265    }
266
267    /// Builds a `RetryConfig`.
268    pub fn build(self) -> RetryConfig {
269        RetryConfig {
270            mode: self.mode.unwrap_or(RetryMode::Standard),
271            max_attempts: self.max_attempts.unwrap_or(3),
272            initial_backoff: self
273                .initial_backoff
274                .unwrap_or_else(|| Duration::from_secs(1)),
275            reconnect_mode: self
276                .reconnect_mode
277                .unwrap_or(ReconnectMode::ReconnectOnTransientError),
278            max_backoff: self.max_backoff.unwrap_or_else(|| Duration::from_secs(20)),
279            use_static_exponential_base: false,
280            retry_spec: None,
281        }
282    }
283}
284
285/// Retry configuration for requests.
286#[non_exhaustive]
287#[derive(Debug, Clone, PartialEq)]
288pub struct RetryConfig {
289    mode: RetryMode,
290    max_attempts: u32,
291    initial_backoff: Duration,
292    max_backoff: Duration,
293    reconnect_mode: ReconnectMode,
294    use_static_exponential_base: bool,
295    retry_spec: Option<RetrySpec>,
296}
297
298impl Storable for RetryConfig {
299    type Storer = StoreReplace<RetryConfig>;
300}
301
302/// Mode for connection re-establishment
303///
304/// By default, when a transient error is encountered, the connection in use will be poisoned. This
305/// behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
306#[derive(Debug, Clone, PartialEq, Copy)]
307pub enum ReconnectMode {
308    /// Reconnect on [`ErrorKind::TransientError`]
309    ReconnectOnTransientError,
310
311    /// Disable reconnect on error
312    ///
313    /// When this setting is applied, 503s, timeouts, and other transient errors will _not_
314    /// lead to a new connection being established unless the connection is closed by the remote.
315    ReuseAllConnections,
316}
317
318impl Storable for ReconnectMode {
319    type Storer = StoreReplace<ReconnectMode>;
320}
321
322/// Version tag for [`RetrySpec`], enabling zero-cost comparisons without
323/// exposing the internal representation.
324///
325/// New versions must be appended at the end — `PartialOrd` is derived from
326/// declaration order. If a version needs to be interleaved between
327/// existing variants (e.g., adding `V2_1_1` after `V2_2` already exists),
328/// replace the derived `Ord`/`PartialOrd` with a manual implementation
329/// that maps each variant to an explicit rank.
330#[doc(hidden)]
331#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
332#[non_exhaustive]
333pub enum RetrySpecVersion {
334    /// Retry Behavior 2.0 (legacy).
335    V2_0,
336    /// Retry Behavior 2.1.
337    V2_1,
338}
339
340/// Version-gated retry parameters derived from `BehaviorVersion`.
341///
342/// `RetrySpec` exists because `BehaviorVersion` lives in
343/// `aws-smithy-runtime-api` while `RetryConfig` lives in `aws-smithy-types`.
344/// `RetryConfig` cannot depend on `BehaviorVersion` directly without
345/// creating a circular crate dependency. Inferring the spec version from
346/// the presence or absence of individual fields would be fragile and
347/// error-prone.
348///
349/// Instead, `BehaviorVersion` is converted into a `RetrySpec` and stored
350/// alongside `RetryConfig` in the config bag. The retry strategy reads
351/// `RetrySpec` to determine version-gated behavior (backoff timing, token
352/// costs, `x-amz-retry-after` bounds) without ever depending on
353/// `BehaviorVersion`.
354///
355/// [`BehaviorVersion`]: crate::config_bag::Storable
356#[doc(hidden)]
357#[derive(Clone, Debug, PartialEq)]
358#[non_exhaustive]
359pub struct RetrySpec {
360    version: RetrySpecVersion,
361    non_throttling_initial_backoff: Duration,
362    long_polling: Option<bool>,
363}
364
365impl RetrySpec {
366    /// The version corresponding to Retry Behavior 2.0 (legacy).
367    pub const V2_0: RetrySpecVersion = RetrySpecVersion::V2_0;
368    /// The version corresponding to Retry Behavior 2.1.
369    pub const V2_1: RetrySpecVersion = RetrySpecVersion::V2_1;
370
371    /// Returns true if this spec's version is at least the given version.
372    pub fn is_at_least(&self, version: RetrySpecVersion) -> bool {
373        self.version >= version
374    }
375
376    /// Create a `RetrySpec` corresponding to Retry Behavior 2.0 (legacy).
377    pub fn v2_0() -> Self {
378        Self {
379            version: Self::V2_0,
380            non_throttling_initial_backoff: Duration::from_secs(1),
381            long_polling: None,
382        }
383    }
384
385    /// Create a `RetrySpec` corresponding to Retry Behavior 2.1.
386    pub fn v2_1() -> Self {
387        Self {
388            version: Self::V2_1,
389            non_throttling_initial_backoff: Duration::from_millis(50),
390            long_polling: None,
391        }
392    }
393
394    /// Set the base backoff for non-throttling errors.
395    pub fn with_non_throttling_initial_backoff(mut self, duration: Duration) -> Self {
396        self.non_throttling_initial_backoff = duration;
397        self
398    }
399
400    /// Get the base backoff for non-throttling errors.
401    pub fn non_throttling_initial_backoff(&self) -> Duration {
402        self.non_throttling_initial_backoff
403    }
404
405    /// Set whether this is a long-polling operation.
406    pub fn with_long_polling(mut self, long_polling: bool) -> Self {
407        self.long_polling = Some(long_polling);
408        self
409    }
410
411    /// Returns whether this is a long-polling operation.
412    pub fn long_polling(&self) -> bool {
413        self.long_polling.unwrap_or(false)
414    }
415
416    fn take_defaults_from(&mut self, other: &RetrySpec) {
417        if self.long_polling.is_none() {
418            self.long_polling = other.long_polling;
419        }
420    }
421}
422
423impl RetryConfig {
424    /// Creates a default `RetryConfig` with `RetryMode::Standard` and max attempts of three.
425    pub fn standard() -> Self {
426        Self {
427            mode: RetryMode::Standard,
428            max_attempts: 3,
429            initial_backoff: Duration::from_secs(1),
430            reconnect_mode: ReconnectMode::ReconnectOnTransientError,
431            max_backoff: Duration::from_secs(20),
432            use_static_exponential_base: false,
433            retry_spec: None,
434        }
435    }
436
437    /// Creates a default `RetryConfig` with `RetryMode::Adaptive` and max attempts of three.
438    pub fn adaptive() -> Self {
439        Self {
440            mode: RetryMode::Adaptive,
441            max_attempts: 3,
442            initial_backoff: Duration::from_secs(1),
443            reconnect_mode: ReconnectMode::ReconnectOnTransientError,
444            max_backoff: Duration::from_secs(20),
445            use_static_exponential_base: false,
446            retry_spec: None,
447        }
448    }
449
450    /// Creates a `RetryConfig` that has retries disabled.
451    pub fn disabled() -> Self {
452        Self::standard().with_max_attempts(1)
453    }
454
455    /// Set this config's [retry mode](RetryMode).
456    pub fn with_retry_mode(mut self, retry_mode: RetryMode) -> Self {
457        self.mode = retry_mode;
458        self
459    }
460
461    /// Set the maximum number of times a request should be tried, including the initial attempt.
462    /// This value must be greater than zero.
463    pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
464        self.max_attempts = max_attempts;
465        self
466    }
467
468    /// Set the [`ReconnectMode`] for the retry strategy
469    ///
470    /// By default, when a transient error is encountered, the connection in use will be poisoned.
471    /// This prevents reusing a connection to a potentially bad host but may increase the load on
472    /// the server.
473    ///
474    /// This behavior can be disabled by setting [`ReconnectMode::ReuseAllConnections`] instead.
475    pub fn with_reconnect_mode(mut self, reconnect_mode: ReconnectMode) -> Self {
476        self.reconnect_mode = reconnect_mode;
477        self
478    }
479
480    /// Set the multiplier used when calculating backoff times as part of an
481    /// [exponential backoff with jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)
482    /// strategy. Most services should work fine with the default duration of 1 second, but if you
483    /// find that your requests are taking too long due to excessive retry backoff, try lowering
484    /// this value.
485    ///
486    /// ## Example
487    ///
488    /// *For a request that gets retried 3 times, when initial_backoff is 1 seconds:*
489    /// - the first retry will occur after 0 to 1 seconds
490    /// - the second retry will occur after 0 to 2 seconds
491    /// - the third retry will occur after 0 to 4 seconds
492    ///
493    /// *For a request that gets retried 3 times, when initial_backoff is 30 milliseconds:*
494    /// - the first retry will occur after 0 to 30 milliseconds
495    /// - the second retry will occur after 0 to 60 milliseconds
496    /// - the third retry will occur after 0 to 120 milliseconds
497    pub fn with_initial_backoff(mut self, initial_backoff: Duration) -> Self {
498        self.initial_backoff = initial_backoff;
499        self
500    }
501
502    /// Set the maximum backoff time.
503    pub fn with_max_backoff(mut self, max_backoff: Duration) -> Self {
504        self.max_backoff = max_backoff;
505        self
506    }
507
508    /// Hint to the retry strategy whether to use a static exponential base.
509    ///
510    /// When a retry strategy uses exponential backoff, it calculates a random base. This causes the
511    /// retry delay to be slightly random, and helps prevent "thundering herd" scenarios. However,
512    /// it's often useful during testing to know exactly how long the delay will be.
513    ///
514    /// Therefore, if you're writing a test and asserting an expected retry delay,
515    /// set this to `true`.
516    #[cfg(feature = "test-util")]
517    pub fn with_use_static_exponential_base(mut self, use_static_exponential_base: bool) -> Self {
518        self.use_static_exponential_base = use_static_exponential_base;
519        self
520    }
521
522    /// Returns the retry mode.
523    pub fn mode(&self) -> RetryMode {
524        self.mode
525    }
526
527    /// Returns the [`ReconnectMode`]
528    pub fn reconnect_mode(&self) -> ReconnectMode {
529        self.reconnect_mode
530    }
531
532    /// Returns the max attempts.
533    pub fn max_attempts(&self) -> u32 {
534        self.max_attempts
535    }
536
537    /// Returns the backoff multiplier duration.
538    pub fn initial_backoff(&self) -> Duration {
539        self.initial_backoff
540    }
541
542    /// Returns the max backoff duration.
543    pub fn max_backoff(&self) -> Duration {
544        self.max_backoff
545    }
546
547    /// Returns true if retry is enabled with this config
548    pub fn has_retry(&self) -> bool {
549        self.max_attempts > 1
550    }
551
552    /// Returns `true` if retry strategies should use a static exponential base instead of the
553    /// default random base.
554    ///
555    /// To set this value, the `test-util` feature must be enabled.
556    pub fn use_static_exponential_base(&self) -> bool {
557        self.use_static_exponential_base
558    }
559
560    /// Set the SDK-internal retry spec.
561    #[doc(hidden)]
562    pub fn with_retry_spec(mut self, retry_spec: RetrySpec) -> Self {
563        self.retry_spec = Some(retry_spec);
564        self
565    }
566
567    /// Returns the SDK-internal retry spec, if set.
568    #[doc(hidden)]
569    pub fn retry_spec(&self) -> Option<&RetrySpec> {
570        self.retry_spec.as_ref()
571    }
572
573    fn take_defaults_from(&mut self, other: &RetryConfig) {
574        if self.retry_spec.is_none() {
575            self.retry_spec = other.retry_spec.clone();
576        } else if let (Some(mine), Some(theirs)) = (self.retry_spec.as_mut(), &other.retry_spec) {
577            mine.take_defaults_from(theirs);
578        }
579    }
580}
581
582/// Merges [`RetryConfig`] from multiple layers in the config bag.
583///
584/// This follows the same pattern as [`MergeTimeoutConfig`](crate::timeout::MergeTimeoutConfig):
585/// the highest-priority `RetryConfig` wins, but unset fields (like `retry_spec`) are
586/// filled in from lower-priority layers via `RetryConfig::take_defaults_from`.
587#[doc(hidden)]
588#[derive(Debug)]
589pub struct MergeRetryConfig;
590
591impl Storable for MergeRetryConfig {
592    type Storer = MergeRetryConfig;
593}
594
595impl Store for MergeRetryConfig {
596    type ReturnedType<'a> = RetryConfig;
597    type StoredType = <StoreReplace<RetryConfig> as Store>::StoredType;
598
599    fn merge_iter(iter: ItemIter<'_, Self>) -> Self::ReturnedType<'_> {
600        let mut result: Option<RetryConfig> = None;
601        for rc in iter {
602            match (result.as_mut(), rc) {
603                (Some(result), Value::Set(rc)) => {
604                    result.take_defaults_from(rc);
605                }
606                (None, Value::Set(rc)) => {
607                    result = Some(rc.clone());
608                }
609                (_, Value::ExplicitlyUnset(_)) => {
610                    result = Some(RetryConfig::disabled());
611                }
612            }
613        }
614        result.unwrap_or_else(RetryConfig::disabled)
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use crate::retry::{RetryConfigBuilder, RetryMode};
621    use std::str::FromStr;
622
623    #[test]
624    fn retry_config_builder_merge_with_favors_self_values_over_other_values() {
625        let self_builder = RetryConfigBuilder::new()
626            .max_attempts(1)
627            .mode(RetryMode::Adaptive);
628        let other_builder = RetryConfigBuilder::new()
629            .max_attempts(5)
630            .mode(RetryMode::Standard);
631        let retry_config = self_builder.take_unset_from(other_builder).build();
632
633        assert_eq!(retry_config.max_attempts, 1);
634        assert_eq!(retry_config.mode, RetryMode::Adaptive);
635    }
636
637    #[test]
638    fn retry_mode_from_str_parses_valid_strings_regardless_of_casing() {
639        assert_eq!(
640            RetryMode::from_str("standard").ok(),
641            Some(RetryMode::Standard)
642        );
643        assert_eq!(
644            RetryMode::from_str("STANDARD").ok(),
645            Some(RetryMode::Standard)
646        );
647        assert_eq!(
648            RetryMode::from_str("StAnDaRd").ok(),
649            Some(RetryMode::Standard)
650        );
651        // assert_eq!(
652        //     RetryMode::from_str("adaptive").ok(),
653        //     Some(RetryMode::Adaptive)
654        // );
655        // assert_eq!(
656        //     RetryMode::from_str("ADAPTIVE").ok(),
657        //     Some(RetryMode::Adaptive)
658        // );
659        // assert_eq!(
660        //     RetryMode::from_str("aDaPtIvE").ok(),
661        //     Some(RetryMode::Adaptive)
662        // );
663    }
664
665    #[test]
666    fn retry_mode_from_str_ignores_whitespace_before_and_after() {
667        assert_eq!(
668            RetryMode::from_str("  standard ").ok(),
669            Some(RetryMode::Standard)
670        );
671        assert_eq!(
672            RetryMode::from_str("   STANDARD  ").ok(),
673            Some(RetryMode::Standard)
674        );
675        assert_eq!(
676            RetryMode::from_str("  StAnDaRd   ").ok(),
677            Some(RetryMode::Standard)
678        );
679        // assert_eq!(
680        //     RetryMode::from_str("  adaptive  ").ok(),
681        //     Some(RetryMode::Adaptive)
682        // );
683        // assert_eq!(
684        //     RetryMode::from_str("   ADAPTIVE ").ok(),
685        //     Some(RetryMode::Adaptive)
686        // );
687        // assert_eq!(
688        //     RetryMode::from_str("  aDaPtIvE    ").ok(),
689        //     Some(RetryMode::Adaptive)
690        // );
691    }
692
693    #[test]
694    fn retry_mode_from_str_wont_parse_invalid_strings() {
695        assert_eq!(RetryMode::from_str("std").ok(), None);
696        assert_eq!(RetryMode::from_str("aws").ok(), None);
697        assert_eq!(RetryMode::from_str("s t a n d a r d").ok(), None);
698        assert_eq!(RetryMode::from_str("a d a p t i v e").ok(), None);
699    }
700
701    #[test]
702    fn merge_retry_config_preserves_retry_spec_from_lower_layer() {
703        use crate::config_bag::{ConfigBag, Layer};
704        use crate::retry::{MergeRetryConfig, RetryConfig, RetrySpec};
705
706        let mut lower = Layer::new("sdk_defaults");
707        lower.store_put(RetryConfig::standard().with_retry_spec(RetrySpec::v2_1()));
708        let mut upper = Layer::new("customer");
709        upper.store_put(RetryConfig::standard().with_max_attempts(5));
710        let bag = ConfigBag::of_layers(vec![lower, upper]);
711
712        let merged = bag.load::<MergeRetryConfig>();
713        assert_eq!(merged.max_attempts(), 5);
714        assert_eq!(merged.retry_spec(), Some(&RetrySpec::v2_1()));
715    }
716
717    #[test]
718    fn merge_retry_config_customer_explicit_retry_spec_wins() {
719        use crate::config_bag::{ConfigBag, Layer};
720        use crate::retry::{MergeRetryConfig, RetryConfig, RetrySpec};
721
722        let mut lower = Layer::new("sdk_defaults");
723        lower.store_put(RetryConfig::standard().with_retry_spec(RetrySpec::v2_1()));
724        let mut upper = Layer::new("customer");
725        upper.store_put(RetryConfig::standard().with_retry_spec(RetrySpec::v2_0()));
726        let bag = ConfigBag::of_layers(vec![lower, upper]);
727
728        let merged = bag.load::<MergeRetryConfig>();
729        assert_eq!(merged.retry_spec(), Some(&RetrySpec::v2_0()));
730    }
731
732    #[test]
733    fn merge_retry_config_long_polling_from_operation_layer() {
734        use crate::config_bag::{ConfigBag, Layer};
735        use crate::retry::{MergeRetryConfig, RetryConfig, RetrySpec};
736
737        let mut lower = Layer::new("sdk_defaults");
738        lower.store_put(RetryConfig::standard().with_retry_spec(RetrySpec::v2_1()));
739        let mut upper = Layer::new("operation");
740        upper.store_put(
741            RetryConfig::standard().with_retry_spec(RetrySpec::v2_1().with_long_polling(true)),
742        );
743        let bag = ConfigBag::of_layers(vec![lower, upper]);
744
745        let merged = bag.load::<MergeRetryConfig>();
746        assert!(merged.retry_spec().unwrap().long_polling());
747    }
748}