//! Bounded exponential backoff helper used by the transport reconnect loop. //! //! Caller pattern: //! ```text //! let mut backoff = ExponentialBackoff::new(base, cap); //! loop { //! match try_open().await { //! Ok(t) => break t, //! Err(e) => { //! tracing::warn!(error = %e, "open failed"); //! tokio::time::sleep(backoff.next_delay()).await; //! } //! } //! } //! ``` use std::time::Duration; #[derive(Debug, Clone)] pub struct ExponentialBackoff { base: Duration, cap: Duration, attempt: u32, } impl ExponentialBackoff { pub fn new(base: Duration, cap: Duration) -> Self { assert!(base > Duration::ZERO, "backoff base must be positive"); assert!(cap >= base, "backoff cap must be >= base"); Self { base, cap, attempt: 0, } } /// The next delay to sleep for. Doubles each call, capped at `cap`. pub fn next_delay(&mut self) -> Duration { let exp = self.attempt.min(31); let delay = self .base .checked_mul(1u32 << exp) .unwrap_or(self.cap) .min(self.cap); self.attempt = self.attempt.saturating_add(1); delay } pub fn reset(&mut self) { self.attempt = 0; } pub fn attempts(&self) -> u32 { self.attempt } } #[cfg(test)] mod tests { use super::*; #[test] fn doubles_until_cap() { // Arrange let mut b = ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(2)); // Act / Assert assert_eq!(b.next_delay(), Duration::from_millis(100)); assert_eq!(b.next_delay(), Duration::from_millis(200)); assert_eq!(b.next_delay(), Duration::from_millis(400)); assert_eq!(b.next_delay(), Duration::from_millis(800)); assert_eq!(b.next_delay(), Duration::from_millis(1600)); assert_eq!(b.next_delay(), Duration::from_secs(2)); // capped assert_eq!(b.next_delay(), Duration::from_secs(2)); // still capped } #[test] fn reset_returns_to_base() { // Arrange let mut b = ExponentialBackoff::new(Duration::from_millis(50), Duration::from_secs(1)); let _ = b.next_delay(); let _ = b.next_delay(); // Act b.reset(); // Assert assert_eq!(b.next_delay(), Duration::from_millis(50)); } }