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