use core::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackoffConfig {
pub initial_ms: u64,
pub max_ms: u64,
pub multiplier: u64,
pub max_attempts: u32,
}
impl Default for BackoffConfig {
fn default() -> Self {
Self {
initial_ms: 100,
max_ms: 30_000,
multiplier: 2,
max_attempts: u32::MAX,
}
}
}
impl BackoffConfig {
#[must_use]
pub fn delay_for(&self, attempt: u32) -> Duration {
let mut d = self.initial_ms;
for _ in 0..attempt {
d = d.saturating_mul(self.multiplier);
if d >= self.max_ms {
d = self.max_ms;
break;
}
}
Duration::from_millis(d)
}
#[must_use]
pub const fn allow(&self, attempt: u32) -> bool {
attempt < self.max_attempts
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn default_starts_at_100ms() {
let b = BackoffConfig::default();
assert_eq!(b.delay_for(0), Duration::from_millis(100));
}
#[test]
fn doubles_each_attempt() {
let b = BackoffConfig {
initial_ms: 50,
max_ms: 10_000,
multiplier: 2,
max_attempts: 100,
};
assert_eq!(b.delay_for(0), Duration::from_millis(50));
assert_eq!(b.delay_for(1), Duration::from_millis(100));
assert_eq!(b.delay_for(2), Duration::from_millis(200));
assert_eq!(b.delay_for(3), Duration::from_millis(400));
}
#[test]
fn caps_at_max_ms() {
let b = BackoffConfig {
initial_ms: 100,
max_ms: 1_000,
multiplier: 4,
max_attempts: 100,
};
assert_eq!(b.delay_for(2), Duration::from_millis(1_000));
assert_eq!(b.delay_for(20), Duration::from_millis(1_000));
}
#[test]
fn allow_respects_max_attempts() {
let b = BackoffConfig {
initial_ms: 1,
max_ms: 10,
multiplier: 2,
max_attempts: 3,
};
assert!(b.allow(0));
assert!(b.allow(1));
assert!(b.allow(2));
assert!(!b.allow(3));
}
}