use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const RECONNECT_ATTEMPTS: u32 = 10;
pub const RECONNECT_BASE_DELAY: Duration = Duration::from_millis(300);
pub const RECONNECT_BACKOFF_MULTIPLIER: u64 = 2;
pub const RECONNECT_MAX_DELAY: Duration = Duration::from_secs(7);
pub(super) fn nominal(attempt: u32) -> Duration {
let base = RECONNECT_BASE_DELAY.as_millis() as u64;
let cap = RECONNECT_MAX_DELAY.as_millis() as u64;
let exp = RECONNECT_BACKOFF_MULTIPLIER.saturating_pow(attempt.saturating_sub(1));
Duration::from_millis(base.saturating_mul(exp).min(cap).max(1))
}
pub(super) fn delay(attempt: u32) -> Duration {
let nominal = nominal(attempt).as_millis() as u64;
let seed =
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.subsec_nanos() as u64).unwrap_or(0);
Duration::from_millis(seed % (nominal + 1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_nominal_grows_geometrically_then_caps() {
assert_eq!(nominal(1), RECONNECT_BASE_DELAY);
assert_eq!(nominal(2), RECONNECT_BASE_DELAY * RECONNECT_BACKOFF_MULTIPLIER as u32);
assert_eq!(
nominal(3),
RECONNECT_BASE_DELAY
* (RECONNECT_BACKOFF_MULTIPLIER * RECONNECT_BACKOFF_MULTIPLIER) as u32
);
let mut prev = Duration::ZERO;
for attempt in 1..=RECONNECT_ATTEMPTS {
let nominal_duration = nominal(attempt);
assert!(nominal_duration >= prev, "backoff must not decrease (attempt {attempt})");
assert!(nominal_duration <= RECONNECT_MAX_DELAY, "backoff must not exceed the cap");
prev = nominal_duration;
}
assert_eq!(nominal(RECONNECT_ATTEMPTS), RECONNECT_MAX_DELAY);
assert_eq!(nominal(u32::MAX), RECONNECT_MAX_DELAY);
}
#[test]
fn backoff_delay_stays_within_nominal_jitter_window() {
for attempt in 1..=RECONNECT_ATTEMPTS {
let nominal_duration = nominal(attempt);
for _ in 0..1000 {
let delay_duration = delay(attempt);
assert!(
delay_duration <= nominal_duration,
"jittered delay {delay_duration:?} exceeded nominal {nominal_duration:?} (attempt {attempt})"
);
}
}
}
}