use std::thread; use std::time::Duration; /// Settings for retrying an operation with exponential backoff. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct RetryConfig { /// Total attempts, including the first immediate try. pub max_attempts: usize, /// Delay after the first failed attempt. pub initial_delay: Duration, /// Upper bound for any single delay. pub max_delay: Duration, /// Multiplier applied after each failed attempt. pub multiplier: u32, } impl RetryConfig { pub fn new(max_attempts: usize, initial_delay: Duration) -> Self { Self { max_attempts, initial_delay, max_delay: Duration::from_secs(30), multiplier: 2, } } pub fn with_max_delay(mut self, max_delay: Duration) -> Self { self.max_delay = max_delay; self } pub fn with_multiplier(mut self, multiplier: u32) -> Self { self.multiplier = multiplier.max(1); self } } impl Default for RetryConfig { fn default() -> Self { Self::new(3, Duration::from_millis(100)) } } /// Retries `operation` until it succeeds or `max_attempts` is reached. /// /// The closure receives a 1-based attempt number. No delay is applied before /// the first attempt. pub fn retry(config: RetryConfig, operation: F) -> Result where F: FnMut(usize) -> Result, { retry_with_sleep(config, operation, thread::sleep) } /// Same as `retry`, but accepts a custom sleep function for tests or runtimes. pub fn retry_with_sleep( config: RetryConfig, mut operation: F, mut sleep: S, ) -> Result where F: FnMut(usize) -> Result, S: FnMut(Duration), { assert!(config.max_attempts > 0, "max_attempts must be greater than zero"); let mut delay = config.initial_delay.min(config.max_delay); for attempt in 1..=config.max_attempts { match operation(attempt) { Ok(value) => return Ok(value), Err(error) if attempt == config.max_attempts => return Err(error), Err(_) => { sleep(delay); delay = delay .checked_mul(config.multiplier) .unwrap_or(config.max_delay) .min(config.max_delay); } } } unreachable!("loop always returns because max_attempts is greater than zero") } #[cfg(test)] mod tests { use super::*; #[test] fn succeeds_after_retries() { let config = RetryConfig::new(5, Duration::from_millis(1)); let mut seen_delays = Vec::new(); let result = retry_with_sleep( config, |attempt| { if attempt < 3 { Err("not yet") } else { Ok(attempt) } }, |delay| seen_delays.push(delay), ); assert_eq!(result, Ok(3)); assert_eq!( seen_delays, vec![Duration::from_millis(1), Duration::from_millis(2)] ); } #[test] fn stops_after_max_attempts_and_caps_delay() { let config = RetryConfig::new(4, Duration::from_millis(10)) .with_max_delay(Duration::from_millis(25)) .with_multiplier(3); let mut attempts = 0; let mut seen_delays = Vec::new(); let result: Result<(), &'static str> = retry_with_sleep( config, |_| { attempts += 1; Err("failed") }, |delay| seen_delays.push(delay), ); assert_eq!(result, Err("failed")); assert_eq!(attempts, 4); assert_eq!( seen_delays, vec![ Duration::from_millis(10), Duration::from_millis(25), Duration::from_millis(25), ] ); } }