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