1use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Backoff {
10 None,
12 Fixed(Duration),
14 Exponential {
16 base: Duration,
18 factor: f64,
20 max: Duration,
22 jitter: bool,
24 },
25}
26
27impl Backoff {
28 pub fn exponential() -> Self {
30 Self::Exponential {
31 base: Duration::from_secs(1),
32 factor: 2.0,
33 max: Duration::from_secs(300),
34 jitter: true,
35 }
36 }
37
38 pub fn delay_for(&self, failed_attempt: u32) -> Duration {
42 crate::retry::compute_delay(self, failed_attempt)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct RetryPolicy {
49 pub max_attempts: u32,
51 pub backoff: Backoff,
53}
54
55impl Default for RetryPolicy {
56 fn default() -> Self {
58 Self {
59 max_attempts: 1,
60 backoff: Backoff::None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum RetryDecision {
68 Retry {
70 delay: Duration,
72 },
73 GiveUp,
75}
76
77impl RetryPolicy {
78 pub fn new(max_attempts: u32, backoff: Backoff) -> Self {
80 Self {
81 max_attempts,
82 backoff,
83 }
84 }
85 pub fn none() -> Self {
87 Self::default()
88 }
89 pub fn exponential(max_attempts: u32) -> Self {
91 Self {
92 max_attempts,
93 backoff: Backoff::exponential(),
94 }
95 }
96 pub fn fixed(max_attempts: u32, delay: Duration) -> Self {
98 Self {
99 max_attempts,
100 backoff: Backoff::Fixed(delay),
101 }
102 }
103
104 pub fn decide(&self, failed_attempt: u32) -> RetryDecision {
106 if failed_attempt >= self.max_attempts {
107 RetryDecision::GiveUp
108 } else {
109 RetryDecision::Retry {
110 delay: self.backoff.delay_for(failed_attempt),
111 }
112 }
113 }
114}
115
116pub(crate) fn compute_delay(backoff: &Backoff, failed_attempt: u32) -> Duration {
122 match backoff {
123 Backoff::None => Duration::ZERO,
124 Backoff::Fixed(delay) => *delay,
125 Backoff::Exponential {
126 base,
127 factor,
128 max,
129 jitter,
130 } => {
131 let computed = exponential_delay(*base, *factor, *max, failed_attempt);
132 if *jitter {
133 full_jitter(computed)
134 } else {
135 computed
136 }
137 }
138 }
139}
140
141fn exponential_delay(base: Duration, factor: f64, max: Duration, failed_attempt: u32) -> Duration {
143 if base.is_zero() || max.is_zero() {
144 return Duration::ZERO;
145 }
146 let exponent = f64::from(failed_attempt.max(1) - 1);
148 let factor = if factor.is_finite() && factor > 0.0 {
150 factor
151 } else {
152 1.0
153 };
154
155 let secs = base.as_secs_f64() * factor.powf(exponent);
156 if !secs.is_finite() {
157 return max;
159 }
160 let capped = secs.clamp(0.0, max.as_secs_f64());
161 Duration::try_from_secs_f64(capped).unwrap_or(max).min(max)
162}
163
164fn full_jitter(delay: Duration) -> Duration {
166 use rand::RngExt;
167
168 let secs = delay.as_secs_f64();
169 if !secs.is_finite() || secs <= 0.0 {
170 return Duration::ZERO;
171 }
172 let sampled = rand::rng().random_range(0.0..=secs);
173 Duration::try_from_secs_f64(sampled)
174 .unwrap_or(delay)
175 .min(delay)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 const MS: fn(u64) -> Duration = Duration::from_millis;
183
184 #[test]
185 fn none_is_zero() {
186 assert_eq!(Backoff::None.delay_for(1), Duration::ZERO);
187 assert_eq!(Backoff::None.delay_for(0), Duration::ZERO);
188 assert_eq!(Backoff::None.delay_for(u32::MAX), Duration::ZERO);
189 }
190
191 #[test]
192 fn fixed_is_constant() {
193 let b = Backoff::Fixed(MS(250));
194 for attempt in [0, 1, 2, 7, u32::MAX] {
195 assert_eq!(b.delay_for(attempt), MS(250));
196 }
197 }
198
199 #[test]
200 fn exponential_first_attempt_is_base() {
201 let b = Backoff::Exponential {
202 base: MS(500),
203 factor: 3.0,
204 max: Duration::from_secs(600),
205 jitter: false,
206 };
207 assert_eq!(b.delay_for(1), MS(500));
208 }
209
210 #[test]
211 fn exponential_grows_by_factor() {
212 let b = Backoff::Exponential {
213 base: Duration::from_secs(1),
214 factor: 2.0,
215 max: Duration::from_secs(600),
216 jitter: false,
217 };
218 assert_eq!(b.delay_for(1), Duration::from_secs(1));
219 assert_eq!(b.delay_for(2), Duration::from_secs(2));
220 assert_eq!(b.delay_for(3), Duration::from_secs(4));
221 assert_eq!(b.delay_for(4), Duration::from_secs(8));
222 }
223
224 #[test]
225 fn exponential_respects_cap() {
226 let max = Duration::from_secs(10);
227 let b = Backoff::Exponential {
228 base: Duration::from_secs(1),
229 factor: 2.0,
230 max,
231 jitter: false,
232 };
233 assert_eq!(b.delay_for(5), Duration::from_secs(10));
234 assert_eq!(b.delay_for(50), max);
235 assert_eq!(b.delay_for(u32::MAX), max);
236 }
237
238 #[test]
239 fn exponential_attempt_zero_is_base() {
240 let b = Backoff::Exponential {
241 base: MS(120),
242 factor: 2.0,
243 max: Duration::from_secs(60),
244 jitter: false,
245 };
246 assert_eq!(b.delay_for(0), MS(120));
247 }
248
249 #[test]
250 fn exponential_handles_degenerate_factors() {
251 let max = Duration::from_secs(60);
252 for factor in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.0, -2.0] {
253 let b = Backoff::Exponential {
254 base: MS(100),
255 factor,
256 max,
257 jitter: false,
258 };
259 for attempt in [0, 1, 2, 1000, u32::MAX] {
260 let d = b.delay_for(attempt);
261 assert!(d <= max, "factor {factor} attempt {attempt} gave {d:?}");
262 }
263 assert_eq!(b.delay_for(3), MS(100), "factor {factor}");
265 }
266 }
267
268 #[test]
269 fn exponential_shrinking_factor_never_underflows() {
270 let b = Backoff::Exponential {
271 base: Duration::from_secs(1),
272 factor: 0.5,
273 max: Duration::from_secs(60),
274 jitter: false,
275 };
276 assert_eq!(b.delay_for(2), MS(500));
277 assert_eq!(b.delay_for(u32::MAX), Duration::ZERO);
278 }
279
280 #[test]
281 fn exponential_zero_base_or_max_is_zero() {
282 let a = Backoff::Exponential {
283 base: Duration::ZERO,
284 factor: 2.0,
285 max: Duration::from_secs(60),
286 jitter: true,
287 };
288 let b = Backoff::Exponential {
289 base: Duration::from_secs(1),
290 factor: 2.0,
291 max: Duration::ZERO,
292 jitter: false,
293 };
294 for attempt in [0, 1, 9, u32::MAX] {
295 assert_eq!(a.delay_for(attempt), Duration::ZERO);
296 assert_eq!(b.delay_for(attempt), Duration::ZERO);
297 }
298 }
299
300 #[test]
301 fn jitter_stays_within_bounds() {
302 let max = Duration::from_secs(30);
303 let b = Backoff::Exponential {
304 base: Duration::from_secs(1),
305 factor: 2.0,
306 max,
307 jitter: true,
308 };
309 let mut saw_below_cap = false;
310 for _ in 0..2_000 {
311 let d = b.delay_for(3);
312 assert!(d <= Duration::from_secs(4), "{d:?} exceeded uncapped value");
313 let capped = b.delay_for(99);
314 assert!(capped <= max, "{capped:?} exceeded max");
315 if d < Duration::from_secs(4) {
316 saw_below_cap = true;
317 }
318 }
319 assert!(
320 saw_below_cap,
321 "jitter never produced a value below the computed delay"
322 );
323 }
324
325 #[test]
326 fn jitter_is_not_constant() {
327 let b = Backoff::Exponential {
328 base: Duration::from_secs(10),
329 factor: 2.0,
330 max: Duration::from_secs(600),
331 jitter: true,
332 };
333 let first = b.delay_for(4);
334 let differs = (0..100).any(|_| b.delay_for(4) != first);
335 assert!(differs, "jitter produced the same value 100 times");
336 }
337
338 #[test]
339 fn huge_attempts_do_not_panic_with_jitter() {
340 let b = Backoff::exponential();
341 for attempt in [0, 1, u32::MAX / 2, u32::MAX] {
342 let d = b.delay_for(attempt);
343 assert!(d <= Duration::from_secs(300));
344 }
345 }
346
347 #[test]
348 fn decide_single_attempt_gives_up_immediately() {
349 let p = RetryPolicy::new(1, Backoff::Fixed(Duration::from_secs(1)));
350 assert_eq!(p.decide(1), RetryDecision::GiveUp);
351 }
352
353 #[test]
354 fn decide_boundaries_for_three_attempts() {
355 let p = RetryPolicy::new(3, Backoff::Fixed(Duration::from_secs(2)));
356 assert_eq!(
357 p.decide(1),
358 RetryDecision::Retry {
359 delay: Duration::from_secs(2)
360 }
361 );
362 assert_eq!(
363 p.decide(2),
364 RetryDecision::Retry {
365 delay: Duration::from_secs(2)
366 }
367 );
368 assert_eq!(p.decide(3), RetryDecision::GiveUp);
369 assert_eq!(p.decide(4), RetryDecision::GiveUp);
370 assert_eq!(p.decide(u32::MAX), RetryDecision::GiveUp);
371 }
372
373 #[test]
374 fn default_policy_has_no_retries() {
375 let p = RetryPolicy::default();
376 assert_eq!(p.max_attempts, 1);
377 assert_eq!(p.backoff, Backoff::None);
378 assert_eq!(p.decide(1), RetryDecision::GiveUp);
379 }
380
381 #[test]
382 fn decide_uses_the_failed_attempt_for_the_delay() {
383 let p = RetryPolicy::new(
384 5,
385 Backoff::Exponential {
386 base: Duration::from_secs(1),
387 factor: 2.0,
388 max: Duration::from_secs(600),
389 jitter: false,
390 },
391 );
392 assert_eq!(
393 p.decide(1),
394 RetryDecision::Retry {
395 delay: Duration::from_secs(1)
396 }
397 );
398 assert_eq!(
399 p.decide(3),
400 RetryDecision::Retry {
401 delay: Duration::from_secs(4)
402 }
403 );
404 }
405}