use core::cell::Cell;
use core::fmt;
use core::time::Duration;
const MAX_ATTEMPT_INDEX: u32 = 30;
const FIRST_RETRY: u32 = 0;
const FIRST_TERM: usize = FIRST_RETRY as usize;
const JITTER_SPREAD: f64 = 1.0 / 6.0;
const JITTER_LOW: f64 = 1.0 - JITTER_SPREAD;
const JITTER_TOP: f64 = 1.0;
const JITTER_TERMS: usize = 16;
const NO_GROWTH: f64 = 1.0;
const MAX_SCALING_FACTOR: f64 = 1e15;
const XORSHIFT_LEFT_A: u32 = 13;
const XORSHIFT_RIGHT_B: u32 = 7;
const XORSHIFT_LEFT_C: u32 = 17;
const UNIT_FRACTION_BITS: u32 = 24;
const UNIT_FRACTION_SCALE: u32 = 1 << UNIT_FRACTION_BITS;
const SPLITMIX_MUL_A: u64 = 0xBF58_476D_1CE4_E5B9;
const SPLITMIX_MUL_B: u64 = 0x94D0_49BB_1331_11EB;
const SPLITMIX_SHIFT_A: u32 = 30;
const SPLITMIX_SHIFT_B: u32 = 27;
const SPLITMIX_SHIFT_C: u32 = 31;
const NON_ZERO_STATE_BIT: u64 = 1;
pub struct RetrySchedule {
kind: Kind,
max: Duration,
terms: [f64; JITTER_TERMS],
cursor: Cell<usize>,
}
#[derive(Clone)]
enum Kind {
Exponential { min: Duration, factor: f64 },
Steps(alloc::vec::Vec<Duration>),
}
impl RetrySchedule {
#[must_use]
pub fn new(min: Duration, max: Duration, factor: f64, seed: u64) -> Self {
Self {
kind: Kind::Exponential {
min,
factor: if factor >= NO_GROWTH {
factor
} else {
NO_GROWTH
},
},
max: max.max(min),
terms: jitter_terms(seed),
cursor: Cell::new(FIRST_TERM + 1),
}
}
#[must_use]
pub fn seeded(min: Duration, max: Duration, factor: f64, entropy: u64) -> Self {
Self::new(min, max, factor, mix_seed(entropy))
}
#[must_use]
pub fn from_steps(steps: &[Duration], cap: Duration, seed: u64) -> Self {
Self {
kind: Kind::Steps(steps.to_vec()),
max: cap,
terms: jitter_terms(seed),
cursor: Cell::new(FIRST_TERM + 1),
}
}
#[must_use]
pub fn with_offset(mut self, offset: usize) -> Self {
self.cursor = Cell::new(offset % JITTER_TERMS);
self
}
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
let raw = self.raw_delay_for_attempt(attempt);
if attempt == FIRST_RETRY {
return raw;
}
let cursor = self.cursor.get() % JITTER_TERMS;
self.cursor.set((cursor + 1) % JITTER_TERMS);
let capped = attempt.min(MAX_ATTEMPT_INDEX);
scaled(raw, term_power(self.terms[cursor], capped))
}
#[must_use]
pub fn raw_delay_for_attempt(&self, attempt: u32) -> Duration {
match &self.kind {
Kind::Steps(steps) => steps
.get(usize::try_from(attempt).unwrap_or(usize::MAX))
.copied()
.unwrap_or(self.max),
Kind::Exponential { min, factor } => {
if attempt > MAX_ATTEMPT_INDEX {
return self.max;
}
if min.is_zero() {
return Duration::ZERO;
}
let grown = term_power(*factor, attempt);
if !grown.is_finite() || grown > MAX_SCALING_FACTOR {
return self.max;
}
min.mul_f64(grown).min(self.max)
}
}
}
#[must_use]
pub fn min(&self) -> Duration {
match &self.kind {
Kind::Steps(steps) => steps.first().copied().unwrap_or(self.max),
Kind::Exponential { min, .. } => *min,
}
}
#[must_use]
pub fn max(&self) -> Duration {
self.max
}
}
impl Clone for RetrySchedule {
fn clone(&self) -> Self {
Self {
kind: self.kind.clone(),
max: self.max,
terms: jitter_terms(mix_seed(self.min().as_nanos() as u64)),
cursor: Cell::new(FIRST_TERM + 1),
}
}
}
impl fmt::Debug for RetrySchedule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RetrySchedule")
.field("min", &self.min())
.field("max", &self.max)
.finish_non_exhaustive()
}
}
fn term_power(base: f64, exponent: u32) -> f64 {
let mut power = 1.0;
for _ in 0..exponent {
power *= base;
}
power
}
fn scaled(raw: Duration, factor: f64) -> Duration {
if !factor.is_finite() || factor >= JITTER_TOP {
return raw;
}
raw.mul_f64(factor.max(JITTER_LOW))
.min(raw)
.max(raw.mul_f64(JITTER_LOW))
}
fn jitter_terms(seed: u64) -> [f64; JITTER_TERMS] {
let mut terms = [1.0; JITTER_TERMS];
let mut state = avalanche(seed);
for term in &mut terms[FIRST_TERM + 1..] {
state ^= state << XORSHIFT_LEFT_A;
state ^= state >> XORSHIFT_RIGHT_B;
state ^= state << XORSHIFT_LEFT_C;
let shift = u64::BITS - UNIT_FRACTION_BITS;
let unit = f64::from((state >> shift) as u32) / f64::from(UNIT_FRACTION_SCALE);
*term = JITTER_LOW + JITTER_SPREAD * unit;
}
terms
}
fn avalanche(value: u64) -> u64 {
let mut z = value;
z = (z ^ (z >> SPLITMIX_SHIFT_A)).wrapping_mul(SPLITMIX_MUL_A);
z = (z ^ (z >> SPLITMIX_SHIFT_B)).wrapping_mul(SPLITMIX_MUL_B);
(z ^ (z >> SPLITMIX_SHIFT_C)) | NON_ZERO_STATE_BIT
}
#[must_use]
pub fn mix_seed(entropy: u64) -> u64 {
avalanche(entropy)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::collections::BTreeSet;
use alloc::vec::Vec;
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
fn ms(n: u64) -> Duration {
Duration::from_millis(n)
}
fn sequence(terms: usize) -> Vec<Duration> {
let s = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED);
let mut out = Vec::with_capacity(terms);
for attempt in 0..terms as u32 {
out.push(s.delay_for_attempt(attempt));
}
out
}
#[test]
fn the_raw_series_doubles_from_the_floor_and_saturates_at_the_cap() {
let s = RetrySchedule::new(ms(500), secs(30), 2.0, 1);
assert_eq!(s.raw_delay_for_attempt(0), ms(500));
assert_eq!(s.raw_delay_for_attempt(1), ms(1_000));
assert_eq!(s.raw_delay_for_attempt(2), ms(2_000));
assert_eq!(s.raw_delay_for_attempt(3), ms(4_000));
assert_eq!(s.raw_delay_for_attempt(6), secs(30));
assert_eq!(s.raw_delay_for_attempt(7), secs(30));
}
#[test]
fn an_attempt_past_the_walk_bound_is_the_cap_without_iterating() {
let s = RetrySchedule::new(secs(1), secs(30), 2.0, 1);
assert_eq!(s.raw_delay_for_attempt(u32::MAX), secs(30));
assert_eq!(s.raw_delay_for_attempt(MAX_ATTEMPT_INDEX + 1), secs(30));
assert!(s.delay_for_attempt(u32::MAX) <= secs(30));
}
#[test]
fn the_first_retry_is_exactly_the_configured_step() {
for (min, cap, factor) in [
(ms(500), secs(30), 2.0),
(ms(2_000), ms(10_000), 2.0),
(secs(1), secs(1), 1.0),
] {
let s = RetrySchedule::new(min, cap, factor, 0xDEAD_BEEF);
assert_eq!(s.delay_for_attempt(0), min);
}
}
#[test]
fn every_later_delay_stays_in_the_configured_band() {
let s = RetrySchedule::new(ms(500), secs(30), 2.0, 0xDEAD_BEEF);
for attempt in 0..64 {
let raw = s.raw_delay_for_attempt(attempt);
let d = s.delay_for_attempt(attempt);
let band = raw;
assert!(
d >= band.mul_f64(JITTER_LOW) && d <= band,
"attempt {attempt}: {d:?} of {band:?}"
);
}
}
#[test]
fn the_delay_stays_jittered_at_the_cap() {
let s = RetrySchedule::new(ms(500), secs(30), 2.0, 7).with_offset(1);
let mut distinct = BTreeSet::new();
for _ in 0..64 {
let d = s.delay_for_attempt(20);
assert!(d >= secs(25) && d <= secs(30), "{d:?}");
distinct.insert(d.as_nanos());
}
assert!(distinct.len() > 1, "a capped delay must not be constant");
}
#[test]
fn the_same_seed_reproduces_the_same_sequence() {
let a = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED);
let b = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED);
let seq = |s: &RetrySchedule| -> Vec<Duration> {
(0..8).map(|i| s.delay_for_attempt(i)).collect()
};
assert_eq!(seq(&a), seq(&b));
}
#[test]
fn a_different_seed_draws_differently() {
let a = RetrySchedule::new(secs(10), secs(30), 2.0, 1);
let b = RetrySchedule::new(secs(10), secs(30), 2.0, 2);
let seq = |s: &RetrySchedule| -> Vec<Duration> {
(0..8).map(|i| s.delay_for_attempt(i)).collect()
};
assert_ne!(seq(&a), seq(&b));
}
#[test]
fn a_clone_reseeds_rather_than_replaying() {
let a = RetrySchedule::new(ms(500), secs(30), 2.0, 3);
let b = a.clone();
let seq = |s: &RetrySchedule| -> Vec<Duration> {
(0..8).map(|i| s.delay_for_attempt(i)).collect()
};
assert_ne!(seq(&a), seq(&b));
}
#[test]
fn an_offset_starts_the_sequence_elsewhere_in_the_same_band() {
let raw = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED).raw_delay_for_attempt(1);
let mut distinct = BTreeSet::new();
for offset in 0..JITTER_TERMS {
let d = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED)
.with_offset(offset)
.delay_for_attempt(1);
assert!(d >= raw.mul_f64(JITTER_LOW) && d <= raw, "{offset}: {d:?}");
distinct.insert(d.as_nanos());
}
assert!(distinct.len() > 1, "each offset must reach its own term");
let wrapped = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED)
.with_offset(JITTER_TERMS)
.delay_for_attempt(1);
let first = RetrySchedule::new(ms(500), secs(30), 2.0, 0x5EED)
.with_offset(0)
.delay_for_attempt(1);
assert_eq!(wrapped, first);
}
#[test]
fn a_literal_step_table_is_taken_as_given_then_the_cap() {
let s = RetrySchedule::from_steps(&[ms(2_000), ms(4_000)], ms(10_000), 0x5EED);
assert_eq!(s.raw_delay_for_attempt(0), ms(2_000));
assert_eq!(s.raw_delay_for_attempt(1), ms(4_000));
assert_eq!(s.raw_delay_for_attempt(2), ms(10_000));
assert_eq!(s.raw_delay_for_attempt(u32::MAX), ms(10_000));
assert_eq!(s.delay_for_attempt(0), ms(2_000), "the first step exactly");
assert_eq!(RetrySchedule::from_steps(&[], ms(7), 1).min(), ms(7));
}
#[test]
fn mixed_seeds_are_pure_and_adjacent_entropy_does_not_share_a_sequence() {
assert_eq!(mix_seed(7), mix_seed(7), "one entropy value, one seed");
assert_ne!(mix_seed(7), mix_seed(8));
for entropy in [0, 1, 2, u64::MAX, 41] {
assert_ne!(avalanche(entropy), 0, "a xorshift state is never zero");
}
let a = RetrySchedule::seeded(ms(500), secs(30), 2.0, 1);
let b = RetrySchedule::seeded(ms(500), secs(30), 2.0, 2);
let seq = |s: &RetrySchedule| -> Vec<Duration> {
(0..8).map(|i| s.delay_for_attempt(i)).collect()
};
assert_ne!(seq(&a), seq(&b));
}
#[test]
fn a_factor_below_one_does_not_shrink_the_series() {
let s = RetrySchedule::new(ms(500), secs(30), 0.25, 1);
assert_eq!(s.raw_delay_for_attempt(0), ms(500));
assert_eq!(s.raw_delay_for_attempt(5), ms(500));
}
#[test]
fn a_hostile_factor_saturates_at_the_cap_instead_of_panicking() {
for factor in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -2.0, 1e300] {
let s = RetrySchedule::new(secs(1), secs(60), factor, 1);
for attempt in [0u32, 1, 30, u32::MAX] {
assert!(
s.delay_for_attempt(attempt) <= secs(60),
"{factor}: {attempt}"
);
}
}
}
#[test]
fn a_cap_below_the_floor_is_raised_to_the_floor() {
let s = RetrySchedule::new(secs(30), secs(1), 2.0, 1);
assert_eq!(s.max(), secs(30));
assert_eq!(s.raw_delay_for_attempt(9), secs(30));
}
#[test]
fn a_zero_floor_is_a_zero_delay_at_every_attempt() {
let s = RetrySchedule::new(Duration::ZERO, secs(1), 2.0, 1);
assert_eq!(s.raw_delay_for_attempt(0), Duration::ZERO);
assert_eq!(s.delay_for_attempt(0), Duration::ZERO);
assert_eq!(s.delay_for_attempt(1), Duration::ZERO);
assert_eq!(
s.delay_for_attempt(20),
Duration::ZERO,
"a disabled backoff never creeps up to its cap"
);
}
#[test]
fn the_jittered_series_is_finite_for_every_attempt() {
let seq = sequence(8);
assert_eq!(seq[0], ms(500));
assert!(seq.iter().all(|d| *d <= secs(30)));
}
}