use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use super::ReconnectPolicy;
const BASE_DELAY_MS: u64 = 250;
const MAX_DELAY: Duration = Duration::from_secs(30);
const SHORT_BACKOFF: Duration = Duration::from_secs(5);
#[must_use]
pub(crate) fn compute_backoff(attempt: u32, policy: ReconnectPolicy) -> Duration {
match policy {
ReconnectPolicy::Immediate => Duration::ZERO,
ReconnectPolicy::ShortBackoff => SHORT_BACKOFF,
ReconnectPolicy::ExponentialBackoff => {
let bounded_attempt = attempt.min(20);
let ceil_ms = BASE_DELAY_MS.saturating_mul(1u64 << bounded_attempt);
let ceil = Duration::from_millis(ceil_ms).min(MAX_DELAY);
let ceil_nanos = u64::try_from(ceil.as_nanos()).unwrap_or(u64::MAX);
if ceil_nanos == 0 {
Duration::ZERO
} else {
Duration::from_nanos(next_jitter_u64() % ceil_nanos)
}
}
}
}
fn next_jitter_u64() -> u64 {
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
static COUNTER: AtomicU64 = AtomicU64::new(0);
let start = START.get_or_init(Instant::now);
let nanos = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut x = nanos.wrapping_add(count.wrapping_mul(0x9E37_79B9_7F4A_7C15));
x ^= x >> 30;
x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
x ^= x >> 27;
x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
x
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::time::Duration;
use super::{BASE_DELAY_MS, MAX_DELAY, ReconnectPolicy, SHORT_BACKOFF, compute_backoff};
#[test]
fn immediate_returns_zero() {
assert_eq!(
compute_backoff(0, ReconnectPolicy::Immediate),
Duration::ZERO
);
assert_eq!(
compute_backoff(99, ReconnectPolicy::Immediate),
Duration::ZERO
);
}
#[test]
fn short_backoff_returns_fixed_5s() {
assert_eq!(
compute_backoff(0, ReconnectPolicy::ShortBackoff),
SHORT_BACKOFF
);
assert_eq!(
compute_backoff(7, ReconnectPolicy::ShortBackoff),
SHORT_BACKOFF
);
}
#[test]
fn exponential_zero_attempt_within_base() {
for _ in 0..100 {
let d = compute_backoff(0, ReconnectPolicy::ExponentialBackoff);
assert!(d <= Duration::from_millis(BASE_DELAY_MS), "got {d:?}");
}
}
#[test]
fn exponential_attempt_one_within_500ms() {
for _ in 0..100 {
let d = compute_backoff(1, ReconnectPolicy::ExponentialBackoff);
assert!(d <= Duration::from_millis(BASE_DELAY_MS * 2), "got {d:?}");
}
}
#[test]
fn exponential_caps_at_max_delay() {
for _ in 0..100 {
let d = compute_backoff(100, ReconnectPolicy::ExponentialBackoff);
assert!(d <= MAX_DELAY, "got {d:?}");
}
}
#[test]
fn exponential_handles_u32_overflow_in_attempt() {
for _ in 0..100 {
let d = compute_backoff(u32::MAX, ReconnectPolicy::ExponentialBackoff);
assert!(
d <= MAX_DELAY,
"must not panic and must respect cap; got {d:?}"
);
}
}
#[test]
fn exponential_two_calls_differ_in_expectation() {
let mut seen = HashSet::new();
for _ in 0..1000 {
seen.insert(compute_backoff(10, ReconnectPolicy::ExponentialBackoff));
}
assert!(
seen.len() >= 100,
"1000 calls must produce at least 100 distinct durations; \
got {} distinct values",
seen.len()
);
}
}