1use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Duration;
8
9#[derive(Clone, Debug)]
42pub struct Retry {
43 pub max_attempts: u32,
45 pub backoff: Backoff,
47}
48
49#[derive(Clone, Copy, Debug, Default)]
51pub enum Backoff {
52 #[default]
54 None,
55
56 Fixed {
58 delay: Duration,
60 },
61
62 Exponential {
67 initial: Duration,
69 max: Duration,
71 jitter: f64,
73 },
74}
75
76impl Default for Retry {
77 fn default() -> Self {
79 Self {
80 max_attempts: 3,
81 backoff: Backoff::Fixed {
82 delay: Duration::from_secs(1),
83 },
84 }
85 }
86}
87
88impl Retry {
89 #[must_use]
91 pub fn none() -> Self {
92 Self {
93 max_attempts: 0,
94 backoff: Backoff::None,
95 }
96 }
97
98 #[must_use]
105 pub fn fixed(attempts: u32, delay: Duration) -> Self {
106 Self {
107 max_attempts: attempts,
108 backoff: Backoff::Fixed { delay },
109 }
110 }
111
112 #[must_use]
132 pub fn exponential(attempts: u32) -> RetryBuilder {
133 RetryBuilder {
134 max_attempts: attempts,
135 ..Default::default()
136 }
137 }
138
139 #[must_use]
143 pub fn compute_delay(&self, attempt: u32) -> Duration {
144 match &self.backoff {
145 Backoff::None => Duration::ZERO,
146 Backoff::Fixed { delay } => *delay,
147 Backoff::Exponential {
148 initial,
149 max,
150 jitter,
151 } => {
152 let shift = attempt.min(31);
155 let multiplier = 1u32.checked_shl(shift).unwrap_or(u32::MAX);
156 let base = initial.saturating_mul(multiplier);
157 let capped = base.min(*max);
158
159 let factor = jitter_factor(*jitter);
161 Duration::from_secs_f64(capped.as_secs_f64() * factor)
162 }
163 }
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct RetryBuilder {
170 max_attempts: u32,
171 initial: Duration,
172 max: Duration,
173 jitter: f64,
174}
175
176impl Default for RetryBuilder {
177 fn default() -> Self {
178 Self {
179 max_attempts: 3,
180 initial: Duration::from_secs(1),
181 max: Duration::from_secs(5),
182 jitter: 0.25,
183 }
184 }
185}
186
187impl RetryBuilder {
188 #[must_use]
190 pub fn initial_delay(mut self, delay: Duration) -> Self {
191 self.initial = delay;
192 self
193 }
194
195 #[must_use]
197 pub fn max_delay(mut self, delay: Duration) -> Self {
198 self.max = delay;
199 self
200 }
201
202 #[must_use]
209 pub fn jitter(mut self, jitter: f64) -> Self {
210 self.jitter = jitter.clamp(0.0, 1.0);
211 self
212 }
213
214 #[must_use]
216 pub fn build(self) -> Retry {
217 Retry {
218 max_attempts: self.max_attempts,
219 backoff: Backoff::Exponential {
220 initial: self.initial,
221 max: self.max,
222 jitter: self.jitter,
223 },
224 }
225 }
226}
227
228impl From<RetryBuilder> for Retry {
229 fn from(builder: RetryBuilder) -> Self {
230 builder.build()
231 }
232}
233
234static JITTER_COUNTER: AtomicU64 = AtomicU64::new(0);
236
237#[allow(
243 clippy::cast_precision_loss,
244 reason = "u64->f64 cast is intentional part of hash-like algorithm"
245)]
246fn jitter_factor(jitter: f64) -> f64 {
247 if jitter <= 0.0 {
248 return 1.0;
249 }
250 let counter = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
252 let hash = counter.wrapping_mul(0x5851_f42d_4c95_7f2d);
253 let random = (hash >> 11) as f64 / ((1u64 << 53) as f64);
255 1.0 + (random - 0.5) * 2.0 * jitter
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_retry_none() {
265 let retry = Retry::none();
266 assert_eq!(retry.max_attempts, 0);
267 assert!(matches!(retry.backoff, Backoff::None));
268 }
269
270 #[test]
271 fn test_retry_default() {
272 let retry = Retry::default();
273 assert_eq!(retry.max_attempts, 3);
274 assert!(
275 matches!(retry.backoff, Backoff::Fixed { delay } if delay == Duration::from_secs(1))
276 );
277 }
278
279 #[test]
280 fn test_retry_fixed() {
281 let retry = Retry::fixed(5, Duration::from_millis(200));
282 assert_eq!(retry.max_attempts, 5);
283 assert!(
284 matches!(retry.backoff, Backoff::Fixed { delay } if delay == Duration::from_millis(200))
285 );
286 }
287
288 #[test]
289 fn test_retry_exponential_builder() {
290 let retry = Retry::exponential(4)
291 .initial_delay(Duration::from_millis(50))
292 .max_delay(Duration::from_secs(1))
293 .jitter(0.1)
294 .build();
295
296 assert_eq!(retry.max_attempts, 4);
297 match retry.backoff {
298 Backoff::Exponential {
299 initial,
300 max,
301 jitter,
302 } => {
303 assert_eq!(initial, Duration::from_millis(50));
304 assert_eq!(max, Duration::from_secs(1));
305 assert!((jitter - 0.1).abs() < f64::EPSILON);
306 }
307 _ => panic!("expected Exponential"),
308 }
309 }
310
311 #[test]
312 fn test_jitter_clamped() {
313 let retry = Retry::exponential(1).jitter(-0.5).build();
314 match retry.backoff {
315 Backoff::Exponential { jitter, .. } => assert_eq!(jitter, 0.0),
316 _ => panic!("expected Exponential"),
317 }
318
319 let retry = Retry::exponential(1).jitter(2.0).build();
320 match retry.backoff {
321 Backoff::Exponential { jitter, .. } => assert_eq!(jitter, 1.0),
322 _ => panic!("expected Exponential"),
323 }
324 }
325
326 #[test]
327 fn test_compute_delay_none() {
328 let retry = Retry::none();
329 assert_eq!(retry.compute_delay(0), Duration::ZERO);
330 assert_eq!(retry.compute_delay(5), Duration::ZERO);
331 }
332
333 #[test]
334 fn test_compute_delay_default() {
335 let retry = Retry::default();
336 assert_eq!(retry.compute_delay(0), Duration::from_secs(1));
337 assert_eq!(retry.compute_delay(5), Duration::from_secs(1));
338 }
339
340 #[test]
341 fn test_compute_delay_fixed() {
342 let retry = Retry::fixed(3, Duration::from_millis(100));
343 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
344 assert_eq!(retry.compute_delay(1), Duration::from_millis(100));
345 assert_eq!(retry.compute_delay(10), Duration::from_millis(100));
346 }
347
348 #[test]
349 fn test_compute_delay_exponential_no_jitter() {
350 let retry = Retry::exponential(5)
351 .initial_delay(Duration::from_millis(100))
352 .max_delay(Duration::from_secs(10))
353 .jitter(0.0)
354 .build();
355
356 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
357 assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
358 assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
359 assert_eq!(retry.compute_delay(3), Duration::from_millis(800));
360 }
361
362 #[test]
363 fn test_compute_delay_exponential_capped() {
364 let retry = Retry::exponential(10)
365 .initial_delay(Duration::from_millis(100))
366 .max_delay(Duration::from_millis(500))
367 .jitter(0.0)
368 .build();
369
370 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
371 assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
372 assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
373 assert_eq!(retry.compute_delay(3), Duration::from_millis(500));
375 assert_eq!(retry.compute_delay(10), Duration::from_millis(500));
376 }
377
378 #[test]
379 fn test_compute_delay_exponential_with_jitter() {
380 let retry = Retry::exponential(3)
381 .initial_delay(Duration::from_millis(100))
382 .max_delay(Duration::from_secs(1))
383 .jitter(0.25)
384 .build();
385
386 for _ in 0..10 {
389 let delay = retry.compute_delay(0);
390 let millis = delay.as_millis();
391 assert!((75..=125).contains(&millis), "delay was {millis}ms");
392 }
393 }
394
395 #[test]
396 fn test_jitter_factor_range() {
397 for _ in 0..100 {
399 let factor = jitter_factor(0.5);
400 assert!((0.5..=1.5).contains(&factor), "factor was {factor}");
401 }
402 }
403
404 #[test]
405 fn test_jitter_factor_zero() {
406 assert_eq!(jitter_factor(0.0), 1.0);
407 assert_eq!(jitter_factor(-0.1), 1.0);
408 }
409
410 #[test]
411 fn test_from_builder() {
412 let builder = Retry::exponential(2).initial_delay(Duration::from_millis(50));
413 let retry: Retry = builder.into();
414 assert_eq!(retry.max_attempts, 2);
415 }
416}