use std::thread; use std::time::Duration; /// Retries `operation` after exponentially increasing delays. /// /// `max_attempts` includes the initial attempt. A value of `0` performs no work /// and returns `None`. Delays saturate at `max_delay`. pub fn retry_with_backoff( max_attempts: usize, initial_delay: Duration, max_delay: Duration, operation: F, ) -> Option> where F: FnMut() -> Result, { retry_with_backoff_using( max_attempts, initial_delay, max_delay, operation, thread::sleep, ) } fn retry_with_backoff_using( max_attempts: usize, initial_delay: Duration, max_delay: Duration, mut operation: F, mut sleep: S, ) -> Option> where F: FnMut() -> Result, S: FnMut(Duration), { if max_attempts == 0 { return None; } let mut delay = initial_delay.min(max_delay); for attempt in 0..max_attempts { match operation() { Ok(value) => return Some(Ok(value)), Err(error) if attempt + 1 == max_attempts => return Some(Err(error)), Err(_) => { sleep(delay); delay = delay.saturating_mul(2).min(max_delay); } } } unreachable!() } #[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_backoff_using( 4, Duration::from_millis(10), Duration::from_millis(25), || { attempts += 1; if attempts < 3 { Err("temporary") } else { Ok(42) } }, |delay| delays.push(delay), ); assert_eq!(result, Some(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: Option> = retry_with_backoff_using( 4, Duration::from_millis(10), Duration::from_millis(15), || { attempts += 1; Err(attempts) }, |delay| delays.push(delay), ); assert_eq!(result, Some(Err(4))); assert_eq!( delays, vec![ Duration::from_millis(10), Duration::from_millis(15), Duration::from_millis(15), ] ); } }