Skip to main content

tower_resilience_core/
classifier.rs

1//! Failure classification for resilience pattern decisions.
2//!
3//! This module provides the [`FailureClassifier`] trait and implementations
4//! for determining whether a service call result should be considered a failure.
5//!
6//! Used by circuit breaker, outlier detection, and other patterns that need
7//! to classify results as successes or failures.
8
9use std::sync::Arc;
10
11/// Trait for classifying whether a result represents a failure.
12///
13/// Implementors determine whether a given `Result<Res, Err>` should be
14/// counted as a failure for resilience pattern purposes (e.g., circuit
15/// breaker tripping, outlier ejection).
16///
17/// # Type Parameters
18///
19/// - `Res`: The success response type
20/// - `Err`: The error type
21pub trait FailureClassifier<Res, Err>: Send + Sync {
22    /// Determines if the given result should be classified as a failure.
23    ///
24    /// Returns `true` if the result represents a failure.
25    fn classify(&self, result: &Result<Res, Err>) -> bool;
26}
27
28/// Default failure classifier that treats all errors as failures.
29///
30/// This classifier implements `FailureClassifier<Res, Err>` for **all** `Res` and `Err` types,
31/// meaning it can be used without specifying concrete response/error types at configuration time.
32///
33/// # Behavior
34///
35/// - `Ok(_)` => not a failure
36/// - `Err(_)` => failure
37///
38/// # Example
39///
40/// ```rust
41/// use tower_resilience_core::classifier::{FailureClassifier, DefaultClassifier};
42///
43/// let classifier = DefaultClassifier;
44///
45/// // Works with any Result type - type inference from the result argument
46/// assert!(!FailureClassifier::<String, std::io::Error>::classify(&classifier, &Ok("success".to_string())));
47/// assert!(FailureClassifier::<String, std::io::Error>::classify(&classifier, &Err(std::io::Error::other("fail"))));
48/// ```
49#[derive(Debug, Clone, Copy, Default)]
50pub struct DefaultClassifier;
51
52impl<Res, Err> FailureClassifier<Res, Err> for DefaultClassifier {
53    fn classify(&self, result: &Result<Res, Err>) -> bool {
54        result.is_err()
55    }
56}
57
58/// A failure classifier backed by a closure.
59///
60/// This allows custom failure classification logic while maintaining type safety.
61/// The closure captures the concrete `Res` and `Err` types.
62///
63/// # Example
64///
65/// ```rust
66/// use tower_resilience_core::classifier::{FailureClassifier, FnClassifier};
67/// use std::io::{Error, ErrorKind};
68///
69/// // Don't count timeouts as failures
70/// let classifier = FnClassifier::new(|result: &Result<String, Error>| {
71///     match result {
72///         Ok(_) => false,
73///         Err(e) if e.kind() == ErrorKind::TimedOut => false,
74///         Err(_) => true,
75///     }
76/// });
77///
78/// assert!(!classifier.classify(&Ok("success".to_string())));
79/// assert!(!classifier.classify(&Err(Error::new(ErrorKind::TimedOut, "timeout"))));
80/// assert!(classifier.classify(&Err(Error::new(ErrorKind::Other, "other error"))));
81/// ```
82#[derive(Clone)]
83pub struct FnClassifier<F> {
84    f: Arc<F>,
85}
86
87impl<F> FnClassifier<F> {
88    /// Creates a new `FnClassifier` from the given closure.
89    pub fn new(f: F) -> Self {
90        Self { f: Arc::new(f) }
91    }
92}
93
94impl<F, Res, Err> FailureClassifier<Res, Err> for FnClassifier<F>
95where
96    F: Fn(&Result<Res, Err>) -> bool + Send + Sync,
97{
98    fn classify(&self, result: &Result<Res, Err>) -> bool {
99        (self.f)(result)
100    }
101}
102
103impl<F> std::fmt::Debug for FnClassifier<F> {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("FnClassifier")
106            .field("f", &"<closure>")
107            .finish()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn default_classifier_treats_errors_as_failures() {
117        let classifier = DefaultClassifier;
118
119        assert!(!FailureClassifier::<(), ()>::classify(&classifier, &Ok(())));
120        assert!(FailureClassifier::<(), ()>::classify(&classifier, &Err(())));
121    }
122
123    #[test]
124    fn default_classifier_works_with_any_types() {
125        let classifier = DefaultClassifier;
126
127        assert!(!FailureClassifier::<String, std::io::Error>::classify(
128            &classifier,
129            &Ok("ok".to_string())
130        ));
131        assert!(FailureClassifier::<String, std::io::Error>::classify(
132            &classifier,
133            &Err(std::io::Error::other("fail"))
134        ));
135
136        assert!(!FailureClassifier::<i32, &str>::classify(
137            &classifier,
138            &Ok(42)
139        ));
140        assert!(FailureClassifier::<i32, &str>::classify(
141            &classifier,
142            &Err("error")
143        ));
144    }
145
146    #[test]
147    fn fn_classifier_custom_logic() {
148        let classifier = FnClassifier::new(
149            |result: &Result<(), String>| matches!(result, Err(e) if e.contains("fatal")),
150        );
151
152        assert!(!classifier.classify(&Ok(())));
153        assert!(!classifier.classify(&Err("warning".to_string())));
154        assert!(classifier.classify(&Err("fatal error".to_string())));
155    }
156
157    #[test]
158    fn fn_classifier_can_treat_some_successes_as_failures() {
159        let classifier = FnClassifier::new(|result: &Result<u16, ()>| match result {
160            Ok(status) if *status >= 500 => true,
161            Err(_) => true,
162            _ => false,
163        });
164
165        assert!(!classifier.classify(&Ok(200)));
166        assert!(!classifier.classify(&Ok(404)));
167        assert!(classifier.classify(&Ok(500)));
168        assert!(classifier.classify(&Ok(503)));
169        assert!(classifier.classify(&Err(())));
170    }
171}