Skip to main content

execution_policy/
classify.rs

1//! Failure classification, kept independent from retry/breaker policy.
2
3/// Coarse classification of an outcome (used by the circuit breaker).
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum FailureClass {
6    Success,
7    Retryable,
8    Permanent,
9    Ignored,
10}
11
12/// Whether the engine should retry after an outcome.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RetryDecision {
15    Retry,
16    Stop,
17}
18
19pub(crate) type ErrorPredicate<E> = Box<dyn Fn(&E) -> bool + Send + Sync>;
20pub(crate) type OutcomeClassifier<T, E> = Box<dyn Fn(&Result<T, E>) -> RetryDecision + Send + Sync>;
21
22/// An optional closure that extracts an explicit retry-after delay hint from
23/// an error value. Used by [`crate::retry::Retry::retry_after`].
24pub type RetryAfterExtractor<E> = Box<dyn Fn(&E) -> Option<std::time::Duration> + Send + Sync>;
25
26/// How an outcome maps to a retry decision. `Result<T, E>` *is* the outcome —
27/// there is no separate `Outcome` type.
28pub(crate) enum Classifier<T, E> {
29    /// Default: retry any `Err`, stop on any `Ok`.
30    RetryAll,
31    /// Retry when the error predicate returns true.
32    WhenErr(ErrorPredicate<E>),
33    /// Full control, inspects `Ok` values too.
34    WhenOutcome(OutcomeClassifier<T, E>),
35}
36
37impl<T, E> Classifier<T, E> {
38    pub(crate) fn decide(&self, outcome: &Result<T, E>) -> RetryDecision {
39        match self {
40            Self::RetryAll => match outcome {
41                Ok(_) => RetryDecision::Stop,
42                Err(_) => RetryDecision::Retry,
43            },
44            Self::WhenErr(pred) => match outcome {
45                Ok(_) => RetryDecision::Stop,
46                Err(e) => {
47                    if pred(e) {
48                        RetryDecision::Retry
49                    } else {
50                        RetryDecision::Stop
51                    }
52                }
53            },
54            Self::WhenOutcome(f) => f(outcome),
55        }
56    }
57}
58
59impl<T, E> std::fmt::Debug for Classifier<T, E> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        let name = match self {
62            Self::RetryAll => "RetryAll",
63            Self::WhenErr(_) => "WhenErr",
64            Self::WhenOutcome(_) => "WhenOutcome",
65        };
66        f.debug_tuple("Classifier").field(&name).finish()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn retry_all_retries_errors_stops_on_ok() {
76        let c: Classifier<u32, &str> = Classifier::RetryAll;
77        assert_eq!(c.decide(&Ok(1)), RetryDecision::Stop);
78        assert_eq!(c.decide(&Err("x")), RetryDecision::Retry);
79    }
80
81    #[test]
82    fn when_err_uses_predicate() {
83        let c: Classifier<u32, i32> = Classifier::WhenErr(Box::new(|e: &i32| *e == 503));
84        assert_eq!(c.decide(&Err(503)), RetryDecision::Retry);
85        assert_eq!(c.decide(&Err(400)), RetryDecision::Stop);
86    }
87
88    #[test]
89    fn when_outcome_can_retry_on_ok() {
90        let c: Classifier<u32, &str> =
91            Classifier::WhenOutcome(Box::new(|o: &Result<u32, &str>| match o {
92                Ok(503) => RetryDecision::Retry,
93                _ => RetryDecision::Stop,
94            }));
95        assert_eq!(c.decide(&Ok(503)), RetryDecision::Retry);
96        assert_eq!(c.decide(&Ok(200)), RetryDecision::Stop);
97    }
98}