use std::thread; use std::time::Duration; /// Configuration for retrying an operation with exponential backoff. /// /// `max_attempts` includes the first call. For example, `max_attempts = 3` /// means one initial attempt and up to two retries. #[derive(Clone, Debug, PartialEq)] pub struct RetryConfig { pub max_attempts: usize, pub initial_delay: Duration, pub max_delay: Duration, pub multiplier: u32, } impl Default for RetryConfig { fn default() -> Self { Self { max_attempts: 3, initial_delay: Duration::from_millis(100), max_delay: Duration::from_secs(5), multiplier: 2, } } } impl RetryConfig { /// Creates a retry config, normalizing values that would make retrying invalid. pub fn new( max_attempts: usize, initial_delay: Duration, max_delay: Duration, multiplier: u32, ) -> Self { Self { max_attempts: max_attempts.max(1), initial_delay, max_delay, multiplier: multiplier.max(1), } } } /// Runs `operation` until it succeeds or the configured attempts are exhausted. /// /// The last error is returned if every attempt fails. pub fn retry(config: RetryConfig, mut operation: F) -> Result where F: FnMut() -> Result, { let config = RetryConfig::new( config.max_attempts, config.initial_delay, config.max_delay, config.multiplier, ); let mut delay = config.initial_delay; for attempt in 1..=config.max_attempts { match operation() { Ok(value) => return Ok(value), Err(error) if attempt == config.max_attempts => return Err(error), Err(_) => { if !delay.is_zero() { thread::sleep(delay); } delay = delay .checked_mul(config.multiplier) .unwrap_or(config.max_delay) .min(config.max_delay); } } } unreachable!("max_attempts is normalized to at least one") } #[cfg(test)] mod tests { use super::*; #[test] fn succeeds_after_retrying() { let config = RetryConfig::new(3, Duration::ZERO, Duration::ZERO, 2); let mut calls = 0; let result = retry(config, || { calls += 1; if calls < 3 { Err("not yet") } else { Ok("done") } }); assert_eq!(result, Ok("done")); assert_eq!(calls, 3); } #[test] fn returns_last_error_after_exhausting_attempts() { let config = RetryConfig::new(2, Duration::ZERO, Duration::ZERO, 2); let mut calls = 0; let result: Result<(), i32> = retry(config, || { calls += 1; Err(calls) }); assert_eq!(result, Err(2)); assert_eq!(calls, 2); } }