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/// How an outcome maps to a retry decision. `Result<T, E>` *is* the outcome —
23/// there is no separate `Outcome` type.
24pub(crate) enum Classifier<T, E> {
25    /// Default: retry any `Err`, stop on any `Ok`.
26    RetryAll,
27    /// Retry when the error predicate returns true.
28    WhenErr(ErrorPredicate<E>),
29    /// Full control, inspects `Ok` values too.
30    WhenOutcome(OutcomeClassifier<T, E>),
31}
32
33impl<T, E> Classifier<T, E> {
34    pub(crate) fn decide(&self, outcome: &Result<T, E>) -> RetryDecision {
35        match self {
36            Self::RetryAll => match outcome {
37                Ok(_) => RetryDecision::Stop,
38                Err(_) => RetryDecision::Retry,
39            },
40            Self::WhenErr(pred) => match outcome {
41                Ok(_) => RetryDecision::Stop,
42                Err(e) => {
43                    if pred(e) {
44                        RetryDecision::Retry
45                    } else {
46                        RetryDecision::Stop
47                    }
48                }
49            },
50            Self::WhenOutcome(f) => f(outcome),
51        }
52    }
53}
54
55impl<T, E> std::fmt::Debug for Classifier<T, E> {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        let name = match self {
58            Self::RetryAll => "RetryAll",
59            Self::WhenErr(_) => "WhenErr",
60            Self::WhenOutcome(_) => "WhenOutcome",
61        };
62        f.debug_tuple("Classifier").field(&name).finish()
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn retry_all_retries_errors_stops_on_ok() {
72        let c: Classifier<u32, &str> = Classifier::RetryAll;
73        assert_eq!(c.decide(&Ok(1)), RetryDecision::Stop);
74        assert_eq!(c.decide(&Err("x")), RetryDecision::Retry);
75    }
76
77    #[test]
78    fn when_err_uses_predicate() {
79        let c: Classifier<u32, i32> = Classifier::WhenErr(Box::new(|e: &i32| *e == 503));
80        assert_eq!(c.decide(&Err(503)), RetryDecision::Retry);
81        assert_eq!(c.decide(&Err(400)), RetryDecision::Stop);
82    }
83
84    #[test]
85    fn when_outcome_can_retry_on_ok() {
86        let c: Classifier<u32, &str> =
87            Classifier::WhenOutcome(Box::new(|o: &Result<u32, &str>| match o {
88                Ok(503) => RetryDecision::Retry,
89                _ => RetryDecision::Stop,
90            }));
91        assert_eq!(c.decide(&Ok(503)), RetryDecision::Retry);
92        assert_eq!(c.decide(&Ok(200)), RetryDecision::Stop);
93    }
94}