1use time::Duration;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Backoff {
6 pub base: Duration,
8 pub max: Duration,
10 pub multiplier: u32,
12 pub jitter: f64,
14}
15
16impl Default for Backoff {
17 fn default() -> Self {
18 Self {
19 base: Duration::seconds(30),
20 max: Duration::minutes(30),
21 multiplier: 2,
22 jitter: 0.2,
25 }
26 }
27}
28
29impl Backoff {
30 pub fn delay_for(&self, failures: u32, random: f64) -> Duration {
35 if failures == 0 {
36 return Duration::ZERO;
37 }
38
39 let exponent = failures.saturating_sub(1);
40 let factor = (self.multiplier as u64).saturating_pow(exponent.min(32));
42 let raw = self
43 .base
44 .saturating_mul(factor.min(i32::MAX as u64) as i32)
45 .min(self.max);
46
47 if self.jitter <= 0.0 {
48 return raw;
49 }
50
51 let jitter = self.jitter.clamp(0.0, 1.0);
52 let random = random.clamp(0.0, 1.0);
53 let scale = 1.0 - jitter + 2.0 * jitter * random;
55
56 let seconds = (raw.as_seconds_f64() * scale).max(0.0);
57 Duration::seconds_f64(seconds).min(self.max)
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 const PLAIN: Backoff = Backoff {
67 base: Duration::seconds(30),
68 max: Duration::minutes(30),
69 multiplier: 2,
70 jitter: 0.0,
71 };
72
73 #[test]
74 fn a_healthy_target_waits_not_at_all() {
75 assert_eq!(PLAIN.delay_for(0, 0.5), Duration::ZERO);
76 }
77
78 #[test]
79 fn the_delay_doubles_per_failure() {
80 assert_eq!(PLAIN.delay_for(1, 0.5), Duration::seconds(30));
81 assert_eq!(PLAIN.delay_for(2, 0.5), Duration::minutes(1));
82 assert_eq!(PLAIN.delay_for(3, 0.5), Duration::minutes(2));
83 assert_eq!(PLAIN.delay_for(4, 0.5), Duration::minutes(4));
84 }
85
86 #[test]
87 fn the_cap_holds_however_long_the_outage_lasts() {
88 assert_eq!(PLAIN.delay_for(50, 0.5), Duration::minutes(30));
89 assert_eq!(PLAIN.delay_for(u32::MAX, 0.5), Duration::minutes(30));
90 }
91
92 #[test]
93 fn jitter_spreads_symmetrically_around_the_delay() {
94 let backoff = Backoff {
95 jitter: 0.2,
96 ..PLAIN
97 };
98
99 assert_eq!(backoff.delay_for(1, 0.5), Duration::seconds(30));
100 assert_eq!(backoff.delay_for(1, 0.0), Duration::seconds(24)); assert_eq!(backoff.delay_for(1, 1.0), Duration::seconds(36)); }
103
104 #[test]
105 fn jitter_never_pushes_a_delay_past_the_cap() {
106 let backoff = Backoff {
107 jitter: 0.5,
108 ..PLAIN
109 };
110 assert!(backoff.delay_for(20, 1.0) <= Duration::minutes(30));
111 }
112
113 #[test]
114 fn a_random_value_out_of_range_is_clamped_rather_than_trusted() {
115 let backoff = Backoff {
116 jitter: 0.2,
117 ..PLAIN
118 };
119 assert_eq!(backoff.delay_for(1, 5.0), backoff.delay_for(1, 1.0));
120 assert_eq!(backoff.delay_for(1, -5.0), backoff.delay_for(1, 0.0));
121 }
122}