use std::num::NonZeroU32;
use std::time::Duration;
const DEFAULT_FACTOR: u32 = 2;
const DEFAULT_BASE: Duration = Duration::from_millis(100);
const DEFAULT_MAX_RETRIES: u32 = 3;
const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(30);
pub trait Backoff {
fn next_delay(&mut self) -> Option<Duration>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackoffConfigError {
ZeroBase,
ZeroFactor,
MaxDelayBelowBase,
}
impl std::fmt::Display for BackoffConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroBase => write!(f, "backoff `base` must be non-zero"),
Self::ZeroFactor => write!(f, "backoff `factor` must be at least 1"),
Self::MaxDelayBelowBase => write!(f, "backoff `max_delay` must be >= `base`"),
}
}
}
impl std::error::Error for BackoffConfigError {}
#[derive(Debug, Clone)]
pub struct ExponentialBackoffConfig {
pub factor: u32,
pub base: Duration,
pub max_retries: u32,
pub max_delay: Duration,
}
impl Default for ExponentialBackoffConfig {
fn default() -> Self {
Self {
factor: DEFAULT_FACTOR,
base: DEFAULT_BASE,
max_retries: DEFAULT_MAX_RETRIES,
max_delay: DEFAULT_MAX_DELAY,
}
}
}
#[derive(Debug, Clone)]
pub struct ExponentialBackoff {
factor: NonZeroU32,
max_delay: Duration,
next: Duration, retries_left: u32, }
impl ExponentialBackoff {
pub fn new(config: ExponentialBackoffConfig) -> Result<Self, BackoffConfigError> {
let ExponentialBackoffConfig {
factor,
base,
max_retries,
max_delay,
} = config;
if base.is_zero() {
return Err(BackoffConfigError::ZeroBase);
}
let factor = NonZeroU32::new(factor).ok_or(BackoffConfigError::ZeroFactor)?;
if max_delay < base {
return Err(BackoffConfigError::MaxDelayBelowBase);
}
Ok(Self {
factor,
max_delay,
next: base,
retries_left: max_retries,
})
}
}
impl Default for ExponentialBackoff {
fn default() -> Self {
Self::new(ExponentialBackoffConfig::default())
.expect("default exponential-backoff config is valid")
}
}
impl Backoff for ExponentialBackoff {
fn next_delay(&mut self) -> Option<Duration> {
if self.retries_left == 0 {
return None; }
self.retries_left -= 1;
let delay = self.next.min(self.max_delay);
self.next = self
.next
.saturating_mul(self.factor.get())
.min(self.max_delay);
Some(delay)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
fn exp(base: u64, factor: u32, max_delay: u64, max_retries: u32) -> ExponentialBackoff {
ExponentialBackoff::new(ExponentialBackoffConfig {
factor,
base: secs(base),
max_retries,
max_delay: secs(max_delay),
})
.unwrap()
}
#[test]
fn grows_exponentially_then_gives_up() {
let mut b = exp(1, 2, 100, 5);
assert_eq!(b.next_delay(), Some(secs(1)));
assert_eq!(b.next_delay(), Some(secs(2)));
assert_eq!(b.next_delay(), Some(secs(4)));
assert_eq!(b.next_delay(), Some(secs(8)));
assert_eq!(b.next_delay(), Some(secs(16)));
assert_eq!(b.next_delay(), None); }
#[test]
fn delay_is_capped_at_max() {
let mut b = exp(10, 10, 30, 4);
assert_eq!(b.next_delay(), Some(secs(10)));
assert_eq!(b.next_delay(), Some(secs(30))); assert_eq!(b.next_delay(), Some(secs(30))); }
#[test]
fn zero_retries_means_one_attempt() {
let mut b = exp(1, 2, 100, 0);
assert_eq!(b.next_delay(), None); }
#[test]
fn huge_factor_does_not_panic() {
let mut b = ExponentialBackoff::new(ExponentialBackoffConfig {
factor: u32::MAX,
base: Duration::from_secs(u64::MAX / 2),
max_retries: 3,
max_delay: Duration::MAX,
})
.unwrap();
let _ = b.next_delay(); let _ = b.next_delay();
}
#[test]
fn rejects_degenerate_configs() {
assert!(matches!(
ExponentialBackoff::new(ExponentialBackoffConfig {
base: secs(0),
..Default::default()
}),
Err(BackoffConfigError::ZeroBase)
));
assert!(matches!(
ExponentialBackoff::new(ExponentialBackoffConfig {
factor: 0,
..Default::default()
}),
Err(BackoffConfigError::ZeroFactor)
));
assert!(matches!(
ExponentialBackoff::new(ExponentialBackoffConfig {
base: secs(10),
max_delay: secs(5),
..Default::default()
}),
Err(BackoffConfigError::MaxDelayBelowBase)
));
}
}