claude_agent/client/resilience/
backoff.rs

1//! Exponential backoff strategy for retry policies.
2
3use std::time::Duration;
4
5#[derive(Clone)]
6pub struct ExponentialBackoff {
7    initial: Duration,
8    max: Duration,
9    factor: f64,
10    jitter: f64,
11}
12
13impl ExponentialBackoff {
14    pub fn new(initial: Duration, max: Duration, factor: f64) -> Self {
15        Self {
16            initial,
17            max,
18            factor,
19            jitter: 0.1,
20        }
21    }
22
23    pub fn with_jitter(mut self, jitter: f64) -> Self {
24        self.jitter = jitter.clamp(0.0, 1.0);
25        self
26    }
27
28    pub fn delay_for(&self, attempt: u32) -> Duration {
29        let base =
30            self.initial.as_millis() as f64 * self.factor.powi(attempt.saturating_sub(1) as i32);
31        let clamped = base.min(self.max.as_millis() as f64);
32
33        let jittered = if self.jitter > 0.0 {
34            let jitter_range = clamped * self.jitter;
35            let jitter_offset = rand::random::<f64>() * jitter_range * 2.0 - jitter_range;
36            (clamped + jitter_offset).max(0.0)
37        } else {
38            clamped
39        };
40
41        Duration::from_millis(jittered as u64)
42    }
43}
44
45impl Default for ExponentialBackoff {
46    fn default() -> Self {
47        Self {
48            initial: Duration::from_millis(100),
49            max: Duration::from_secs(30),
50            factor: 2.0,
51            jitter: 0.1,
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_exponential_backoff() {
62        let backoff =
63            ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(10), 2.0)
64                .with_jitter(0.0);
65
66        assert_eq!(backoff.delay_for(1), Duration::from_millis(100));
67        assert_eq!(backoff.delay_for(2), Duration::from_millis(200));
68        assert_eq!(backoff.delay_for(3), Duration::from_millis(400));
69        assert_eq!(backoff.delay_for(4), Duration::from_millis(800));
70    }
71
72    #[test]
73    fn test_exponential_backoff_max() {
74        let backoff =
75            ExponentialBackoff::new(Duration::from_millis(100), Duration::from_millis(500), 2.0)
76                .with_jitter(0.0);
77
78        assert_eq!(backoff.delay_for(10), Duration::from_millis(500));
79    }
80}