Skip to main content

connpool/resilience/
retry.rs

1//! Retry policy: exponential backoff with jitter for `factory.create()`.
2
3use std::error::Error;
4use std::sync::Arc;
5
6use crate::core::errors::PoolError;
7use crate::rng::Rng;
8
9/// A predicate deciding whether a given factory error is retryable.
10type RetryOn = Arc<dyn Fn(&(dyn Error + 'static)) -> bool + Send + Sync>;
11
12/// How to retry `factory.create()`: exponential backoff with jitter.
13///
14/// `jitter` in `[0, 1]`: `0` = deterministic delays, `1` = "full jitter" (uniform in
15/// `[0, delay]`). `retry_on` narrows what is retryable (e.g. don't retry auth failures);
16/// `None` means retry everything.
17#[derive(Clone)]
18pub struct RetryPolicy {
19    /// Maximum attempts (including the first). `1` = no retries.
20    pub max_attempts: u32,
21    /// Base delay in seconds for the first retry.
22    pub base_delay: f64,
23    /// Upper bound on any single delay, in seconds.
24    pub max_delay: f64,
25    /// Exponential growth factor between attempts.
26    pub multiplier: f64,
27    /// Jitter fraction in `[0, 1]`.
28    pub jitter: f64,
29    /// Optional predicate: retry only when it returns `true`. `None` = retry everything.
30    pub retry_on: Option<RetryOn>,
31}
32
33impl std::fmt::Debug for RetryPolicy {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("RetryPolicy")
36            .field("max_attempts", &self.max_attempts)
37            .field("base_delay", &self.base_delay)
38            .field("max_delay", &self.max_delay)
39            .field("multiplier", &self.multiplier)
40            .field("jitter", &self.jitter)
41            .field("retry_on", &self.retry_on.as_ref().map(|_| "<fn>"))
42            .finish()
43    }
44}
45
46impl Default for RetryPolicy {
47    fn default() -> Self {
48        Self {
49            max_attempts: 3,
50            base_delay: 0.1,
51            max_delay: 5.0,
52            multiplier: 2.0,
53            jitter: 0.5,
54            retry_on: None,
55        }
56    }
57}
58
59impl RetryPolicy {
60    /// A policy that never retries (one attempt only).
61    pub fn no_retry() -> Self {
62        Self {
63            max_attempts: 1,
64            ..Self::default()
65        }
66    }
67
68    /// Returns a copy with `retry_on` set to `predicate`.
69    pub fn with_retry_on<F>(mut self, predicate: F) -> Self
70    where
71        F: Fn(&(dyn Error + 'static)) -> bool + Send + Sync + 'static,
72    {
73        self.retry_on = Some(Arc::new(predicate));
74        self
75    }
76
77    /// Checks the policy's invariants.
78    pub fn validate(&self) -> Result<(), PoolError> {
79        if self.max_attempts < 1 {
80            return Err(PoolError::Config("retry.max_attempts must be >= 1".into()));
81        }
82        if self.base_delay < 0.0 || self.max_delay < 0.0 || self.multiplier < 1.0 {
83            return Err(PoolError::Config(
84                "retry delays must be >= 0 and multiplier >= 1".into(),
85            ));
86        }
87        if self.jitter < 0.0 || self.jitter > 1.0 {
88            return Err(PoolError::Config(
89                "retry.jitter must be within [0, 1]".into(),
90            ));
91        }
92        Ok(())
93    }
94
95    /// Reports whether `err` is retryable under this policy.
96    pub fn should_retry(&self, err: &(dyn Error + 'static)) -> bool {
97        match &self.retry_on {
98            None => true,
99            Some(p) => p(err),
100        }
101    }
102
103    /// The delay before the next attempt, given how many attempts have failed so far
104    /// (`failed_attempts` is 1-based: pass `1` after the first failure).
105    pub fn delay_for(&self, failed_attempts: u32, rng: &mut dyn Rng) -> f64 {
106        // base_delay == 0 means no backoff configured; guard separately so a large
107        // `multiplier.powi(..)` overflow to +Inf can't turn `0.0 * Inf` into NaN.
108        let ceiling = if self.base_delay == 0.0 {
109            0.0
110        } else {
111            self.max_delay
112                .min(self.base_delay * self.multiplier.powi(failed_attempts as i32 - 1))
113        };
114        ceiling * (1.0 - self.jitter + self.jitter * rng.next_f64())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::rng::SystemRng;
122
123    #[test]
124    fn defaults_and_no_retry() {
125        let p = RetryPolicy::default();
126        assert_eq!(p.max_attempts, 3);
127        assert_eq!(p.base_delay, 0.1);
128        assert_eq!(p.max_delay, 5.0);
129        assert_eq!(p.multiplier, 2.0);
130        assert_eq!(p.jitter, 0.5);
131        assert!(p.retry_on.is_none());
132        assert_eq!(RetryPolicy::no_retry().max_attempts, 1);
133        assert!(format!("{p:?}").contains("RetryPolicy"));
134        assert!(format!("{:?}", p.clone().with_retry_on(|_| true)).contains("<fn>"));
135    }
136
137    #[test]
138    fn validate_rules() {
139        assert!(RetryPolicy::default().validate().is_ok());
140        let bad = RetryPolicy {
141            max_attempts: 0,
142            ..Default::default()
143        };
144        assert!(matches!(bad.validate(), Err(PoolError::Config(_))));
145        let bad = RetryPolicy {
146            base_delay: -1.0,
147            ..Default::default()
148        };
149        assert!(bad.validate().is_err());
150        let bad = RetryPolicy {
151            multiplier: 0.5,
152            ..Default::default()
153        };
154        assert!(bad.validate().is_err());
155        let bad = RetryPolicy {
156            jitter: 1.5,
157            ..Default::default()
158        };
159        assert!(bad.validate().is_err());
160    }
161
162    #[test]
163    fn should_retry_predicate() {
164        let p = RetryPolicy::default();
165        let err: Box<dyn Error + Send + Sync> = "x".into();
166        assert!(p.should_retry(&*err));
167        let p = RetryPolicy::default().with_retry_on(|e| e.to_string() == "retry");
168        let yes: Box<dyn Error + Send + Sync> = "retry".into();
169        let no: Box<dyn Error + Send + Sync> = "stop".into();
170        assert!(p.should_retry(&*yes));
171        assert!(!p.should_retry(&*no));
172    }
173
174    #[test]
175    fn delay_backoff_and_jitter_bounds() {
176        let mut rng = SystemRng::with_seed(7);
177        let p = RetryPolicy::default();
178        // failed_attempts=1 -> ceiling 0.1; jitter 0.5 -> delay in [0.05, 0.1].
179        for _ in 0..1000 {
180            let d = p.delay_for(1, &mut rng);
181            assert!((0.05..=0.1).contains(&d), "delay {d}");
182        }
183        // failed_attempts=2 -> ceiling 0.2.
184        let d = p.delay_for(2, &mut rng);
185        assert!((0.1..=0.2).contains(&d));
186        // Ceiling is capped at max_delay.
187        let capped = RetryPolicy {
188            base_delay: 10.0,
189            max_delay: 1.0,
190            jitter: 0.0,
191            ..Default::default()
192        };
193        assert_eq!(capped.delay_for(5, &mut rng), 1.0);
194        // jitter=0 -> deterministic == ceiling.
195        let det = RetryPolicy {
196            jitter: 0.0,
197            ..Default::default()
198        };
199        assert_eq!(det.delay_for(1, &mut rng), 0.1);
200    }
201
202    #[test]
203    fn delay_zero_base_never_nan_even_on_power_overflow() {
204        let mut rng = SystemRng::with_seed(7);
205        // base_delay == 0.0 && multiplier > 1 && failed_attempts large enough that
206        // multiplier.powi(..) overflows to +Inf: 0.0 * Inf used to yield NaN.
207        let p = RetryPolicy {
208            base_delay: 0.0,
209            multiplier: 2.0,
210            ..Default::default()
211        };
212        let d = p.delay_for(10_000, &mut rng);
213        assert!(d.is_finite(), "delay {d}");
214        assert_eq!(d, 0.0);
215    }
216}