1use std::time::Duration;
4
5const JITTER_PERCENT: u64 = 20;
6const MIN_DELAY: Duration = Duration::from_secs(1);
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Backoff {
10 steps: Vec<Duration>,
11 base: Duration,
12 max: Duration,
13}
14
15impl Backoff {
16 pub fn explicit(steps: Vec<Duration>) -> Self {
19 Self {
20 steps,
21 base: MIN_DELAY,
22 max: MIN_DELAY,
23 }
24 }
25
26 pub fn exponential(base: Duration, max: Duration) -> Self {
28 Self {
29 steps: Vec::new(),
30 base,
31 max: max.max(base),
32 }
33 }
34
35 pub fn delay(&self, attempt: u32, jitter_seed: u64) -> Duration {
39 let attempt = attempt.max(1);
40 jittered(self.nominal(attempt), attempt, jitter_seed).max(MIN_DELAY)
41 }
42
43 pub fn longest(&self) -> Duration {
45 self.steps.iter().copied().max().unwrap_or(self.max)
46 }
47
48 fn nominal(&self, attempt: u32) -> Duration {
49 let index = usize::try_from(attempt - 1).unwrap_or(usize::MAX);
50 match self.steps.last() {
51 Some(last) => self.steps.get(index).unwrap_or(last).to_owned(),
52 None => 1u32
53 .checked_shl(attempt - 1)
54 .and_then(|factor| self.base.checked_mul(factor))
55 .map_or(self.max, |delay| delay.min(self.max)),
56 }
57 }
58}
59
60fn jittered(nominal: Duration, attempt: u32, seed: u64) -> Duration {
61 let span = 2 * JITTER_PERCENT + 1;
62 let percent = 100 - JITTER_PERCENT + mix(seed, attempt) % span;
63 let millis = u128::from(percent) * nominal.as_millis() / 100;
64 Duration::from_millis(u64::try_from(millis).unwrap_or(u64::MAX))
65}
66
67fn mix(seed: u64, attempt: u32) -> u64 {
69 let z = seed
70 .wrapping_add(u64::from(attempt).wrapping_mul(0x9E37_79B9_7F4A_7C15))
71 .wrapping_add(0x9E37_79B9_7F4A_7C15);
72 let z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
73 let z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
74 z ^ (z >> 31)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 const fn secs(n: u64) -> Duration {
82 Duration::from_secs(n)
83 }
84
85 fn within_jitter(delay: Duration, nominal: Duration) -> bool {
86 let (low, high) = (nominal.mul_f64(0.8), nominal.mul_f64(1.2));
87 (low.max(MIN_DELAY)..=high.max(MIN_DELAY)).contains(&delay)
88 }
89
90 #[test]
91 fn explicit_steps_clamp_to_the_last() {
92 let backoff = Backoff::explicit(vec![secs(10), secs(30)]);
93 for (attempt, nominal) in [(1, 10), (2, 30), (3, 30), (50, 30), (u32::MAX, 30)] {
94 let delay = backoff.delay(attempt, 7);
95 assert!(within_jitter(delay, secs(nominal)), "{attempt}: {delay:?}");
96 }
97 }
98
99 #[test]
100 fn attempt_zero_is_the_first_retry() {
101 let backoff = Backoff::explicit(vec![secs(10), secs(30)]);
102 assert_eq!(backoff.delay(0, 7), backoff.delay(1, 7));
103 }
104
105 #[test]
106 fn exponential_doubles_and_caps() {
107 let backoff = Backoff::exponential(secs(5), secs(300));
108 for (attempt, nominal) in [
109 (1, 5),
110 (2, 10),
111 (3, 20),
112 (6, 160),
113 (7, 300),
114 (8, 300),
115 (33, 300),
116 (64, 300),
117 (u32::MAX, 300),
118 ] {
119 let delay = backoff.delay(attempt, 99);
120 assert!(within_jitter(delay, secs(nominal)), "{attempt}: {delay:?}");
121 }
122 }
123
124 #[test]
125 fn a_cap_below_the_base_is_the_base() {
126 let backoff = Backoff::exponential(secs(30), secs(10));
127 assert!(within_jitter(backoff.delay(1, 1), secs(30)));
128 assert!(within_jitter(backoff.delay(9, 1), secs(30)));
129 }
130
131 #[test]
132 fn the_longest_wait_is_the_cap_or_the_longest_step() {
133 for (backoff, longest) in [
134 (Backoff::exponential(secs(5), secs(300)), secs(300)),
135 (Backoff::exponential(secs(30), secs(10)), secs(30)),
136 (
137 Backoff::explicit(vec![secs(10), secs(30), secs(20)]),
138 secs(30),
139 ),
140 (Backoff::explicit(vec![]), MIN_DELAY),
141 ] {
142 assert_eq!(backoff.longest(), longest, "{backoff:?}");
143 let jittered = longest.mul_f64(1.2).max(MIN_DELAY);
144 for attempt in (1..=40).chain([u32::MAX]) {
145 assert!(backoff.delay(attempt, 3) <= jittered, "{backoff:?}");
146 }
147 }
148 }
149
150 #[test]
151 fn jitter_is_deterministic_bounded_and_spread() {
152 let backoff = Backoff::exponential(secs(100), secs(100));
153 let delays: Vec<Duration> = (0..500).map(|seed| backoff.delay(1, seed)).collect();
154 assert!(delays.iter().all(|d| within_jitter(*d, secs(100))));
155 assert_eq!(delays[3], backoff.delay(1, 3));
156 let distinct: std::collections::BTreeSet<_> = delays.iter().collect();
157 assert!(distinct.len() > 20, "{}", distinct.len());
158 assert!(delays.iter().any(|d| *d < secs(100)));
159 assert!(delays.iter().any(|d| *d > secs(100)));
160 }
161
162 #[test]
163 fn a_delay_is_never_below_one_second() {
164 for backoff in [
165 Backoff::explicit(vec![]),
166 Backoff::explicit(vec![Duration::ZERO]),
167 Backoff::explicit(vec![Duration::from_millis(900)]),
168 Backoff::exponential(Duration::ZERO, Duration::ZERO),
169 Backoff::exponential(Duration::from_millis(10), Duration::from_millis(20)),
170 ] {
171 for seed in 0..50 {
172 assert!(backoff.delay(1, seed) >= MIN_DELAY, "{backoff:?}");
173 }
174 }
175 }
176
177 #[test]
178 fn huge_durations_do_not_overflow() {
179 let backoff = Backoff::exponential(Duration::MAX, Duration::MAX);
180 assert!(backoff.delay(40, 5) >= MIN_DELAY);
181 let backoff = Backoff::explicit(vec![Duration::MAX]);
182 assert!(backoff.delay(1, 5) >= MIN_DELAY);
183 }
184}