use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use crate::config::BackoffConfig;
#[derive(Debug)]
pub(crate) struct Backoff {
config: BackoffConfig,
state: AtomicU64,
}
impl Backoff {
pub(crate) fn new(config: BackoffConfig) -> Self {
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos() as u64)
.unwrap_or(0x9E37_79B9_7F4A_7C15);
Self { config, state: AtomicU64::new(seed | 1) }
}
pub(crate) fn delay(&self, attempt: u32) -> Duration {
let initial = self.config.initial.as_millis().max(1) as f64;
let max = (self.config.max.as_millis().max(1) as f64).max(initial);
let factor = self.config.factor.max(1.0);
let growth = factor.powi(attempt.saturating_sub(1) as i32);
let base = (initial * growth).min(max);
let jitter = self.config.jitter.clamp(0.0, 1.0);
if jitter == 0.0 {
return Duration::from_millis(base.round() as u64);
}
let lower = base * (1.0 - jitter);
let upper = base * (1.0 + jitter);
let randomized = lower + self.next_unit() * (upper - lower);
Duration::from_millis(randomized.max(0.0).round() as u64)
}
fn next_unit(&self) -> f64 {
let mut z = self.state.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed).wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z >> 11) as f64 / (1u64 << 53) as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grows_geometrically_and_caps() {
let backoff = Backoff::new(BackoffConfig {
initial: Duration::from_millis(10),
max: Duration::from_millis(100),
factor: 2.0,
jitter: 0.0,
});
assert_eq!(backoff.delay(1), Duration::from_millis(10));
assert_eq!(backoff.delay(2), Duration::from_millis(20));
assert_eq!(backoff.delay(3), Duration::from_millis(40));
assert_eq!(backoff.delay(10), Duration::from_millis(100));
}
#[test]
fn jitter_stays_within_bounds() {
let backoff = Backoff::new(BackoffConfig {
initial: Duration::from_millis(100),
max: Duration::from_millis(100),
factor: 2.0,
jitter: 0.5,
});
for _ in 0..1_000 {
let delay = backoff.delay(1).as_millis();
assert!((50..=150).contains(&delay), "delay {delay} out of jittered bounds");
}
}
}