use core::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPace {
base: Duration,
jitter: Duration,
}
impl RetryPace {
pub const fn fixed(base: Duration) -> Self {
Self {
base,
jitter: Duration::ZERO,
}
}
pub const fn jittered(base: Duration, jitter: Duration) -> Self {
Self { base, jitter }
}
pub const fn base(&self) -> Duration {
self.base
}
pub const fn jitter(&self) -> Duration {
self.jitter
}
pub fn wait(&self) -> Duration {
if self.jitter.is_zero() {
return self.base;
}
let span = self.jitter.as_millis().saturating_add(1);
let mut byte = [0u8; 8];
let offset_ms = match getrandom::fill(&mut byte) {
Ok(()) => (u64::from_le_bytes(byte) as u128 % span) as u64,
Err(_) => 0,
};
self.base + Duration::from_millis(offset_ms)
}
pub fn max_wait(&self) -> Duration {
self.base + self.jitter
}
}
pub const RECOVERY_BAND: (Duration, Duration) =
(Duration::from_millis(250), Duration::from_secs(5));
pub const LOCAL_SOCKET: RetryPace = RetryPace::fixed(Duration::from_millis(500));
pub const RENDEZVOUS: RetryPace =
RetryPace::jittered(Duration::from_secs(3), Duration::from_secs(4));
pub const OS_STATE: RetryPace = RetryPace::fixed(Duration::from_secs(2));
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fixed_pace_does_not_move() {
let pace = RetryPace::fixed(Duration::from_secs(3));
let waits: Vec<Duration> = (0..100).map(|_| pace.wait()).collect();
assert!(
waits.iter().all(|w| *w == Duration::from_secs(3)),
"a fixed pace grew"
);
}
#[test]
fn a_jittered_pace_stays_inside_its_band_and_actually_varies() {
let pace = RetryPace::jittered(Duration::from_secs(3), Duration::from_secs(4));
let waits: Vec<Duration> = (0..200).map(|_| pace.wait()).collect();
assert!(
waits
.iter()
.all(|w| *w >= pace.base() && *w <= pace.max_wait()),
"jitter escaped its band"
);
let distinct: std::collections::BTreeSet<u128> =
waits.iter().map(|w| w.as_millis()).collect();
assert!(
distinct.len() > 10,
"jitter produced only {} distinct waits in 200 draws",
distinct.len()
);
}
#[test]
fn zero_jitter_is_exactly_the_base() {
let pace = RetryPace::jittered(Duration::from_secs(2), Duration::ZERO);
assert_eq!(pace.wait(), Duration::from_secs(2));
}
#[test]
fn the_shipped_paces_sit_in_the_recovery_band() {
let (lo, hi) = RECOVERY_BAND;
for (name, pace) in [
("LOCAL_SOCKET", LOCAL_SOCKET),
("RENDEZVOUS", RENDEZVOUS),
("OS_STATE", OS_STATE),
] {
assert!(
pace.base() >= lo && pace.base() <= hi,
"{name} base {:?} is outside the recovery band {lo:?}..={hi:?}",
pace.base()
);
}
}
#[test]
fn the_rendezvous_pace_keeps_the_jitter_the_bench_needed() {
assert!(RENDEZVOUS.jitter() >= RENDEZVOUS.base());
}
}