use std::time::Duration;
pub(crate) fn exponential_delay(
attempt: u32,
base: Duration,
multiplier: f64,
max: Duration,
) -> Duration {
let base_secs = base.as_secs_f64();
if base_secs <= 0.0 {
return Duration::ZERO;
}
let factor = multiplier.max(1.0).powi(attempt as i32);
let secs = (base_secs * factor).min(max.as_secs_f64());
Duration::from_secs_f64(secs.max(0.0))
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct BackoffParams {
pub base: Duration,
pub max: Duration,
pub multiplier: f64,
pub jitter: f64,
}
impl BackoffParams {
pub(crate) fn delay_for(&self, attempt: u32, seed: &mut u64) -> Duration {
with_jitter(
exponential_delay(attempt, self.base, self.multiplier, self.max),
self.jitter,
seed,
)
}
}
pub(crate) fn with_jitter(delay: Duration, jitter: f64, seed: &mut u64) -> Duration {
let jitter = jitter.clamp(0.0, 1.0);
if jitter == 0.0 {
return delay;
}
let r = next_unit_f64(seed);
let factor = 1.0 + (r * 2.0 - 1.0) * jitter;
Duration::from_secs_f64((delay.as_secs_f64() * factor).max(0.0))
}
fn next_unit_f64(state: &mut u64) -> f64 {
if *state == 0 {
*state = 0x9E3779B97F4A7C15;
}
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
(*state >> 11) as f64 / (1u64 << 53) as f64
}
pub(crate) fn seed_from_time() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x2545_F491_4F6C_DD1D);
nanos ^ 0x9E3779B97F4A7C15
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exponential_delay_doubles_by_default_and_caps() {
let base = Duration::from_secs(1);
let max = Duration::from_secs(10);
assert_eq!(exponential_delay(0, base, 2.0, max), Duration::from_secs(1));
assert_eq!(exponential_delay(1, base, 2.0, max), Duration::from_secs(2));
assert_eq!(exponential_delay(2, base, 2.0, max), Duration::from_secs(4));
assert_eq!(exponential_delay(3, base, 2.0, max), Duration::from_secs(8));
assert_eq!(exponential_delay(4, base, 2.0, max), max);
assert_eq!(exponential_delay(10, base, 2.0, max), max);
}
#[test]
fn exponential_delay_rejects_shrinking_multiplier() {
let base = Duration::from_secs(2);
let max = Duration::from_secs(60);
assert_eq!(exponential_delay(5, base, 0.1, max), base);
}
#[test]
fn a_zero_base_delay_stays_zero_at_every_attempt() {
let max = Duration::from_secs(60);
for attempt in [0, 1, 10, 100, 1000] {
assert_eq!(
exponential_delay(attempt, Duration::ZERO, 2.0, max),
Duration::ZERO,
"attempt {attempt}"
);
}
}
#[test]
fn with_jitter_zero_is_identity() {
let delay = Duration::from_secs(4);
let mut seed = 12345u64;
assert_eq!(with_jitter(delay, 0.0, &mut seed), delay);
}
#[test]
fn with_jitter_stays_within_bounds() {
let delay = Duration::from_secs(10);
let mut seed = seed_from_time();
for _ in 0..1000 {
let jittered = with_jitter(delay, 0.2, &mut seed);
assert!(jittered.as_secs_f64() >= 8.0 - 1e-9);
assert!(jittered.as_secs_f64() <= 12.0 + 1e-9);
}
}
#[test]
fn with_jitter_full_range_never_negative() {
let delay = Duration::from_millis(1);
let mut seed = 1u64;
for _ in 0..1000 {
let jittered = with_jitter(delay, 1.0, &mut seed);
assert!(jittered.as_secs_f64() >= 0.0);
assert!(jittered.as_secs_f64() <= 0.002 + 1e-9);
}
}
#[test]
fn seed_from_time_is_nonzero() {
assert_ne!(seed_from_time(), 0);
}
#[test]
fn jitter_sequence_is_not_constant() {
let delay = Duration::from_secs(10);
let mut seed = 42u64;
let a = with_jitter(delay, 0.3, &mut seed);
let b = with_jitter(delay, 0.3, &mut seed);
assert_ne!(a, b);
}
}