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}