use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
pub trait Clock {
fn now(&self) -> Duration;
}
#[derive(Clone, Debug)]
pub struct SystemClock {
origin: Instant,
}
impl SystemClock {
#[must_use]
pub fn new() -> Self {
Self {
origin: Instant::now(),
}
}
}
impl Default for SystemClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for SystemClock {
fn now(&self) -> Duration {
self.origin.elapsed()
}
}
#[derive(Debug)]
pub struct ManualClock {
nanos: AtomicU64,
}
impl ManualClock {
#[must_use]
pub fn new() -> Self {
Self {
nanos: AtomicU64::new(0),
}
}
#[must_use]
pub fn at(start: Duration) -> Self {
Self {
nanos: AtomicU64::new(saturating_nanos(start)),
}
}
pub fn advance(&self, delta: Duration) {
let delta_nanos = saturating_nanos(delta);
let _ = self
.nanos
.try_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
Some(current.saturating_add(delta_nanos))
});
}
pub fn set(&self, instant: Duration) {
self.nanos
.store(saturating_nanos(instant), Ordering::SeqCst);
}
}
impl Default for ManualClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for ManualClock {
fn now(&self) -> Duration {
Duration::from_nanos(self.nanos.load(Ordering::SeqCst))
}
}
fn saturating_nanos(duration: Duration) -> u64 {
u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
}
#[derive(Clone, Debug)]
pub struct DeterministicRng {
state: u64,
}
impl DeterministicRng {
#[must_use]
pub fn new(seed: u64) -> Self {
Self { state: seed }
}
pub fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
pub fn next_unit_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Deadline {
at: Duration,
}
impl Deadline {
#[must_use]
pub fn after<C: Clock>(clock: &C, ttl: Duration) -> Self {
Self {
at: clock.now().saturating_add(ttl),
}
}
#[must_use]
pub fn at(at: Duration) -> Self {
Self { at }
}
#[must_use]
pub fn is_expired<C: Clock>(&self, clock: &C) -> bool {
clock.now() >= self.at
}
#[must_use]
pub fn remaining<C: Clock>(&self, clock: &C) -> Duration {
self.at.saturating_sub(clock.now())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BackoffPolicy {
pub base: Duration,
pub factor: f64,
pub max: Duration,
pub jitter: f64,
pub attempts: u32,
}
impl BackoffPolicy {
#[must_use]
pub const fn exponential(base: Duration, max: Duration, attempts: u32) -> Self {
Self {
base,
factor: 2.0,
max,
jitter: 0.0,
attempts,
}
}
#[must_use]
pub const fn with_jitter(mut self, jitter: f64) -> Self {
self.jitter = jitter;
self
}
}
#[must_use]
pub fn backoff_schedule(policy: &BackoffPolicy, seed: u64) -> Vec<Duration> {
let mut rng = DeterministicRng::new(seed);
let base_nanos = policy.base.as_nanos() as f64;
let max_nanos = policy.max.as_nanos() as f64;
let mut schedule = Vec::with_capacity(policy.attempts as usize);
for attempt in 0..policy.attempts {
let exponent = i32::try_from(attempt).unwrap_or(i32::MAX);
let raw = base_nanos * policy.factor.powi(exponent);
let capped = raw.min(max_nanos);
let delay = if policy.jitter > 0.0 {
let unit = rng.next_unit_f64();
let low = 1.0 - policy.jitter;
let span = 2.0 * policy.jitter;
capped * (low + span * unit)
} else {
capped
};
schedule.push(Duration::from_nanos(delay.max(0.0) as u64));
}
schedule
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manual_clock_advances_only_when_told() {
let clock = ManualClock::new();
assert_eq!(clock.now(), Duration::ZERO);
let deadline = Deadline::after(&clock, Duration::from_secs(5));
assert!(!deadline.is_expired(&clock));
clock.advance(Duration::from_secs(5));
assert!(deadline.is_expired(&clock));
assert_eq!(deadline.remaining(&clock), Duration::ZERO);
}
#[test]
fn backoff_schedule_is_reproducible_for_a_seed() {
let policy =
BackoffPolicy::exponential(Duration::from_millis(100), Duration::from_secs(10), 5)
.with_jitter(0.25);
let first = backoff_schedule(&policy, 0xC0FF_EE00);
let second = backoff_schedule(&policy, 0xC0FF_EE00);
let different_seed = backoff_schedule(&policy, 0xDEAD_BEEF);
assert_eq!(first, second);
assert_eq!(first.len(), 5);
assert_ne!(first, different_seed);
}
#[test]
fn jitter_free_backoff_is_pure_exponential_and_capped() {
let policy =
BackoffPolicy::exponential(Duration::from_millis(100), Duration::from_millis(500), 4);
let schedule = backoff_schedule(&policy, 1);
assert_eq!(
schedule,
vec![
Duration::from_millis(100),
Duration::from_millis(200),
Duration::from_millis(400),
Duration::from_millis(500), ]
);
}
#[test]
fn deterministic_rng_is_seed_reproducible() {
let draw = |seed: u64| {
let mut rng = DeterministicRng::new(seed);
[rng.next_u64(), rng.next_u64(), rng.next_u64()]
};
assert_eq!(draw(42), draw(42));
assert_ne!(draw(42), draw(43));
}
#[test]
fn rng_unit_values_stay_in_half_open_unit_interval() {
let mut rng = DeterministicRng::new(7);
for _ in 0..1_000 {
let value = rng.next_unit_f64();
assert!((0.0..1.0).contains(&value));
}
}
#[test]
fn system_clock_is_monotonic() {
let clock = SystemClock::new();
let first = clock.now();
let second = clock.now();
assert!(second >= first);
}
#[test]
fn manual_clock_at_and_set_position_absolute_time() {
let clock = ManualClock::at(Duration::from_secs(100));
assert_eq!(clock.now(), Duration::from_secs(100));
clock.set(Duration::from_secs(5));
assert_eq!(clock.now(), Duration::from_secs(5));
let deadline = Deadline::at(Duration::from_secs(10));
assert!(!deadline.is_expired(&clock));
assert_eq!(deadline.remaining(&clock), Duration::from_secs(5));
}
#[test]
fn manual_clock_advance_saturates_instead_of_wrapping() {
let clock = ManualClock::at(Duration::from_nanos(u64::MAX - 2));
clock.advance(Duration::from_nanos(10));
assert_eq!(clock.now(), Duration::from_nanos(u64::MAX));
let deadline = Deadline::at(Duration::from_nanos(u64::MAX - 1));
assert!(deadline.is_expired(&clock));
assert_eq!(deadline.remaining(&clock), Duration::ZERO);
}
#[test]
fn jittered_backoff_stays_within_symmetric_bounds() {
let policy =
BackoffPolicy::exponential(Duration::from_millis(100), Duration::from_secs(60), 6)
.with_jitter(0.25);
for (attempt, delay) in backoff_schedule(&policy, 99).into_iter().enumerate() {
let exponent = i32::try_from(attempt).unwrap_or(i32::MAX);
let capped = (100.0 * 2.0_f64.powi(exponent)).min(60_000.0);
let low = Duration::from_secs_f64((capped * 0.75) / 1000.0);
let high = Duration::from_secs_f64((capped * 1.25) / 1000.0);
assert!(delay >= low, "attempt {attempt}: {delay:?} < {low:?}");
assert!(delay <= high, "attempt {attempt}: {delay:?} > {high:?}");
}
}
#[test]
fn manual_clock_is_deterministic_and_monotonic_under_identical_advances() {
let run = || {
let clock = ManualClock::new();
let mut samples = Vec::new();
for step in [10_u64, 5, 0, 100] {
clock.advance(Duration::from_millis(step));
samples.push(clock.now());
}
samples
};
assert_eq!(run(), run());
for pair in run().windows(2) {
assert!(pair[1] >= pair[0]);
}
}
}