#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureClass {
Success,
Retryable,
Permanent,
Ignored,
}
impl FailureClass {
pub fn classify<T, E>(
outcome: &Result<T, E>,
is_permanent: impl Fn(&E) -> bool,
is_transient: impl Fn(&E) -> bool,
) -> Self {
match outcome {
Ok(_) => Self::Success,
Err(e) if is_permanent(e) => Self::Permanent,
Err(e) if is_transient(e) => Self::Retryable,
Err(_) => Self::Permanent,
}
}
pub fn should_advance(self) -> bool {
matches!(self, Self::Retryable)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryDecision {
Retry,
Stop,
}
pub(crate) type ErrorPredicate<E> = Box<dyn Fn(&E) -> bool + Send + Sync>;
pub(crate) type OutcomeClassifier<T, E> = Box<dyn Fn(&Result<T, E>) -> RetryDecision + Send + Sync>;
pub type RetryAfterExtractor<E> = Box<dyn Fn(&E) -> Option<std::time::Duration> + Send + Sync>;
pub(crate) enum Classifier<T, E> {
RetryAll,
WhenErr(ErrorPredicate<E>),
WhenOutcome(OutcomeClassifier<T, E>),
}
impl<T, E> Classifier<T, E> {
pub(crate) fn decide(&self, outcome: &Result<T, E>) -> RetryDecision {
match self {
Self::RetryAll => match outcome {
Ok(_) => RetryDecision::Stop,
Err(_) => RetryDecision::Retry,
},
Self::WhenErr(pred) => match outcome {
Ok(_) => RetryDecision::Stop,
Err(e) => {
if pred(e) {
RetryDecision::Retry
} else {
RetryDecision::Stop
}
}
},
Self::WhenOutcome(f) => f(outcome),
}
}
}
impl<T, E> std::fmt::Debug for Classifier<T, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::RetryAll => "RetryAll",
Self::WhenErr(_) => "WhenErr",
Self::WhenOutcome(_) => "WhenOutcome",
};
f.debug_tuple("Classifier").field(&name).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retry_all_retries_errors_stops_on_ok() {
let c: Classifier<u32, &str> = Classifier::RetryAll;
assert_eq!(c.decide(&Ok(1)), RetryDecision::Stop);
assert_eq!(c.decide(&Err("x")), RetryDecision::Retry);
}
#[test]
fn when_err_uses_predicate() {
let c: Classifier<u32, i32> = Classifier::WhenErr(Box::new(|e: &i32| *e == 503));
assert_eq!(c.decide(&Err(503)), RetryDecision::Retry);
assert_eq!(c.decide(&Err(400)), RetryDecision::Stop);
}
#[test]
fn when_outcome_can_retry_on_ok() {
let c: Classifier<u32, &str> =
Classifier::WhenOutcome(Box::new(|o: &Result<u32, &str>| match o {
Ok(503) => RetryDecision::Retry,
_ => RetryDecision::Stop,
}));
assert_eq!(c.decide(&Ok(503)), RetryDecision::Retry);
assert_eq!(c.decide(&Ok(200)), RetryDecision::Stop);
}
#[test]
fn classify_permanent_error_does_not_advance() {
let outcome: Result<u32, u16> = Err(400);
let class = FailureClass::classify(
&outcome,
|e: &u16| (400..500).contains(e),
|e: &u16| *e == 429 || *e >= 500,
);
assert_eq!(class, FailureClass::Permanent);
assert!(!class.should_advance());
}
#[test]
fn classify_transient_error_advances() {
let outcome: Result<u32, u16> = Err(429);
let class = FailureClass::classify(
&outcome,
|e: &u16| (400..500).contains(e) && *e != 429,
|e: &u16| *e == 429,
);
assert_eq!(class, FailureClass::Retryable);
assert!(class.should_advance());
}
#[test]
fn classify_unknown_error_fails_fast() {
let outcome: Result<u32, u16> = Err(418);
let class = FailureClass::classify(&outcome, |_| false, |_| false);
assert_eq!(class, FailureClass::Permanent);
assert!(!class.should_advance());
}
}