aws_smithy_runtime_api/client/retries/
classifiers.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Classifiers for determining if a retry is necessary and related code.
7//!
8//! When a request fails, a retry strategy should inspect the result with retry
9//! classifiers to understand if and how the request should be retried.
10//!
11//! Because multiple classifiers are often used, and because some are more
12//! specific than others in what they identify as retryable, classifiers are
13//! run in a sequence that is determined by their priority.
14//!
15//! Classifiers that are higher priority are run **after** classifiers
16//! with a lower priority. The intention is that:
17//!
18//! 1. Generic classifiers that look at things like the HTTP error code run
19//!     first.
20//! 2. More specific classifiers such as ones that check for certain error
21//!     messages are run **after** the generic classifiers. This gives them the
22//!     ability to override the actions set by the generic retry classifiers.
23//!
24//! Put another way:
25//!
26//! | large nets target common failures with basic behavior | run before            | small nets target specific failures with special behavior|
27//! |-------------------------------------------------------|-----------------------|----------------------------------------------------------|
28//! | low priority classifiers                              | results overridden by | high priority classifiers                                |
29
30use crate::box_error::BoxError;
31use crate::client::interceptors::context::InterceptorContext;
32use crate::client::runtime_components::sealed::ValidateConfig;
33use crate::client::runtime_components::RuntimeComponents;
34use crate::impl_shared_conversions;
35use aws_smithy_types::config_bag::ConfigBag;
36use aws_smithy_types::retry::ErrorKind;
37use std::fmt;
38use std::sync::Arc;
39use std::time::Duration;
40
41/// The result of running a [`ClassifyRetry`] on a [`InterceptorContext`].
42#[non_exhaustive]
43#[derive(Clone, Eq, PartialEq, Debug, Default)]
44pub enum RetryAction {
45    /// When a classifier can't run or has no opinion, this action is returned.
46    ///
47    /// For example, if a classifier requires a parsed response and response parsing failed,
48    /// this action is returned. If all classifiers return this action, no retry should be
49    /// attempted.
50    #[default]
51    NoActionIndicated,
52    /// When a classifier runs and thinks a response should be retried, this action is returned.
53    RetryIndicated(RetryReason),
54    /// When a classifier runs and decides a response must not be retried, this action is returned.
55    ///
56    /// This action stops retry classification immediately, skipping any following classifiers.
57    RetryForbidden,
58}
59
60impl fmt::Display for RetryAction {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Self::NoActionIndicated => write!(f, "no action indicated"),
64            Self::RetryForbidden => write!(f, "retry forbidden"),
65            Self::RetryIndicated(reason) => write!(f, "retry {reason}"),
66        }
67    }
68}
69
70impl RetryAction {
71    /// Create a new `RetryAction` indicating that a retry is necessary.
72    pub fn retryable_error(kind: ErrorKind) -> Self {
73        Self::RetryIndicated(RetryReason::RetryableError {
74            kind,
75            retry_after: None,
76        })
77    }
78
79    /// Create a new `RetryAction` indicating that a retry is necessary after an explicit delay.
80    pub fn retryable_error_with_explicit_delay(kind: ErrorKind, retry_after: Duration) -> Self {
81        Self::RetryIndicated(RetryReason::RetryableError {
82            kind,
83            retry_after: Some(retry_after),
84        })
85    }
86
87    /// Create a new `RetryAction` indicating that a retry is necessary because of a transient error.
88    pub fn transient_error() -> Self {
89        Self::retryable_error(ErrorKind::TransientError)
90    }
91
92    /// Create a new `RetryAction` indicating that a retry is necessary because of a throttling error.
93    pub fn throttling_error() -> Self {
94        Self::retryable_error(ErrorKind::ThrottlingError)
95    }
96
97    /// Create a new `RetryAction` indicating that a retry is necessary because of a server error.
98    pub fn server_error() -> Self {
99        Self::retryable_error(ErrorKind::ServerError)
100    }
101
102    /// Create a new `RetryAction` indicating that a retry is necessary because of a client error.
103    pub fn client_error() -> Self {
104        Self::retryable_error(ErrorKind::ClientError)
105    }
106
107    /// Check if a retry is indicated.
108    pub fn should_retry(&self) -> bool {
109        match self {
110            Self::NoActionIndicated | Self::RetryForbidden => false,
111            Self::RetryIndicated(_) => true,
112        }
113    }
114}
115
116/// The reason for a retry.
117#[non_exhaustive]
118#[derive(Clone, Eq, PartialEq, Debug)]
119pub enum RetryReason {
120    /// When an error is received that should be retried, this reason is returned.
121    RetryableError {
122        /// The kind of error.
123        kind: ErrorKind,
124        /// A server may tell us to retry only after a specific time has elapsed.
125        retry_after: Option<Duration>,
126    },
127}
128
129impl fmt::Display for RetryReason {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Self::RetryableError { kind, retry_after } => {
133                let after = retry_after
134                    .map(|d| format!(" after {d:?}"))
135                    .unwrap_or_default();
136                write!(f, "{kind} error{after}")
137            }
138        }
139    }
140}
141
142/// The priority of a retry classifier. Classifiers with a higher priority will
143/// run **after** classifiers with a lower priority and may override their
144/// result. Classifiers with equal priorities make no guarantees about which
145/// will run first.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct RetryClassifierPriority {
148    inner: Inner,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152enum Inner {
153    /// The default priority for the `HttpStatusCodeClassifier`.
154    HttpStatusCodeClassifier,
155    /// The default priority for the `ModeledAsRetryableClassifier`.
156    ModeledAsRetryableClassifier,
157    /// The default priority for the `TransientErrorClassifier`.
158    TransientErrorClassifier,
159    /// The priority of some other classifier.
160    Other(i8),
161}
162
163impl PartialOrd for RetryClassifierPriority {
164    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
165        Some(self.as_i8().cmp(&other.as_i8()))
166    }
167}
168
169impl Ord for RetryClassifierPriority {
170    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
171        self.as_i8().cmp(&other.as_i8())
172    }
173}
174
175impl RetryClassifierPriority {
176    /// Create a new `RetryClassifierPriority` with the default priority for the `HttpStatusCodeClassifier`.
177    pub fn http_status_code_classifier() -> Self {
178        Self {
179            inner: Inner::HttpStatusCodeClassifier,
180        }
181    }
182
183    /// Create a new `RetryClassifierPriority` with the default priority for the `ModeledAsRetryableClassifier`.
184    pub fn modeled_as_retryable_classifier() -> Self {
185        Self {
186            inner: Inner::ModeledAsRetryableClassifier,
187        }
188    }
189
190    /// Create a new `RetryClassifierPriority` with the default priority for the `TransientErrorClassifier`.
191    pub fn transient_error_classifier() -> Self {
192        Self {
193            inner: Inner::TransientErrorClassifier,
194        }
195    }
196
197    #[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_before` instead"]
198    /// Create a new `RetryClassifierPriority` with lower priority than the given priority.
199    pub fn with_lower_priority_than(other: Self) -> Self {
200        Self::run_before(other)
201    }
202
203    /// Create a new `RetryClassifierPriority` that can be overridden by the given priority.
204    ///
205    /// Retry classifiers are run in order from lowest to highest priority. A classifier that
206    /// runs later can override a decision from a classifier that runs earlier.
207    pub fn run_before(other: Self) -> Self {
208        Self {
209            inner: Inner::Other(other.as_i8() - 1),
210        }
211    }
212
213    #[deprecated = "use the less-confusingly-named `RetryClassifierPriority::run_after` instead"]
214    /// Create a new `RetryClassifierPriority` with higher priority than the given priority.
215    pub fn with_higher_priority_than(other: Self) -> Self {
216        Self::run_after(other)
217    }
218
219    /// Create a new `RetryClassifierPriority` that can override the given priority.
220    ///
221    /// Retry classifiers are run in order from lowest to highest priority. A classifier that
222    /// runs later can override a decision from a classifier that runs earlier.
223    pub fn run_after(other: Self) -> Self {
224        Self {
225            inner: Inner::Other(other.as_i8() + 1),
226        }
227    }
228
229    fn as_i8(&self) -> i8 {
230        match self.inner {
231            Inner::HttpStatusCodeClassifier => 0,
232            Inner::ModeledAsRetryableClassifier => 10,
233            Inner::TransientErrorClassifier => 20,
234            Inner::Other(i) => i,
235        }
236    }
237}
238
239impl Default for RetryClassifierPriority {
240    fn default() -> Self {
241        Self {
242            inner: Inner::Other(0),
243        }
244    }
245}
246
247/// Classifies what kind of retry is needed for a given [`InterceptorContext`].
248pub trait ClassifyRetry: Send + Sync + fmt::Debug {
249    /// Run this classifier on the [`InterceptorContext`] to determine if the previous request
250    /// should be retried. Returns a [`RetryAction`].
251    fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction;
252
253    /// The name of this retry classifier.
254    ///
255    /// Used for debugging purposes.
256    fn name(&self) -> &'static str;
257
258    /// The priority of this retry classifier.
259    ///
260    /// Classifiers with a higher priority will override the
261    /// results of classifiers with a lower priority. Classifiers with equal priorities make no
262    /// guarantees about which will override the other.
263    ///
264    /// Retry classifiers are run in order of increasing priority. Any decision
265    /// (return value other than `NoActionIndicated`) from a higher priority
266    /// classifier will override the decision of a lower priority classifier with one exception:
267    /// [`RetryAction::RetryForbidden`] is treated differently: If ANY classifier returns `RetryForbidden`,
268    /// this request will not be retried.
269    fn priority(&self) -> RetryClassifierPriority {
270        RetryClassifierPriority::default()
271    }
272}
273
274impl_shared_conversions!(convert SharedRetryClassifier from ClassifyRetry using SharedRetryClassifier::new);
275
276#[derive(Debug, Clone)]
277/// Retry classifier used by the retry strategy to classify responses as retryable or not.
278pub struct SharedRetryClassifier(Arc<dyn ClassifyRetry>);
279
280impl SharedRetryClassifier {
281    /// Given a [`ClassifyRetry`] trait object, create a new `SharedRetryClassifier`.
282    pub fn new(retry_classifier: impl ClassifyRetry + 'static) -> Self {
283        Self(Arc::new(retry_classifier))
284    }
285}
286
287impl ClassifyRetry for SharedRetryClassifier {
288    fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
289        self.0.classify_retry(ctx)
290    }
291
292    fn name(&self) -> &'static str {
293        self.0.name()
294    }
295
296    fn priority(&self) -> RetryClassifierPriority {
297        self.0.priority()
298    }
299}
300
301impl ValidateConfig for SharedRetryClassifier {
302    fn validate_final_config(
303        &self,
304        _runtime_components: &RuntimeComponents,
305        _cfg: &ConfigBag,
306    ) -> Result<(), BoxError> {
307        #[cfg(debug_assertions)]
308        {
309            // Because this is validating that the implementation is correct rather
310            // than validating user input, we only want to run this in debug builds.
311            let retry_classifiers = _runtime_components.retry_classifiers_slice();
312            let out_of_order: Vec<_> = retry_classifiers
313                .windows(2)
314                .filter(|&w| w[0].value().priority() > w[1].value().priority())
315                .collect();
316
317            if !out_of_order.is_empty() {
318                return Err("retry classifiers are mis-ordered; this is a bug".into());
319            }
320        }
321        Ok(())
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::{ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier};
328    use crate::client::interceptors::context::InterceptorContext;
329
330    #[test]
331    fn test_preset_priorities() {
332        let before_modeled_as_retryable = RetryClassifierPriority::run_before(
333            RetryClassifierPriority::modeled_as_retryable_classifier(),
334        );
335        let mut list = vec![
336            RetryClassifierPriority::modeled_as_retryable_classifier(),
337            RetryClassifierPriority::http_status_code_classifier(),
338            RetryClassifierPriority::transient_error_classifier(),
339            before_modeled_as_retryable,
340        ];
341        list.sort();
342
343        assert_eq!(
344            vec![
345                RetryClassifierPriority::http_status_code_classifier(),
346                before_modeled_as_retryable,
347                RetryClassifierPriority::modeled_as_retryable_classifier(),
348                RetryClassifierPriority::transient_error_classifier(),
349            ],
350            list
351        );
352    }
353
354    #[test]
355    fn test_classifier_run_before() {
356        // Ensure low-priority classifiers run *before* high-priority classifiers.
357        let high_priority_classifier = RetryClassifierPriority::default();
358        let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
359        let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
360
361        let mut list = vec![
362            mid_priority_classifier,
363            high_priority_classifier,
364            low_priority_classifier,
365        ];
366        list.sort();
367
368        assert_eq!(
369            vec![
370                low_priority_classifier,
371                mid_priority_classifier,
372                high_priority_classifier
373            ],
374            list
375        );
376    }
377
378    #[test]
379    fn test_classifier_run_after() {
380        // Ensure high-priority classifiers run *after* low-priority classifiers.
381        let low_priority_classifier = RetryClassifierPriority::default();
382        let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
383        let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
384
385        let mut list = vec![
386            mid_priority_classifier,
387            low_priority_classifier,
388            high_priority_classifier,
389        ];
390        list.sort();
391
392        assert_eq!(
393            vec![
394                low_priority_classifier,
395                mid_priority_classifier,
396                high_priority_classifier
397            ],
398            list
399        );
400    }
401
402    #[derive(Debug)]
403    struct ClassifierStub {
404        name: &'static str,
405        priority: RetryClassifierPriority,
406    }
407
408    impl ClassifyRetry for ClassifierStub {
409        fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
410            todo!()
411        }
412
413        fn name(&self) -> &'static str {
414            self.name
415        }
416
417        fn priority(&self) -> RetryClassifierPriority {
418            self.priority
419        }
420    }
421
422    fn wrap(name: &'static str, priority: RetryClassifierPriority) -> SharedRetryClassifier {
423        SharedRetryClassifier::new(ClassifierStub { name, priority })
424    }
425
426    #[test]
427    fn test_shared_classifier_run_before() {
428        // Ensure low-priority classifiers run *before* high-priority classifiers,
429        // even after wrapping.
430        let high_priority_classifier = RetryClassifierPriority::default();
431        let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
432        let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
433
434        let mut list = vec![
435            wrap("mid", mid_priority_classifier),
436            wrap("high", high_priority_classifier),
437            wrap("low", low_priority_classifier),
438        ];
439        list.sort_by_key(|rc| rc.priority());
440
441        let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
442        assert_eq!(vec!["low", "mid", "high"], actual);
443    }
444
445    #[test]
446    fn test_shared_classifier_run_after() {
447        // Ensure high-priority classifiers run *after* low-priority classifiers,
448        // even after wrapping.
449        let low_priority_classifier = RetryClassifierPriority::default();
450        let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
451        let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
452
453        let mut list = vec![
454            wrap("mid", mid_priority_classifier),
455            wrap("high", high_priority_classifier),
456            wrap("low", low_priority_classifier),
457        ];
458        list.sort_by_key(|rc| rc.priority());
459
460        let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
461        assert_eq!(vec!["low", "mid", "high"], actual);
462    }
463
464    #[test]
465    fn test_shared_preset_priorities() {
466        let before_modeled_as_retryable = RetryClassifierPriority::run_before(
467            RetryClassifierPriority::modeled_as_retryable_classifier(),
468        );
469        let mut list = vec![
470            wrap(
471                "modeled as retryable",
472                RetryClassifierPriority::modeled_as_retryable_classifier(),
473            ),
474            wrap(
475                "http status code",
476                RetryClassifierPriority::http_status_code_classifier(),
477            ),
478            wrap(
479                "transient error",
480                RetryClassifierPriority::transient_error_classifier(),
481            ),
482            wrap("before 'modeled as retryable'", before_modeled_as_retryable),
483        ];
484        list.sort_by_key(|rc| rc.priority());
485
486        let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
487        assert_eq!(
488            vec![
489                "http status code",
490                "before 'modeled as retryable'",
491                "modeled as retryable",
492                "transient error"
493            ],
494            actual
495        );
496    }
497}