use std::num::NonZeroU32;
use std::time::Duration;
const DEFAULT_INITIAL: Duration = Duration::from_millis(500);
const DEFAULT_MAX: Duration = Duration::from_secs(30);
const DEFAULT_MAX_ATTEMPTS: u32 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BackoffPolicy {
pub(crate) initial: Duration,
pub(crate) max: Duration,
pub(crate) max_attempts: Option<NonZeroU32>,
}
impl Default for BackoffPolicy {
fn default() -> Self {
Self {
initial: DEFAULT_INITIAL,
max: DEFAULT_MAX,
max_attempts: NonZeroU32::new(DEFAULT_MAX_ATTEMPTS),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Jitter(u64);
impl Jitter {
#[must_use]
fn from_entropy() -> Self {
use std::hash::{BuildHasher, Hasher, RandomState};
let seed = RandomState::new().build_hasher().finish();
Self::from_seed(seed)
}
#[must_use]
const fn from_seed(seed: u64) -> Self {
Self(if seed == 0 {
0x9E37_79B9_7F4A_7C15
} else {
seed
})
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, bound: Duration) -> Duration {
let nanos = u64::try_from(bound.as_nanos()).unwrap_or(u64::MAX);
match nanos.checked_add(1) {
Some(range) => Duration::from_nanos(self.next_u64() % range),
None => bound,
}
}
}
#[derive(Debug)]
pub(crate) struct Backoff {
policy: BackoffPolicy,
attempts: u32,
base: Duration,
jitter: Jitter,
}
impl Backoff {
#[must_use]
pub(crate) fn new(policy: BackoffPolicy) -> Self {
Self {
policy,
attempts: 0,
base: policy.initial,
jitter: Jitter::from_entropy(),
}
}
#[must_use]
#[cfg(test)]
pub(crate) fn with_seed(policy: BackoffPolicy, seed: u64) -> Self {
Self {
policy,
attempts: 0,
base: policy.initial,
jitter: Jitter::from_seed(seed),
}
}
#[must_use]
#[inline]
pub(crate) const fn attempts(&self) -> u32 {
self.attempts
}
#[must_use]
pub(crate) fn next_delay(&mut self) -> Option<Duration> {
self.attempts = self.attempts.checked_add(1)?;
if let Some(max) = self.policy.max_attempts
&& self.attempts > max.get()
{
return None;
}
let delay = self.jitter.below(self.base);
self.base = self
.base
.checked_mul(2)
.unwrap_or(self.policy.max)
.min(self.policy.max);
Some(delay)
}
pub(crate) fn reset(&mut self) {
self.attempts = 0;
self.base = self.policy.initial;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(max_attempts: Option<u32>) -> BackoffPolicy {
BackoffPolicy {
initial: Duration::from_millis(500),
max: Duration::from_secs(4),
max_attempts: max_attempts.and_then(NonZeroU32::new),
}
}
#[test]
fn test_backoff_delays_stay_within_the_doubling_base() {
let mut backoff = Backoff::with_seed(policy(None), 42);
let bases = [
Duration::from_millis(500),
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(4),
Duration::from_secs(4),
Duration::from_secs(4),
];
for base in bases {
match backoff.next_delay() {
Some(delay) => assert!(delay <= base, "{delay:?} exceeds base {base:?}"),
None => panic!("an unbounded policy must never exhaust"),
}
}
}
#[test]
fn test_backoff_is_capped_by_max_attempts() {
let mut backoff = Backoff::with_seed(policy(Some(3)), 7);
assert!(backoff.next_delay().is_some());
assert!(backoff.next_delay().is_some());
assert!(backoff.next_delay().is_some());
assert_eq!(backoff.next_delay(), None);
assert_eq!(backoff.attempts(), 4);
}
#[test]
fn test_backoff_reset_restores_the_first_delay_and_the_budget() {
let mut backoff = Backoff::with_seed(policy(Some(2)), 7);
assert!(backoff.next_delay().is_some());
assert!(backoff.next_delay().is_some());
backoff.reset();
assert_eq!(backoff.attempts(), 0);
match backoff.next_delay() {
Some(delay) => assert!(delay <= Duration::from_millis(500)),
None => panic!("a reset schedule must have budget again"),
}
}
#[test]
fn test_backoff_is_jittered_not_constant() {
let mut backoff = Backoff::with_seed(policy(None), 12345);
let mut seen = std::collections::BTreeSet::new();
for _ in 0..8 {
if let Some(delay) = backoff.next_delay() {
seen.insert(delay);
}
}
assert!(seen.len() > 1, "delays did not vary: {seen:?}");
}
#[test]
fn test_backoff_same_seed_gives_same_schedule() {
let mut a = Backoff::with_seed(policy(None), 99);
let mut b = Backoff::with_seed(policy(None), 99);
for _ in 0..5 {
assert_eq!(a.next_delay(), b.next_delay());
}
}
}