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}