use std::thread; use std::time::Duration; /// Configuration for retrying an operation with exponential backoff. #[derive(Clone, Copy, Debug)] pub struct Backoff { /// Total number of attempts, including the first call. pub max_attempts: usize, /// Delay after the first failed attempt. pub initial_delay: Duration, /// Maximum delay between attempts. pub max_delay: Duration, } impl Default for Backoff { fn default() -> Self { Self { max_attempts: 3, initial_delay: Duration::from_millis(100), max_delay: Duration::from_secs(5), } } } /// Retries `operation`, doubling the delay after each failure. /// /// Returns immediately on success, or returns the final error after all /// attempts have failed. `max_attempts` must be greater than zero. pub fn retry(config: Backoff, operation: F) -> Result where F: FnMut() -> Result, { retry_with_sleep(config, operation, thread::sleep) } /// Variant of [`retry`] with an injectable sleep function, useful for tests. pub fn retry_with_sleep( config: Backoff, mut operation: F, mut sleep: S, ) -> Result where F: FnMut() -> 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() { Ok(value) => return Ok(value), Err(error) if attempt == config.max_attempts => return Err(error), Err(_) => { sleep(delay); delay = delay .checked_mul(2) .unwrap_or(config.max_delay) .min(config.max_delay); } } } unreachable!("max_attempts is guaranteed to be greater than zero") } #[cfg(test)] mod tests { use super::*; #[test] fn succeeds_after_transient_failures() { let mut attempts = 0; let mut delays = Vec::new(); let result = retry_with_sleep( Backoff { max_attempts: 4, initial_delay: Duration::from_millis(10), max_delay: Duration::from_millis(100), }, || { attempts += 1; if attempts < 3 { Err("temporary failure") } else { Ok(42) } }, |delay| delays.push(delay), ); assert_eq!(result, Ok(42)); assert_eq!(attempts, 3); assert_eq!( delays, vec![Duration::from_millis(10), Duration::from_millis(20)] ); } #[test] fn stops_after_max_attempts_and_caps_delay() { let mut attempts = 0; let mut delays = Vec::new(); let result: Result<(), usize> = retry_with_sleep( Backoff { max_attempts: 4, initial_delay: Duration::from_millis(40), max_delay: Duration::from_millis(50), }, || { attempts += 1; Err(attempts) }, |delay| delays.push(delay), ); assert_eq!(result, Err(4)); assert_eq!(attempts, 4); assert_eq!( delays, vec![ Duration::from_millis(40), Duration::from_millis(50), Duration::from_millis(50), ] ); } }