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]
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
154pub struct RetryBuilder {
156 max_attempts: u32,
157 initial: Duration,
158 max: Duration,
159 jitter: f64,
160}
161
162impl Default for RetryBuilder {
163 fn default() -> Self {
164 Self {
165 max_attempts: 3,
166 initial: Duration::from_secs(1),
167 max: Duration::from_secs(5),
168 jitter: 0.25,
169 }
170 }
171}
172
173impl RetryBuilder {
174 pub fn initial_delay(mut self, delay: Duration) -> Self {
176 self.initial = delay;
177 self
178 }
179
180 pub fn max_delay(mut self, delay: Duration) -> Self {
182 self.max = delay;
183 self
184 }
185
186 pub fn jitter(mut self, jitter: f64) -> Self {
193 self.jitter = jitter.clamp(0.0, 1.0);
194 self
195 }
196
197 pub fn build(self) -> Retry {
199 Retry {
200 max_attempts: self.max_attempts,
201 backoff: Backoff::Exponential {
202 initial: self.initial,
203 max: self.max,
204 jitter: self.jitter,
205 },
206 }
207 }
208}
209
210impl From<RetryBuilder> for Retry {
211 fn from(builder: RetryBuilder) -> Self {
212 builder.build()
213 }
214}
215
216static JITTER_COUNTER: AtomicU64 = AtomicU64::new(0);
218
219fn jitter_factor(jitter: f64) -> f64 {
225 if jitter <= 0.0 {
226 return 1.0;
227 }
228 let counter = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
230 let hash = counter.wrapping_mul(0x5851f42d4c957f2d);
231 let random = (hash >> 11) as f64 / ((1u64 << 53) as f64);
233 1.0 + (random - 0.5) * 2.0 * jitter
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_retry_none() {
243 let retry = Retry::none();
244 assert_eq!(retry.max_attempts, 0);
245 assert!(matches!(retry.backoff, Backoff::None));
246 }
247
248 #[test]
249 fn test_retry_default() {
250 let retry = Retry::default();
251 assert_eq!(retry.max_attempts, 3);
252 assert!(
253 matches!(retry.backoff, Backoff::Fixed { delay } if delay == Duration::from_secs(1))
254 );
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::none();
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_default() {
313 let retry = Retry::default();
314 assert_eq!(retry.compute_delay(0), Duration::from_secs(1));
315 assert_eq!(retry.compute_delay(5), Duration::from_secs(1));
316 }
317
318 #[test]
319 fn test_compute_delay_fixed() {
320 let retry = Retry::fixed(3, Duration::from_millis(100));
321 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
322 assert_eq!(retry.compute_delay(1), Duration::from_millis(100));
323 assert_eq!(retry.compute_delay(10), Duration::from_millis(100));
324 }
325
326 #[test]
327 fn test_compute_delay_exponential_no_jitter() {
328 let retry = Retry::exponential(5)
329 .initial_delay(Duration::from_millis(100))
330 .max_delay(Duration::from_secs(10))
331 .jitter(0.0)
332 .build();
333
334 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
335 assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
336 assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
337 assert_eq!(retry.compute_delay(3), Duration::from_millis(800));
338 }
339
340 #[test]
341 fn test_compute_delay_exponential_capped() {
342 let retry = Retry::exponential(10)
343 .initial_delay(Duration::from_millis(100))
344 .max_delay(Duration::from_millis(500))
345 .jitter(0.0)
346 .build();
347
348 assert_eq!(retry.compute_delay(0), Duration::from_millis(100));
349 assert_eq!(retry.compute_delay(1), Duration::from_millis(200));
350 assert_eq!(retry.compute_delay(2), Duration::from_millis(400));
351 assert_eq!(retry.compute_delay(3), Duration::from_millis(500));
353 assert_eq!(retry.compute_delay(10), Duration::from_millis(500));
354 }
355
356 #[test]
357 fn test_compute_delay_exponential_with_jitter() {
358 let retry = Retry::exponential(3)
359 .initial_delay(Duration::from_millis(100))
360 .max_delay(Duration::from_secs(1))
361 .jitter(0.25)
362 .build();
363
364 for _ in 0..10 {
367 let delay = retry.compute_delay(0);
368 let millis = delay.as_millis();
369 assert!((75..=125).contains(&millis), "delay was {}ms", millis);
370 }
371 }
372
373 #[test]
374 fn test_jitter_factor_range() {
375 for _ in 0..100 {
377 let factor = jitter_factor(0.5);
378 assert!((0.5..=1.5).contains(&factor), "factor was {}", factor);
379 }
380 }
381
382 #[test]
383 fn test_jitter_factor_zero() {
384 assert_eq!(jitter_factor(0.0), 1.0);
385 assert_eq!(jitter_factor(-0.1), 1.0);
386 }
387
388 #[test]
389 fn test_from_builder() {
390 let builder = Retry::exponential(2).initial_delay(Duration::from_millis(50));
391 let retry: Retry = builder.into();
392 assert_eq!(retry.max_attempts, 2);
393 }
394}