use std::time::Duration;
pub trait IntervalFunction: Send + Sync {
fn next_interval(&self, attempt: usize) -> Duration;
}
#[derive(Debug, Clone)]
pub struct FixedInterval {
duration: Duration,
}
impl FixedInterval {
pub fn new(duration: Duration) -> Self {
Self { duration }
}
}
impl IntervalFunction for FixedInterval {
fn next_interval(&self, _attempt: usize) -> Duration {
self.duration
}
}
#[derive(Debug, Clone)]
pub struct ExponentialBackoff {
initial_interval: Duration,
multiplier: f64,
max_interval: Option<Duration>,
}
impl ExponentialBackoff {
pub fn new(initial_interval: Duration) -> Self {
Self {
initial_interval,
multiplier: 2.0,
max_interval: None,
}
}
pub fn multiplier(mut self, multiplier: f64) -> Self {
self.multiplier = multiplier;
self
}
pub fn max_interval(mut self, max_interval: Duration) -> Self {
self.max_interval = Some(max_interval);
self
}
}
impl IntervalFunction for ExponentialBackoff {
fn next_interval(&self, attempt: usize) -> Duration {
let multiplier = self.multiplier.powi(attempt as i32);
let interval = self.initial_interval.mul_f64(multiplier);
if let Some(max) = self.max_interval {
interval.min(max)
} else {
interval
}
}
}
#[derive(Debug, Clone)]
pub struct ExponentialRandomBackoff {
initial_interval: Duration,
multiplier: f64,
randomization_factor: f64,
max_interval: Option<Duration>,
}
impl ExponentialRandomBackoff {
pub fn new(initial_interval: Duration, randomization_factor: f64) -> Self {
Self {
initial_interval,
multiplier: 2.0,
randomization_factor: randomization_factor.clamp(0.0, 1.0),
max_interval: None,
}
}
pub fn multiplier(mut self, multiplier: f64) -> Self {
self.multiplier = multiplier;
self
}
pub fn max_interval(mut self, max_interval: Duration) -> Self {
self.max_interval = Some(max_interval);
self
}
fn randomize(&self, duration: Duration) -> Duration {
use rand::Rng;
let mut rng = rand::rng();
let delta = duration.as_secs_f64() * self.randomization_factor;
let min = duration.as_secs_f64() - delta;
let max = duration.as_secs_f64() + delta;
let randomized = rng.random_range(min..=max);
Duration::from_secs_f64(randomized.max(0.0))
}
}
impl IntervalFunction for ExponentialRandomBackoff {
fn next_interval(&self, attempt: usize) -> Duration {
let multiplier = self.multiplier.powi(attempt as i32);
let interval = self.initial_interval.mul_f64(multiplier);
let capped = if let Some(max) = self.max_interval {
interval.min(max)
} else {
interval
};
self.randomize(capped)
}
}
pub struct FnInterval<F> {
f: F,
}
impl<F> FnInterval<F>
where
F: Fn(usize) -> Duration + Send + Sync,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> IntervalFunction for FnInterval<F>
where
F: Fn(usize) -> Duration + Send + Sync,
{
fn next_interval(&self, attempt: usize) -> Duration {
(self.f)(attempt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_interval_returns_same_duration() {
let backoff = FixedInterval::new(Duration::from_secs(1));
assert_eq!(backoff.next_interval(0), Duration::from_secs(1));
assert_eq!(backoff.next_interval(1), Duration::from_secs(1));
assert_eq!(backoff.next_interval(10), Duration::from_secs(1));
}
#[test]
fn exponential_backoff_grows() {
let backoff = ExponentialBackoff::new(Duration::from_millis(100));
assert_eq!(backoff.next_interval(0), Duration::from_millis(100));
assert_eq!(backoff.next_interval(1), Duration::from_millis(200));
assert_eq!(backoff.next_interval(2), Duration::from_millis(400));
assert_eq!(backoff.next_interval(3), Duration::from_millis(800));
}
#[test]
fn exponential_backoff_custom_multiplier() {
let backoff = ExponentialBackoff::new(Duration::from_millis(100)).multiplier(3.0);
assert_eq!(backoff.next_interval(0), Duration::from_millis(100));
assert_eq!(backoff.next_interval(1), Duration::from_millis(300));
assert_eq!(backoff.next_interval(2), Duration::from_millis(900));
}
#[test]
fn exponential_backoff_respects_max() {
let backoff = ExponentialBackoff::new(Duration::from_millis(100))
.max_interval(Duration::from_millis(500));
assert_eq!(backoff.next_interval(0), Duration::from_millis(100));
assert_eq!(backoff.next_interval(1), Duration::from_millis(200));
assert_eq!(backoff.next_interval(2), Duration::from_millis(400));
assert_eq!(backoff.next_interval(3), Duration::from_millis(500)); assert_eq!(backoff.next_interval(4), Duration::from_millis(500)); }
#[test]
fn exponential_random_backoff_has_variance() {
let backoff = ExponentialRandomBackoff::new(Duration::from_millis(100), 0.5);
let mut intervals = Vec::new();
for _ in 0..10 {
intervals.push(backoff.next_interval(1));
}
let all_same = intervals.windows(2).all(|w| w[0] == w[1]);
assert!(!all_same, "Randomized intervals should vary");
for interval in intervals {
assert!(
interval >= Duration::from_millis(100) && interval <= Duration::from_millis(300),
"Interval {:?} outside expected range",
interval
);
}
}
#[test]
fn fn_interval_uses_custom_function() {
let backoff = FnInterval::new(|attempt| Duration::from_secs((attempt + 1) as u64));
assert_eq!(backoff.next_interval(0), Duration::from_secs(1));
assert_eq!(backoff.next_interval(1), Duration::from_secs(2));
assert_eq!(backoff.next_interval(2), Duration::from_secs(3));
}
}