#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod jitter;
pub use jitter::{equal_jitter, full_jitter};
use core::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Kind {
Constant,
Linear { step: Duration },
Exponential { factor: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Backoff {
base: Duration,
kind: Kind,
max_delay: Duration,
max_retries: Option<u32>,
}
impl Backoff {
pub const fn constant(base: Duration) -> Self {
Self {
base,
kind: Kind::Constant,
max_delay: Duration::MAX,
max_retries: None,
}
}
pub const fn linear(base: Duration, step: Duration) -> Self {
Self {
base,
kind: Kind::Linear { step },
max_delay: Duration::MAX,
max_retries: None,
}
}
pub const fn exponential(base: Duration, factor: u32) -> Self {
Self {
base,
kind: Kind::Exponential { factor },
max_delay: Duration::MAX,
max_retries: None,
}
}
pub const fn with_max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = max_delay;
self
}
pub const fn with_max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = Some(max_retries);
self
}
pub const fn base(&self) -> Duration {
self.base
}
pub const fn max_delay(&self) -> Duration {
self.max_delay
}
pub const fn max_retries(&self) -> Option<u32> {
self.max_retries
}
pub fn delay(&self, attempt: u32) -> Option<Duration> {
if let Some(max) = self.max_retries {
if attempt >= max {
return None;
}
}
let raw = match self.kind {
Kind::Constant => self.base,
Kind::Linear { step } => self.base.saturating_add(step.saturating_mul(attempt)),
Kind::Exponential { factor } => {
if factor <= 1 {
self.base
} else {
let mut delay = self.base;
let mut i = 0;
while i < attempt {
if delay >= self.max_delay {
delay = self.max_delay;
break;
}
let next = delay.saturating_mul(factor);
if next == delay {
break;
}
delay = next;
i += 1;
}
delay
}
}
};
Some(if raw > self.max_delay {
self.max_delay
} else {
raw
})
}
pub const fn delays(&self) -> Delays {
Delays {
backoff: *self,
attempt: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct Delays {
backoff: Backoff,
attempt: u32,
}
impl Iterator for Delays {
type Item = Duration;
fn next(&mut self) -> Option<Self::Item> {
let delay = self.backoff.delay(self.attempt)?;
self.attempt = self.attempt.saturating_add(1);
Some(delay)
}
}
#[cfg(test)]
mod tests {
use super::*;
const MS: fn(u64) -> Duration = Duration::from_millis;
#[test]
fn constant_is_flat() {
let b = Backoff::constant(MS(50));
assert_eq!(b.delay(0), Some(MS(50)));
assert_eq!(b.delay(1000), Some(MS(50)));
}
#[test]
fn linear_grows_by_step() {
let b = Backoff::linear(MS(100), MS(25));
assert_eq!(b.delay(0), Some(MS(100)));
assert_eq!(b.delay(1), Some(MS(125)));
assert_eq!(b.delay(4), Some(MS(200)));
}
#[test]
fn exponential_doubles() {
let b = Backoff::exponential(MS(10), 2);
assert_eq!(b.delay(0), Some(MS(10)));
assert_eq!(b.delay(1), Some(MS(20)));
assert_eq!(b.delay(2), Some(MS(40)));
assert_eq!(b.delay(3), Some(MS(80)));
}
#[test]
fn exponential_factor_three() {
let b = Backoff::exponential(MS(1), 3);
assert_eq!(b.delay(0), Some(MS(1)));
assert_eq!(b.delay(1), Some(MS(3)));
assert_eq!(b.delay(2), Some(MS(9)));
}
#[test]
fn max_delay_caps() {
let b = Backoff::exponential(MS(100), 2).with_max_delay(MS(500));
assert_eq!(b.delay(0), Some(MS(100)));
assert_eq!(b.delay(2), Some(MS(400)));
assert_eq!(b.delay(3), Some(MS(500))); assert_eq!(b.delay(50), Some(MS(500)));
}
#[test]
fn max_retries_stops() {
let b = Backoff::constant(MS(10)).with_max_retries(3);
assert_eq!(b.delay(0), Some(MS(10)));
assert_eq!(b.delay(2), Some(MS(10)));
assert_eq!(b.delay(3), None);
assert_eq!(b.delay(100), None);
}
#[test]
fn exponential_factor_below_two_is_constant() {
let b = Backoff::exponential(MS(7), 1);
assert_eq!(b.delay(0), Some(MS(7)));
assert_eq!(b.delay(9), Some(MS(7)));
}
#[test]
fn huge_attempt_saturates_without_hanging() {
let b = Backoff::exponential(Duration::from_secs(1), 2);
assert_eq!(b.delay(u32::MAX), Some(Duration::MAX));
}
#[test]
fn zero_base_exponential_does_not_hang() {
let b = Backoff::exponential(Duration::ZERO, 2);
assert_eq!(b.delay(u32::MAX), Some(Duration::ZERO));
assert_eq!(b.delay(0), Some(Duration::ZERO));
let capped = Backoff::exponential(Duration::ZERO, 4).with_max_delay(MS(500));
assert_eq!(capped.delay(u32::MAX), Some(Duration::ZERO));
}
#[test]
fn linear_saturates() {
let b = Backoff::linear(Duration::MAX, Duration::from_secs(1));
assert_eq!(b.delay(10), Some(Duration::MAX));
}
#[test]
fn delays_iterator_respects_limit() {
let b = Backoff::exponential(MS(10), 2).with_max_retries(4);
let collected: heapless_vec::Vec = b.delays().collect();
assert_eq!(collected.as_slice(), &[MS(10), MS(20), MS(40), MS(80)]);
}
#[test]
fn getters() {
let b = Backoff::exponential(MS(5), 2)
.with_max_delay(MS(100))
.with_max_retries(7);
assert_eq!(b.base(), MS(5));
assert_eq!(b.max_delay(), MS(100));
assert_eq!(b.max_retries(), Some(7));
}
mod heapless_vec {
use core::time::Duration;
pub struct Vec {
data: [Duration; 16],
len: usize,
}
impl Vec {
pub fn as_slice(&self) -> &[Duration] {
&self.data[..self.len]
}
}
impl FromIterator<Duration> for Vec {
fn from_iter<I: IntoIterator<Item = Duration>>(iter: I) -> Self {
let mut data = [Duration::ZERO; 16];
let mut len = 0;
for d in iter {
data[len] = d;
len += 1;
}
Self { data, len }
}
}
}
}