Skip to main content

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.cmp(other))
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    ///
252    /// Prefer implementing [`classify_retry_v2`](ClassifyRetry::classify_retry_v2) when a
253    /// classifier needs the [`RetryAction`] accumulated by earlier-running classifiers (e.g. to
254    /// refine an already-retryable verdict). This method has no access to that cumulative verdict;
255    /// it remains the required building block that `classify_retry_v2` delegates to by default.
256    fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction;
257
258    /// Run this classifier with awareness of the [`RetryAction`] accumulated by
259    /// earlier-running (lower-priority) classifiers. `previous` is the running
260    /// result of classification so far ([`RetryAction::NoActionIndicated`] until
261    /// some classifier sets one).
262    ///
263    /// The default implementation ignores `previous` and delegates to
264    /// [`classify_retry`](ClassifyRetry::classify_retry), so existing classifiers
265    /// are unaffected. Override this to refine a verdict produced by an
266    /// earlier-running classifier — for example, to attach a server-directed
267    /// delay to an already-retryable response.
268    fn classify_retry_v2(&self, ctx: &InterceptorContext, previous: &RetryAction) -> RetryAction {
269        let _ = previous;
270        self.classify_retry(ctx)
271    }
272
273    /// The name of this retry classifier.
274    ///
275    /// Used for debugging purposes.
276    fn name(&self) -> &'static str;
277
278    /// The priority of this retry classifier.
279    ///
280    /// Classifiers with a higher priority will override the
281    /// results of classifiers with a lower priority. Classifiers with equal priorities make no
282    /// guarantees about which will override the other.
283    ///
284    /// Retry classifiers are run in order of increasing priority. Any decision
285    /// (return value other than `NoActionIndicated`) from a higher priority
286    /// classifier will override the decision of a lower priority classifier with one exception:
287    /// [`RetryAction::RetryForbidden`] is treated differently: If ANY classifier returns `RetryForbidden`,
288    /// this request will not be retried.
289    fn priority(&self) -> RetryClassifierPriority {
290        RetryClassifierPriority::default()
291    }
292}
293
294impl_shared_conversions!(convert SharedRetryClassifier from ClassifyRetry using SharedRetryClassifier::new);
295
296#[derive(Debug, Clone)]
297/// Retry classifier used by the retry strategy to classify responses as retryable or not.
298pub struct SharedRetryClassifier(Arc<dyn ClassifyRetry>);
299
300impl SharedRetryClassifier {
301    /// Given a [`ClassifyRetry`] trait object, create a new `SharedRetryClassifier`.
302    pub fn new(retry_classifier: impl ClassifyRetry + 'static) -> Self {
303        Self(Arc::new(retry_classifier))
304    }
305}
306
307impl ClassifyRetry for SharedRetryClassifier {
308    fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
309        self.0.classify_retry(ctx)
310    }
311
312    fn classify_retry_v2(&self, ctx: &InterceptorContext, previous: &RetryAction) -> RetryAction {
313        self.0.classify_retry_v2(ctx, previous)
314    }
315
316    fn name(&self) -> &'static str {
317        self.0.name()
318    }
319
320    fn priority(&self) -> RetryClassifierPriority {
321        self.0.priority()
322    }
323}
324
325impl ValidateConfig for SharedRetryClassifier {
326    fn validate_final_config(
327        &self,
328        _runtime_components: &RuntimeComponents,
329        _cfg: &ConfigBag,
330    ) -> Result<(), BoxError> {
331        #[cfg(debug_assertions)]
332        {
333            // Because this is validating that the implementation is correct rather
334            // than validating user input, we only want to run this in debug builds.
335            let retry_classifiers = _runtime_components.retry_classifiers_slice();
336            let out_of_order: Vec<_> = retry_classifiers
337                .windows(2)
338                .filter(|&w| w[0].value().priority() > w[1].value().priority())
339                .collect();
340
341            if !out_of_order.is_empty() {
342                return Err("retry classifiers are mis-ordered; this is a bug".into());
343            }
344        }
345        Ok(())
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::{ClassifyRetry, RetryAction, RetryClassifierPriority, SharedRetryClassifier};
352    use crate::client::interceptors::context::{Input, InterceptorContext};
353
354    #[test]
355    fn test_preset_priorities() {
356        let before_modeled_as_retryable = RetryClassifierPriority::run_before(
357            RetryClassifierPriority::modeled_as_retryable_classifier(),
358        );
359        let mut list = vec![
360            RetryClassifierPriority::modeled_as_retryable_classifier(),
361            RetryClassifierPriority::http_status_code_classifier(),
362            RetryClassifierPriority::transient_error_classifier(),
363            before_modeled_as_retryable,
364        ];
365        list.sort();
366
367        assert_eq!(
368            vec![
369                RetryClassifierPriority::http_status_code_classifier(),
370                before_modeled_as_retryable,
371                RetryClassifierPriority::modeled_as_retryable_classifier(),
372                RetryClassifierPriority::transient_error_classifier(),
373            ],
374            list
375        );
376    }
377
378    #[test]
379    fn test_classifier_run_before() {
380        // Ensure low-priority classifiers run *before* high-priority classifiers.
381        let high_priority_classifier = RetryClassifierPriority::default();
382        let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
383        let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
384
385        let mut list = vec![
386            mid_priority_classifier,
387            high_priority_classifier,
388            low_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    #[test]
403    fn test_classifier_run_after() {
404        // Ensure high-priority classifiers run *after* low-priority classifiers.
405        let low_priority_classifier = RetryClassifierPriority::default();
406        let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
407        let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
408
409        let mut list = vec![
410            mid_priority_classifier,
411            low_priority_classifier,
412            high_priority_classifier,
413        ];
414        list.sort();
415
416        assert_eq!(
417            vec![
418                low_priority_classifier,
419                mid_priority_classifier,
420                high_priority_classifier
421            ],
422            list
423        );
424    }
425
426    #[derive(Debug)]
427    struct ClassifierStub {
428        name: &'static str,
429        priority: RetryClassifierPriority,
430    }
431
432    impl ClassifyRetry for ClassifierStub {
433        fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
434            todo!()
435        }
436
437        fn name(&self) -> &'static str {
438            self.name
439        }
440
441        fn priority(&self) -> RetryClassifierPriority {
442            self.priority
443        }
444    }
445
446    fn wrap(name: &'static str, priority: RetryClassifierPriority) -> SharedRetryClassifier {
447        SharedRetryClassifier::new(ClassifierStub { name, priority })
448    }
449
450    #[test]
451    fn test_shared_classifier_run_before() {
452        // Ensure low-priority classifiers run *before* high-priority classifiers,
453        // even after wrapping.
454        let high_priority_classifier = RetryClassifierPriority::default();
455        let mid_priority_classifier = RetryClassifierPriority::run_before(high_priority_classifier);
456        let low_priority_classifier = RetryClassifierPriority::run_before(mid_priority_classifier);
457
458        let mut list = [
459            wrap("mid", mid_priority_classifier),
460            wrap("high", high_priority_classifier),
461            wrap("low", low_priority_classifier),
462        ];
463        list.sort_by_key(|rc| rc.priority());
464
465        let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
466        assert_eq!(vec!["low", "mid", "high"], actual);
467    }
468
469    #[test]
470    fn test_shared_classifier_run_after() {
471        // Ensure high-priority classifiers run *after* low-priority classifiers,
472        // even after wrapping.
473        let low_priority_classifier = RetryClassifierPriority::default();
474        let mid_priority_classifier = RetryClassifierPriority::run_after(low_priority_classifier);
475        let high_priority_classifier = RetryClassifierPriority::run_after(mid_priority_classifier);
476
477        let mut list = [
478            wrap("mid", mid_priority_classifier),
479            wrap("high", high_priority_classifier),
480            wrap("low", low_priority_classifier),
481        ];
482        list.sort_by_key(|rc| rc.priority());
483
484        let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
485        assert_eq!(vec!["low", "mid", "high"], actual);
486    }
487
488    #[test]
489    fn test_shared_preset_priorities() {
490        let before_modeled_as_retryable = RetryClassifierPriority::run_before(
491            RetryClassifierPriority::modeled_as_retryable_classifier(),
492        );
493        let mut list = [
494            wrap(
495                "modeled as retryable",
496                RetryClassifierPriority::modeled_as_retryable_classifier(),
497            ),
498            wrap(
499                "http status code",
500                RetryClassifierPriority::http_status_code_classifier(),
501            ),
502            wrap(
503                "transient error",
504                RetryClassifierPriority::transient_error_classifier(),
505            ),
506            wrap("before 'modeled as retryable'", before_modeled_as_retryable),
507        ];
508        list.sort_by_key(|rc| rc.priority());
509
510        let actual: Vec<_> = list.iter().map(|it| it.name()).collect();
511        assert_eq!(
512            vec![
513                "http status code",
514                "before 'modeled as retryable'",
515                "modeled as retryable",
516                "transient error"
517            ],
518            actual
519        );
520    }
521
522    fn test_ctx() -> InterceptorContext {
523        InterceptorContext::new(Input::erase("input"))
524    }
525
526    // A classifier that only implements `classify_retry`, returning a fixed
527    // verdict and ignoring the context.
528    #[derive(Debug)]
529    struct FixedClassifier(RetryAction);
530
531    impl ClassifyRetry for FixedClassifier {
532        fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
533            self.0.clone()
534        }
535
536        fn name(&self) -> &'static str {
537            "fixed"
538        }
539    }
540
541    // The default `classify_retry_v2` implementation must ignore `previous` and
542    // delegate to `classify_retry`, so classifiers that don't override it are
543    // unaffected by the newer signature.
544    #[test]
545    fn default_classify_retry_v2_delegates_and_ignores_previous() {
546        let ctx = test_ctx();
547        let classifier = FixedClassifier(RetryAction::transient_error());
548
549        // Regardless of what `previous` is, the result matches `classify_retry`.
550        for previous in [
551            RetryAction::NoActionIndicated,
552            RetryAction::RetryForbidden,
553            RetryAction::throttling_error(),
554        ] {
555            assert_eq!(
556                classifier.classify_retry_v2(&ctx, &previous),
557                classifier.classify_retry(&ctx),
558            );
559        }
560    }
561
562    // A classifier whose `classify_retry_v2` behaves observably differently from
563    // its `classify_retry`: it echoes back the `previous` verdict.
564    #[derive(Debug)]
565    struct PreviousEchoClassifier;
566
567    impl ClassifyRetry for PreviousEchoClassifier {
568        fn classify_retry(&self, _ctx: &InterceptorContext) -> RetryAction {
569            RetryAction::NoActionIndicated
570        }
571
572        fn classify_retry_v2(
573            &self,
574            _ctx: &InterceptorContext,
575            previous: &RetryAction,
576        ) -> RetryAction {
577            previous.clone()
578        }
579
580        fn name(&self) -> &'static str {
581            "previous-echo"
582        }
583    }
584
585    // `SharedRetryClassifier` must forward `classify_retry_v2` to the wrapped
586    // classifier's override rather than falling back to `classify_retry`.
587    #[test]
588    fn shared_classifier_forwards_classify_retry_v2_to_inner() {
589        let ctx = test_ctx();
590        let shared = SharedRetryClassifier::new(PreviousEchoClassifier);
591
592        // If forwarding were (incorrectly) routed to `classify_retry`, this
593        // would be `NoActionIndicated` instead of the echoed `previous`.
594        assert_eq!(
595            shared.classify_retry_v2(&ctx, &RetryAction::throttling_error()),
596            RetryAction::throttling_error(),
597        );
598
599        // The plain `classify_retry` path remains unaffected.
600        assert_eq!(shared.classify_retry(&ctx), RetryAction::NoActionIndicated);
601    }
602}