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 pub fn none() -> Self {
82 Self {
83 max_attempts: 0,
84 backoff: Backoff::None,
85 }
86 }
87
88 pub fn fixed(attempts: u32, delay: Duration) -> Self {
95 Self {
96 max_attempts: attempts,
97 backoff: Backoff::Fixed { delay },
98 }
99 }
100
101 pub fn exponential(attempts: u32) -> RetryBuilder {
121 RetryBuilder {
122 max_attempts: attempts,
123 ..Default::default()
124 }
125 }
126
127 pub fn compute_delay(&self, attempt: u32) -> Duration {
131 match &self.backoff {
132 Backoff::None => Duration::ZERO,
133 Backoff::Fixed { delay } => *delay,
134 Backoff::Exponential {
135 initial,
136 max,
137 jitter,
138 } => {
139 let shift = attempt.min(31);
142 let multiplier = 1u32.checked_shl(shift).unwrap_or(u32::MAX);
143 let base = initial.saturating_mul(multiplier);
144 let capped = base.min(*max);
145
146 let factor = jitter_factor(*jitter);
148 Duration::from_secs_f64(capped.as_secs_f64() * factor)
149 }
150 }
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct RetryBuilder {
157 max_attempts: u32,
158 initial: Duration,
159 max: Duration,
160 jitter: f64,
161}
162
163impl Default for RetryBuilder {
164 fn default() -> Self {
165 Self {
166 max_attempts: 3,
167 initial: Duration::from_secs(1),
168 max: Duration::from_secs(5),
169 jitter: 0.25,
170 }
171 }
172}
173
174impl RetryBuilder {
175 pub fn initial_delay(mut self, delay: Duration) -> Self {
177 self.initial = delay;
178 self
179 }
180
181 pub fn max_delay(mut self, delay: Duration) -> Self {
183 self.max = delay;
184 self
185 }
186
187 pub fn jitter(mut self, jitter: f64) -> Self {
194 self.jitter = jitter.clamp(0.0, 1.0);
195 self
196 }
197
198 pub fn build(self) -> Retry {
200 Retry {
201 max_attempts: self.max_attempts,
202 backoff: Backoff::Exponential {
203 initial: self.initial,
204 max: self.max,
205 jitter: self.jitter,
206 },
207 }
208 }
209}
210
211impl From<RetryBuilder> for Retry {
212 fn from(builder: RetryBuilder) -> Self {
213 builder.build()
214 }
215}
216
217static JITTER_COUNTER: AtomicU64 = AtomicU64::new(0);
219
220fn jitter_factor(jitter: f64) -> f64 {
226 if jitter <= 0.0 {
227 return 1.0;
228 }
229 let counter = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
231 let hash = counter.wrapping_mul(0x5851f42d4c957f2d);
232 let random = (hash >> 11) as f64 / ((1u64 << 53) as f64);
234 1.0 + (random - 0.5) * 2.0 * jitter
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_retry_none() {
244 let retry = Retry::none();
245 assert_eq!(retry.max_attempts, 0);
246 assert!(matches!(retry.backoff, Backoff::None));
247 }
248
249 #[test]
250 fn test_retry_default() {
251 let retry = Retry::default();
252 assert_eq!(retry.max_attempts, 3);
253 assert!(
254 matches!(retry.backoff, Backoff::Fixed { delay } if delay == Duration::from_secs(1))
255 );
256 }
257
258 #[test]
259 fn test_retry_fixed() {
260 let retry = Retry::fixed(5, Duration::from_millis(200));
261 assert_eq!(retry.max_attempts, 5);
262 assert!(
263 matches!(retry.backoff, Backoff::Fixed { delay } if delay == Duration::from_millis(200))
264 );
265 }
266
267 #[test]
268 fn test_retry_exponential_builder() {
269 let retry = Retry::exponential(4)
270 .initial_delay(Duration::from_millis(50))
271 .max_delay(Duration::from_secs(1))
272 .jitter(0.1)
273 .build();
274
275 assert_eq!(retry.max_attempts, 4);
276 match retry.backoff {
277 Backoff::Exponential {
278 initial,
279 max,
280 jitter,
281 } => {
282 assert_eq!(initial, Duration::from_millis(50));
283 assert_eq!(max, Duration::from_secs(1));
284 assert!((jitter - 0.1).abs() < f64::EPSILON);
285 }
286 _ => panic!("expected Exponential"),
287 }
288 }
289
290 #[test]
291 fn test_jitter_clamped() {
292 let retry = Retry::exponential(1).jitter(-0.5).build();
293 match retry.backoff {
294 Backoff::Exponential { jitter, .. } => assert_eq!(jitter, 0.0),
295 _ => panic!("expected Exponential"),
296 }
297
298 let retry = Retry::exponential(1).jitter(2.0).build();
299 match retry.backoff {
300 Backoff::Exponential { jitter, .. } => assert_eq!(jitter, 1.0),
301 _ => panic!("expected Exponential"),
302 }
303 }
304
305 #[test]
306 fn test_compute_delay_none() {
307 let retry = Retry::none();
308 assert_eq!(retry.compute_delay(0), Duration::ZERO);
309 assert_eq!(retry.compute_delay(5), Duration::ZERO);
310 }
311
312 #[test]
313 fn test_compute_delay_default() {
314 let retry = Retry::default();
315 assert_eq!(retry.compute_delay(0), Duration::from_secs(1));
316 assert_eq!(retry.compute_delay(5), Duration::from_secs(1));
317 }
318
319 #[test]
320 fn test_compute_delay_fixed() {
321 let retry = Retry::fixed(3, Duration::from_millis(100));
322 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
323 assert_eq!(retry.compute_delay(1), Duration::from_millis(100));
324 assert_eq!(retry.compute_delay(10), Duration::from_millis(100));
325 }
326
327 #[test]
328 fn test_compute_delay_exponential_no_jitter() {
329 let retry = Retry::exponential(5)
330 .initial_delay(Duration::from_millis(100))
331 .max_delay(Duration::from_secs(10))
332 .jitter(0.0)
333 .build();
334
335 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
336 assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
337 assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
338 assert_eq!(retry.compute_delay(3), Duration::from_millis(800));
339 }
340
341 #[test]
342 fn test_compute_delay_exponential_capped() {
343 let retry = Retry::exponential(10)
344 .initial_delay(Duration::from_millis(100))
345 .max_delay(Duration::from_millis(500))
346 .jitter(0.0)
347 .build();
348
349 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
350 assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
351 assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
352 assert_eq!(retry.compute_delay(3), Duration::from_millis(500));
354 assert_eq!(retry.compute_delay(10), Duration::from_millis(500));
355 }
356
357 #[test]
358 fn test_compute_delay_exponential_with_jitter() {
359 let retry = Retry::exponential(3)
360 .initial_delay(Duration::from_millis(100))
361 .max_delay(Duration::from_secs(1))
362 .jitter(0.25)
363 .build();
364
365 for _ in 0..10 {
368 let delay = retry.compute_delay(0);
369 let millis = delay.as_millis();
370 assert!((75..=125).contains(&millis), "delay was {}ms", millis);
371 }
372 }
373
374 #[test]
375 fn test_jitter_factor_range() {
376 for _ in 0..100 {
378 let factor = jitter_factor(0.5);
379 assert!((0.5..=1.5).contains(&factor), "factor was {}", factor);
380 }
381 }
382
383 #[test]
384 fn test_jitter_factor_zero() {
385 assert_eq!(jitter_factor(0.0), 1.0);
386 assert_eq!(jitter_factor(-0.1), 1.0);
387 }
388
389 #[test]
390 fn test_from_builder() {
391 let builder = Retry::exponential(2).initial_delay(Duration::from_millis(50));
392 let retry: Retry = builder.into();
393 assert_eq!(retry.max_attempts, 2);
394 }
395}