use std::num::NonZeroU32;
use std::time::Duration;
const DEFAULT_FACTOR: u32 = 2;
const DEFAULT_BASE: Duration = Duration::from_millis(100);
const DEFAULT_MAX_RETRIES: u32 = 3;
const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(30);
const DECORRELATED_MULTIPLIER: u32 = 3;
pub trait Backoff {
fn next_delay(&mut self) -> Option<Duration>;
fn jittered(self) -> Jittered<Self>
where
Self: Sized,
{
Jittered::new(self)
}
fn jittered_with_seed(self, seed: u64) -> Jittered<Self>
where
Self: Sized,
{
Jittered::with_seed(self, seed)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackoffConfigError {
ZeroBase,
ZeroFactor,
MaxDelayBelowBase,
}
impl std::fmt::Display for BackoffConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroBase => write!(f, "backoff `base` must be non-zero"),
Self::ZeroFactor => write!(f, "backoff `factor` must be at least 1"),
Self::MaxDelayBelowBase => write!(f, "backoff `max_delay` must be >= `base`"),
}
}
}
impl std::error::Error for BackoffConfigError {}
#[derive(Debug, Clone)]
pub struct ExponentialBackoffConfig {
pub factor: u32,
pub base: Duration,
pub max_retries: u32,
pub max_delay: Duration,
}
impl Default for ExponentialBackoffConfig {
fn default() -> Self {
Self {
factor: DEFAULT_FACTOR,
base: DEFAULT_BASE,
max_retries: DEFAULT_MAX_RETRIES,
max_delay: DEFAULT_MAX_DELAY,
}
}
}
#[derive(Debug, Clone)]
pub struct ExponentialBackoff {
factor: NonZeroU32,
max_delay: Duration,
next: Duration, retries_left: u32, }
impl ExponentialBackoff {
pub fn new(config: ExponentialBackoffConfig) -> Result<Self, BackoffConfigError> {
let ExponentialBackoffConfig {
factor,
base,
max_retries,
max_delay,
} = config;
if base.is_zero() {
return Err(BackoffConfigError::ZeroBase);
}
let factor = NonZeroU32::new(factor).ok_or(BackoffConfigError::ZeroFactor)?;
if max_delay < base {
return Err(BackoffConfigError::MaxDelayBelowBase);
}
Ok(Self {
factor,
max_delay,
next: base,
retries_left: max_retries,
})
}
}
impl Default for ExponentialBackoff {
fn default() -> Self {
Self::new(ExponentialBackoffConfig::default())
.expect("default exponential-backoff config is valid")
}
}
impl Backoff for ExponentialBackoff {
fn next_delay(&mut self) -> Option<Duration> {
if self.retries_left == 0 {
return None; }
self.retries_left -= 1;
let delay = self.next.min(self.max_delay);
self.next = self
.next
.saturating_mul(self.factor.get())
.min(self.max_delay);
Some(delay)
}
}
fn rand_duration(rng: &mut fastrand::Rng, lo: Duration, hi: Duration) -> Duration {
let lo = lo.as_nanos().min(u64::MAX as u128) as u64;
let hi = hi.as_nanos().min(u64::MAX as u128) as u64;
if hi <= lo {
return Duration::from_nanos(lo);
}
Duration::from_nanos(lo + rng.u64(0..=(hi - lo)))
}
#[derive(Debug)]
pub struct Jittered<B> {
inner: B,
rng: fastrand::Rng,
}
impl<B> Jittered<B> {
pub fn new(inner: B) -> Self {
Self {
inner,
rng: fastrand::Rng::new(),
}
}
pub fn with_seed(inner: B, seed: u64) -> Self {
Self {
inner,
rng: fastrand::Rng::with_seed(seed),
}
}
}
impl<B: Backoff> Backoff for Jittered<B> {
fn next_delay(&mut self) -> Option<Duration> {
let delay = self.inner.next_delay()?;
Some(rand_duration(&mut self.rng, Duration::ZERO, delay))
}
}
#[derive(Debug, Clone)]
pub struct DecorrelatedBackoffConfig {
pub base: Duration,
pub max_retries: u32,
pub max_delay: Duration,
}
impl Default for DecorrelatedBackoffConfig {
fn default() -> Self {
Self {
base: DEFAULT_BASE,
max_retries: DEFAULT_MAX_RETRIES,
max_delay: DEFAULT_MAX_DELAY,
}
}
}
#[derive(Debug)]
pub struct DecorrelatedBackoff {
base: Duration,
max_delay: Duration,
prev: Duration, retries_left: u32,
rng: fastrand::Rng,
}
impl DecorrelatedBackoff {
pub fn new(config: DecorrelatedBackoffConfig) -> Result<Self, BackoffConfigError> {
Self::build(config, fastrand::Rng::new())
}
pub fn with_seed(
config: DecorrelatedBackoffConfig,
seed: u64,
) -> Result<Self, BackoffConfigError> {
Self::build(config, fastrand::Rng::with_seed(seed))
}
fn build(
config: DecorrelatedBackoffConfig,
rng: fastrand::Rng,
) -> Result<Self, BackoffConfigError> {
let DecorrelatedBackoffConfig {
base,
max_retries,
max_delay,
} = config;
if base.is_zero() {
return Err(BackoffConfigError::ZeroBase);
}
if max_delay < base {
return Err(BackoffConfigError::MaxDelayBelowBase);
}
Ok(Self {
base,
max_delay,
prev: base,
retries_left: max_retries,
rng,
})
}
}
impl Backoff for DecorrelatedBackoff {
fn next_delay(&mut self) -> Option<Duration> {
if self.retries_left == 0 {
return None; }
self.retries_left -= 1;
let hi = self.prev.saturating_mul(DECORRELATED_MULTIPLIER);
let delay = rand_duration(&mut self.rng, self.base, hi)
.min(self.max_delay)
.max(self.base);
self.prev = delay; Some(delay)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
fn exp(base: u64, factor: u32, max_delay: u64, max_retries: u32) -> ExponentialBackoff {
ExponentialBackoff::new(ExponentialBackoffConfig {
factor,
base: secs(base),
max_retries,
max_delay: secs(max_delay),
})
.unwrap()
}
#[test]
fn grows_exponentially_then_gives_up() {
let mut b = exp(1, 2, 100, 5);
assert_eq!(b.next_delay(), Some(secs(1)));
assert_eq!(b.next_delay(), Some(secs(2)));
assert_eq!(b.next_delay(), Some(secs(4)));
assert_eq!(b.next_delay(), Some(secs(8)));
assert_eq!(b.next_delay(), Some(secs(16)));
assert_eq!(b.next_delay(), None); }
#[test]
fn factor_of_one_holds_the_delay_constant() {
assert_eq!(drain(exp(7, 1, 100, 4)), vec![secs(7); 4]);
}
#[test]
fn delay_is_capped_at_max() {
let mut b = exp(10, 10, 30, 4);
assert_eq!(b.next_delay(), Some(secs(10)));
assert_eq!(b.next_delay(), Some(secs(30))); assert_eq!(b.next_delay(), Some(secs(30))); }
#[test]
fn zero_retries_means_one_attempt() {
let mut b = exp(1, 2, 100, 0);
assert_eq!(b.next_delay(), None); }
#[test]
fn huge_factor_does_not_panic() {
let mut b = ExponentialBackoff::new(ExponentialBackoffConfig {
factor: u32::MAX,
base: Duration::from_secs(u64::MAX / 2),
max_retries: 3,
max_delay: Duration::MAX,
})
.unwrap();
assert!(b.next_delay().unwrap() > Duration::ZERO);
assert!(b.next_delay().unwrap() > Duration::ZERO);
}
#[test]
fn rejects_degenerate_configs() {
assert!(matches!(
ExponentialBackoff::new(ExponentialBackoffConfig {
base: secs(0),
..Default::default()
}),
Err(BackoffConfigError::ZeroBase)
));
assert!(matches!(
ExponentialBackoff::new(ExponentialBackoffConfig {
factor: 0,
..Default::default()
}),
Err(BackoffConfigError::ZeroFactor)
));
assert!(matches!(
ExponentialBackoff::new(ExponentialBackoffConfig {
base: secs(10),
max_delay: secs(5),
..Default::default()
}),
Err(BackoffConfigError::MaxDelayBelowBase)
));
}
fn exp6() -> ExponentialBackoff {
exp(1, 2, 100, 6) }
fn drain(mut b: impl Backoff) -> Vec<Duration> {
std::iter::from_fn(move || b.next_delay()).collect()
}
#[test]
fn full_jitter_stays_within_bounds() {
let plain = drain(exp6());
let mut j = Jittered::with_seed(exp6(), 42);
for p in &plain {
let d = j.next_delay().unwrap();
assert!(
d <= *p,
"full jitter exceeded the base delay: {d:?} > {p:?}"
);
}
assert_eq!(j.next_delay(), None);
}
#[test]
fn seed_makes_jitter_reproducible() {
let seq = |seed| drain(Jittered::with_seed(exp6(), seed));
assert_eq!(seq(7), seq(7));
assert_ne!(seq(1), seq(2));
}
#[test]
fn jitter_actually_moves_the_delay() {
let jittered = drain(Jittered::with_seed(exp6(), 5));
let plain = drain(exp6());
assert!(
jittered.iter().zip(&plain).any(|(d, p)| *d < *p / 2),
"jitter never dropped below half the plain delay"
);
assert!(
jittered.iter().zip(&plain).any(|(d, p)| *d > *p / 2),
"jitter never rose above half the plain delay"
);
}
#[test]
fn jittered_combinator_wraps_the_inner_strategy() {
let mut b = exp6().jittered();
let plain = drain(exp6());
for p in &plain {
let d = b.next_delay().unwrap();
assert!(
d <= *p,
"combinator exceeded the inner delay: {d:?} for {p:?}"
);
}
assert_eq!(b.next_delay(), None);
}
#[test]
fn jittered_with_seed_matches_the_named_constructor() {
assert_eq!(
drain(exp6().jittered_with_seed(42)),
drain(Jittered::with_seed(exp6(), 42))
);
assert_eq!(
drain(exp6().jittered_with_seed(7)),
drain(exp6().jittered_with_seed(7))
);
assert_ne!(
drain(exp6().jittered_with_seed(1)),
drain(exp6().jittered_with_seed(2))
);
}
#[test]
fn unseeded_jitter_differs_between_instances() {
assert_ne!(drain(exp6().jittered()), drain(exp6().jittered()));
}
#[test]
fn jitter_handles_zero_and_extreme_delays() {
struct Fixed(std::vec::IntoIter<Duration>);
impl Backoff for Fixed {
fn next_delay(&mut self) -> Option<Duration> {
self.0.next()
}
}
let inner = Fixed(vec![Duration::ZERO, Duration::MAX, secs(1)].into_iter());
let mut j = Jittered::with_seed(inner, 1);
assert_eq!(j.next_delay(), Some(Duration::ZERO)); let _ = j.next_delay().unwrap(); assert!(j.next_delay().unwrap() <= secs(1));
assert_eq!(j.next_delay(), None);
}
fn dec(base: u64, max_delay: u64, max_retries: u32, seed: u64) -> DecorrelatedBackoff {
DecorrelatedBackoff::with_seed(
DecorrelatedBackoffConfig {
base: secs(base),
max_retries,
max_delay: secs(max_delay),
},
seed,
)
.unwrap()
}
#[test]
fn decorrelated_draws_each_delay_from_the_previous_one() {
let (base, cap) = (secs(1), secs(30));
let mut ever_above_double = false;
for seed in 0..16 {
let mut prev = base;
let mut b = dec(1, 30, 40, seed);
while let Some(d) = b.next_delay() {
let hi = (prev * 3).min(cap);
assert!(
d >= base && d <= hi,
"{d:?} outside [{base:?}, {hi:?}] (seed {seed})"
);
ever_above_double |= d > prev * 2 && d < cap;
prev = d;
}
}
assert!(ever_above_double, "no delay ever exceeded `prev * 2`");
}
#[test]
fn decorrelated_feeds_the_capped_delay_into_the_next_draw() {
let cap = secs(2);
let (mut below, mut total) = (0u32, 0u32);
for seed in 0..32 {
for d in drain(dec(1, 2, 32, seed)) {
below += u32::from(d < cap);
total += 1;
}
}
assert!(
below * 10 >= total,
"sequence stopped recovering below the cap: {below}/{total}"
);
}
#[test]
fn unseeded_decorrelated_differs_between_instances() {
let cfg = || DecorrelatedBackoffConfig {
base: secs(1),
max_retries: 8,
max_delay: secs(60),
};
assert_ne!(
drain(DecorrelatedBackoff::new(cfg()).unwrap()),
drain(DecorrelatedBackoff::new(cfg()).unwrap())
);
}
#[test]
fn decorrelated_first_delay_is_already_random() {
let firsts: Vec<_> = (0..16)
.map(|seed| dec(1, 1000, 1, seed).next_delay().unwrap())
.collect();
assert!(firsts.iter().all(|d| *d >= secs(1) && *d <= secs(3)));
assert!(
firsts.iter().any(|d| *d != secs(1)),
"first delay never moved off `base`"
);
}
#[test]
fn decorrelated_gives_up_after_max_retries() {
assert_eq!(drain(dec(1, 100, 5, 3)).len(), 5);
assert_eq!(dec(1, 100, 0, 3).next_delay(), None); }
#[test]
fn decorrelated_seed_is_reproducible() {
let seq = |seed| drain(dec(1, 60, 8, seed));
assert_eq!(seq(7), seq(7));
assert_ne!(seq(1), seq(2));
}
#[test]
fn decorrelated_reaches_the_cap() {
let hit = (0..32).any(|seed| drain(dec(1, 4, 24, seed)).contains(&secs(4)));
assert!(hit, "cap was never reached");
}
#[test]
fn decorrelated_base_equal_to_cap_is_constant() {
assert_eq!(drain(dec(5, 5, 4, 12345)), vec![secs(5); 4]);
}
#[test]
fn decorrelated_huge_base_does_not_panic() {
let mut b = DecorrelatedBackoff::with_seed(
DecorrelatedBackoffConfig {
base: Duration::from_secs(u64::MAX / 2),
max_retries: 3,
max_delay: Duration::MAX,
},
1,
)
.unwrap();
let base = Duration::from_secs(u64::MAX / 2);
assert!(b.next_delay().unwrap() >= base);
assert!(b.next_delay().unwrap() >= base);
}
#[test]
fn decorrelated_rejects_degenerate_configs() {
assert!(matches!(
DecorrelatedBackoff::new(DecorrelatedBackoffConfig {
base: secs(0),
..Default::default()
}),
Err(BackoffConfigError::ZeroBase)
));
assert!(matches!(
DecorrelatedBackoff::new(DecorrelatedBackoffConfig {
base: secs(10),
max_delay: secs(5),
..Default::default()
}),
Err(BackoffConfigError::MaxDelayBelowBase)
));
}
}