claude_agent/client/resilience/
backoff.rs1use 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}