1use std::thread;
24use std::time::Duration;
25
26#[derive(Debug, Clone, Copy)]
28pub enum BackoffStrategy {
29 None,
31
32 Constant(Duration),
34
35 Linear {
37 initial: Duration,
39 increment: Duration,
41 max: Duration,
43 },
44
45 Exponential {
47 initial: Duration,
49 max: Duration,
51 multiplier: f64,
53 },
54}
55
56impl BackoffStrategy {
57 #[must_use]
59 #[allow(
60 clippy::cast_possible_truncation,
61 clippy::cast_sign_loss,
62 clippy::cast_precision_loss,
63 clippy::cast_possible_wrap
64 )]
65 pub fn delay_for_attempt(&self, attempt: usize) -> Duration {
66 match self {
67 Self::None => Duration::ZERO,
68
69 Self::Constant(d) => *d,
70
71 Self::Linear {
72 initial,
73 increment,
74 max,
75 } => {
76 let delay = *initial + (*increment * attempt as u32);
77 delay.min(*max)
78 }
79
80 Self::Exponential {
81 initial,
82 max,
83 multiplier,
84 } => {
85 let mult = multiplier.powi(attempt as i32);
86 let delay_nanos = initial.as_nanos() as f64 * mult;
87 let delay = Duration::from_nanos(delay_nanos as u64);
88 delay.min(*max)
89 }
90 }
91 }
92}
93
94impl Default for BackoffStrategy {
95 fn default() -> Self {
96 Self::Exponential {
97 initial: Duration::from_millis(100),
98 max: Duration::from_secs(30),
99 multiplier: 2.0,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy)]
106pub struct RetryConfig {
107 pub max_attempts: usize,
109 pub backoff: BackoffStrategy,
111 pub jitter: bool,
113}
114
115impl RetryConfig {
116 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 #[must_use]
124 pub const fn max_attempts(mut self, n: usize) -> Self {
125 if n < 1 {
126 self.max_attempts = 1;
127 } else {
128 self.max_attempts = n;
129 }
130 self
131 }
132
133 #[must_use]
135 pub const fn backoff(mut self, strategy: BackoffStrategy) -> Self {
136 self.backoff = strategy;
137 self
138 }
139
140 #[must_use]
142 pub const fn jitter(mut self, enabled: bool) -> Self {
143 self.jitter = enabled;
144 self
145 }
146
147 #[must_use]
149 pub const fn no_retry() -> Self {
150 Self {
151 max_attempts: 1,
152 backoff: BackoffStrategy::None,
153 jitter: false,
154 }
155 }
156
157 #[must_use]
159 pub const fn with_constant_delay(attempts: usize, delay: Duration) -> Self {
160 Self {
161 max_attempts: attempts,
162 backoff: BackoffStrategy::Constant(delay),
163 jitter: false,
164 }
165 }
166
167 #[must_use]
169 pub const fn with_exponential_backoff(
170 attempts: usize,
171 initial: Duration,
172 max: Duration,
173 ) -> Self {
174 Self {
175 max_attempts: attempts,
176 backoff: BackoffStrategy::Exponential {
177 initial,
178 max,
179 multiplier: 2.0,
180 },
181 jitter: true,
182 }
183 }
184}
185
186impl Default for RetryConfig {
187 fn default() -> Self {
188 Self {
189 max_attempts: 3,
190 backoff: BackoffStrategy::default(),
191 jitter: true,
192 }
193 }
194}
195
196#[derive(Debug)]
198pub struct RetryResult<T, E> {
199 pub result: Result<T, E>,
201 pub attempts: usize,
203 pub total_time: Duration,
205}
206
207impl<T, E> RetryResult<T, E> {
208 #[must_use]
210 pub const fn is_ok(&self) -> bool {
211 self.result.is_ok()
212 }
213
214 #[must_use]
216 pub const fn is_err(&self) -> bool {
217 self.result.is_err()
218 }
219
220 pub fn unwrap(self) -> T
226 where
227 E: std::fmt::Debug,
228 {
229 self.result.unwrap()
230 }
231
232 pub fn into_result(self) -> Result<T, E> {
238 self.result
239 }
240}
241
242#[allow(
258 clippy::cast_possible_truncation,
259 clippy::cast_sign_loss,
260 clippy::cast_precision_loss
261)]
262pub fn retry<T, E, F>(config: RetryConfig, mut operation: F) -> RetryResult<T, E>
263where
264 F: FnMut() -> Result<T, E>,
265{
266 let start = std::time::Instant::now();
267 let mut last_error: Option<E> = None;
268 let max_attempts = config.max_attempts.max(1);
270
271 for attempt in 0..max_attempts {
272 match operation() {
273 Ok(value) => {
274 return RetryResult {
275 result: Ok(value),
276 attempts: attempt + 1,
277 total_time: start.elapsed(),
278 };
279 }
280 Err(e) => {
281 last_error = Some(e);
282
283 if attempt + 1 < max_attempts {
285 let mut delay = config.backoff.delay_for_attempt(attempt);
286
287 if config.jitter && delay > Duration::ZERO {
289 let jitter_factor = simple_random() * 0.25;
290 let jitter =
291 Duration::from_nanos((delay.as_nanos() as f64 * jitter_factor) as u64);
292 delay += jitter;
293 }
294
295 if delay > Duration::ZERO {
296 thread::sleep(delay);
297 }
298 }
299 }
300 }
301 }
302
303 RetryResult {
304 result: Err(last_error.expect("At least one attempt should have been made")),
305 attempts: max_attempts,
306 total_time: start.elapsed(),
307 }
308}
309
310pub fn retry_with_context<T, E, F>(config: RetryConfig, mut operation: F) -> RetryResult<T, E>
317where
318 F: FnMut(usize) -> Result<T, E>,
319{
320 let start = std::time::Instant::now();
321 let mut last_error: Option<E> = None;
322 let max_attempts = config.max_attempts.max(1);
323
324 for attempt in 0..max_attempts {
325 match operation(attempt) {
326 Ok(value) => {
327 return RetryResult {
328 result: Ok(value),
329 attempts: attempt + 1,
330 total_time: start.elapsed(),
331 };
332 }
333 Err(e) => {
334 last_error = Some(e);
335
336 if attempt + 1 < max_attempts {
337 let delay = config.backoff.delay_for_attempt(attempt);
338 if delay > Duration::ZERO {
339 thread::sleep(delay);
340 }
341 }
342 }
343 }
344 }
345
346 RetryResult {
347 result: Err(last_error.expect("At least one attempt should have been made")),
348 attempts: max_attempts,
349 total_time: start.elapsed(),
350 }
351}
352
353fn simple_random() -> f64 {
355 use std::time::SystemTime;
356 let nanos = SystemTime::now()
357 .duration_since(SystemTime::UNIX_EPOCH)
358 .unwrap_or_default()
359 .subsec_nanos();
360 f64::from(nanos % 1000) / 1000.0
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use std::cell::Cell;
367
368 #[test]
369 fn test_retry_succeeds_first_try() {
370 let config = RetryConfig::new().max_attempts(3);
371 let result = retry(config, || Ok::<_, &str>("success"));
372
373 assert!(result.is_ok());
374 assert_eq!(result.attempts, 1);
375 assert_eq!(result.unwrap(), "success");
376 }
377
378 #[test]
379 fn test_retry_succeeds_after_failures() {
380 let attempts = Cell::new(0);
381 let config = RetryConfig::new()
382 .max_attempts(3)
383 .backoff(BackoffStrategy::None);
384
385 let result = retry(config, || {
386 let n = attempts.get();
387 attempts.set(n + 1);
388 if n < 2 { Err("not yet") } else { Ok("success") }
389 });
390
391 assert!(result.is_ok());
392 assert_eq!(result.attempts, 3);
393 }
394
395 #[test]
396 fn test_retry_exhausted() {
397 let config = RetryConfig::new()
398 .max_attempts(3)
399 .backoff(BackoffStrategy::None);
400
401 let result = retry(config, || Err::<(), _>("always fails"));
402
403 assert!(result.is_err());
404 assert_eq!(result.attempts, 3);
405 }
406
407 #[test]
408 fn test_backoff_constant() {
409 let strategy = BackoffStrategy::Constant(Duration::from_millis(100));
410 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(100));
411 assert_eq!(strategy.delay_for_attempt(5), Duration::from_millis(100));
412 }
413
414 #[test]
415 fn test_backoff_exponential() {
416 let strategy = BackoffStrategy::Exponential {
417 initial: Duration::from_millis(100),
418 max: Duration::from_secs(10),
419 multiplier: 2.0,
420 };
421
422 assert_eq!(strategy.delay_for_attempt(0), Duration::from_millis(100));
423 assert_eq!(strategy.delay_for_attempt(1), Duration::from_millis(200));
424 assert_eq!(strategy.delay_for_attempt(2), Duration::from_millis(400));
425 assert_eq!(strategy.delay_for_attempt(3), Duration::from_millis(800));
426 }
427
428 #[test]
429 fn test_backoff_max_cap() {
430 let strategy = BackoffStrategy::Exponential {
431 initial: Duration::from_secs(1),
432 max: Duration::from_secs(5),
433 multiplier: 2.0,
434 };
435
436 assert_eq!(strategy.delay_for_attempt(10), Duration::from_secs(5));
438 }
439
440 #[test]
441 fn test_no_retry_config() {
442 let config = RetryConfig::no_retry();
443 assert_eq!(config.max_attempts, 1);
444 }
445
446 #[test]
447 fn test_zero_max_attempts_does_not_panic() {
448 let config = RetryConfig {
450 max_attempts: 0,
451 backoff: BackoffStrategy::None,
452 jitter: false,
453 };
454 let result = retry(config, || Err::<(), _>("fail"));
455 assert!(result.is_err());
457 assert_eq!(result.attempts, 1);
458 }
459
460 #[test]
461 fn test_zero_max_attempts_with_context() {
462 let config = RetryConfig {
463 max_attempts: 0,
464 backoff: BackoffStrategy::None,
465 jitter: false,
466 };
467 let result = retry_with_context(config, |_| Err::<(), _>("fail"));
468 assert!(result.is_err());
469 assert_eq!(result.attempts, 1);
470 }
471}