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}