use rand::Rng;
use rand::RngExt;
pub fn fibonacci(n: u8) -> u64 {
match n {
0 => 0,
1 | 2 => 1,
_ => {
let mut a = 1u64;
let mut b = 1u64;
for _ in 2..n {
let next = a.saturating_add(b);
a = b;
b = next;
}
b
}
}
}
fn apply_jitter<R: Rng>(base: f64, jitter_factor: f64, rng: &mut R) -> u64 {
let jitter_factor = jitter_factor.clamp(0.0, 1.0);
let random_scalar: f64 = rng.random_range(0.0..=1.0);
let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
(base * jitter_blend) as u64
}
pub trait BackoffStrategy {
fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64>;
fn should_retry(&self, attempt: u8) -> bool;
fn max_attempts(&self) -> u8;
}
#[derive(Debug, Clone, Copy)]
pub struct ExponentialBackoff {
pub max_attempts: u8,
pub base_delay_ms: u64,
pub multiplier: f64,
pub max_delay_ms: u64,
pub jitter_factor: f64,
}
impl ExponentialBackoff {
pub fn new() -> Self {
Self::default()
}
pub fn base_delay_ms(mut self, ms: u64) -> Self {
self.base_delay_ms = ms;
self
}
pub fn multiplier(mut self, multiplier: f64) -> Self {
self.multiplier = multiplier;
self
}
pub fn max_delay_ms(mut self, ms: u64) -> Self {
self.max_delay_ms = ms;
self
}
pub fn max_attempts(mut self, attempts: u8) -> Self {
self.max_attempts = attempts;
self
}
pub fn jitter_factor(mut self, factor: f64) -> Self {
self.jitter_factor = factor.clamp(0.0, 1.0);
self
}
}
impl Default for ExponentialBackoff {
fn default() -> Self {
Self {
max_attempts: 3,
base_delay_ms: 100,
multiplier: 2.0,
max_delay_ms: 10_000,
jitter_factor: 1.0, }
}
}
impl BackoffStrategy for ExponentialBackoff {
fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
if attempt >= self.max_attempts {
return None;
}
let exponent = attempt.saturating_sub(1) as i32;
let base_exponential = (self.base_delay_ms as f64) * self.multiplier.powi(exponent);
let capped = base_exponential.min(self.max_delay_ms as f64);
Some(apply_jitter(capped, self.jitter_factor, rng))
}
fn should_retry(&self, attempt: u8) -> bool {
attempt < self.max_attempts
}
fn max_attempts(&self) -> u8 {
self.max_attempts
}
}
#[derive(Debug, Clone, Copy)]
pub struct ConstantBackoff {
pub delay_ms: u64,
pub max_attempts: u8,
pub jitter_factor: f64,
}
impl ConstantBackoff {
pub fn new() -> Self {
Self::default()
}
pub fn delay_ms(mut self, ms: u64) -> Self {
self.delay_ms = ms;
self
}
pub fn max_attempts(mut self, attempts: u8) -> Self {
self.max_attempts = attempts;
self
}
pub fn jitter_factor(mut self, factor: f64) -> Self {
self.jitter_factor = factor.clamp(0.0, 1.0);
self
}
}
impl Default for ConstantBackoff {
fn default() -> Self {
Self {
delay_ms: 100,
max_attempts: 3,
jitter_factor: 0.0, }
}
}
impl BackoffStrategy for ConstantBackoff {
fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
if attempt >= self.max_attempts {
return None;
}
Some(apply_jitter(self.delay_ms as f64, self.jitter_factor, rng))
}
fn should_retry(&self, attempt: u8) -> bool {
attempt < self.max_attempts
}
fn max_attempts(&self) -> u8 {
self.max_attempts
}
}
#[derive(Debug, Clone, Copy)]
pub struct FibonacciBackoff {
pub base_delay_ms: u64,
pub max_delay_ms: u64,
pub max_attempts: u8,
pub jitter_factor: f64,
}
impl FibonacciBackoff {
pub fn new() -> Self {
Self::default()
}
pub fn base_delay_ms(mut self, ms: u64) -> Self {
self.base_delay_ms = ms;
self
}
pub fn max_delay_ms(mut self, ms: u64) -> Self {
self.max_delay_ms = ms;
self
}
pub fn max_attempts(mut self, attempts: u8) -> Self {
self.max_attempts = attempts;
self
}
pub fn jitter_factor(mut self, factor: f64) -> Self {
self.jitter_factor = factor.clamp(0.0, 1.0);
self
}
}
impl Default for FibonacciBackoff {
fn default() -> Self {
Self {
base_delay_ms: 100,
max_delay_ms: 10_000,
max_attempts: 8,
jitter_factor: 1.0, }
}
}
impl BackoffStrategy for FibonacciBackoff {
fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
if attempt >= self.max_attempts {
return None;
}
let fib = fibonacci(attempt);
let base = ((self.base_delay_ms as f64) * (fib as f64)).min(self.max_delay_ms as f64);
Some(apply_jitter(base, self.jitter_factor, rng))
}
fn should_retry(&self, attempt: u8) -> bool {
attempt < self.max_attempts
}
fn max_attempts(&self) -> u8 {
self.max_attempts
}
}
#[derive(Debug, Clone, Copy)]
pub enum BackoffPolicy {
Exponential(ExponentialBackoff),
Constant(ConstantBackoff),
Fibonacci(FibonacciBackoff),
}
impl BackoffPolicy {
pub fn max_attempts(&self) -> u8 {
match self {
BackoffPolicy::Exponential(policy) => policy.max_attempts,
BackoffPolicy::Constant(policy) => policy.max_attempts,
BackoffPolicy::Fibonacci(policy) => policy.max_attempts,
}
}
}
impl BackoffStrategy for BackoffPolicy {
fn delay<R: Rng>(&self, attempt: u8, rng: &mut R) -> Option<u64> {
match self {
BackoffPolicy::Exponential(policy) => policy.delay(attempt, rng),
BackoffPolicy::Constant(policy) => policy.delay(attempt, rng),
BackoffPolicy::Fibonacci(policy) => policy.delay(attempt, rng),
}
}
fn should_retry(&self, attempt: u8) -> bool {
match self {
BackoffPolicy::Exponential(policy) => policy.should_retry(attempt),
BackoffPolicy::Constant(policy) => policy.should_retry(attempt),
BackoffPolicy::Fibonacci(policy) => policy.should_retry(attempt),
}
}
fn max_attempts(&self) -> u8 {
match self {
BackoffPolicy::Exponential(policy) => policy.max_attempts(),
BackoffPolicy::Constant(policy) => policy.max_attempts(),
BackoffPolicy::Fibonacci(policy) => policy.max_attempts(),
}
}
}
impl From<ExponentialBackoff> for BackoffPolicy {
fn from(value: ExponentialBackoff) -> Self {
BackoffPolicy::Exponential(value)
}
}
impl From<ConstantBackoff> for BackoffPolicy {
fn from(value: ConstantBackoff) -> Self {
BackoffPolicy::Constant(value)
}
}
impl From<FibonacciBackoff> for BackoffPolicy {
fn from(value: FibonacciBackoff) -> Self {
BackoffPolicy::Fibonacci(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn test_exponential_backoff_builder() {
let backoff = ExponentialBackoff::new()
.base_delay_ms(200)
.multiplier(3.0)
.max_delay_ms(5000)
.max_attempts(5)
.jitter_factor(0.5);
assert_eq!(backoff.base_delay_ms, 200);
assert_eq!(backoff.multiplier, 3.0);
assert_eq!(backoff.max_delay_ms, 5000);
assert_eq!(backoff.max_attempts, 5);
assert_eq!(backoff.jitter_factor, 0.5);
}
#[test]
fn test_exponential_delays() {
let backoff = ExponentialBackoff::new()
.base_delay_ms(100)
.multiplier(2.0)
.jitter_factor(0.0);
let mut rng = StdRng::seed_from_u64(42);
assert_eq!(backoff.delay(1, &mut rng), Some(100));
assert_eq!(backoff.delay(2, &mut rng), Some(200));
assert_eq!(backoff.delay(3, &mut rng), None); }
#[test]
fn test_constant_backoff() {
let backoff = ConstantBackoff::new()
.delay_ms(500)
.max_attempts(4)
.jitter_factor(0.0);
let mut rng = StdRng::seed_from_u64(42);
assert_eq!(backoff.delay(1, &mut rng), Some(500));
assert_eq!(backoff.delay(2, &mut rng), Some(500));
assert_eq!(backoff.delay(3, &mut rng), Some(500));
assert_eq!(backoff.delay(4, &mut rng), None);
}
#[test]
fn test_fibonacci_sequence() {
assert_eq!(fibonacci(1), 1);
assert_eq!(fibonacci(2), 1);
assert_eq!(fibonacci(3), 2);
assert_eq!(fibonacci(4), 3);
assert_eq!(fibonacci(5), 5);
assert_eq!(fibonacci(6), 8);
assert_eq!(fibonacci(7), 13);
}
#[test]
fn test_fibonacci_backoff() {
let backoff = FibonacciBackoff::new()
.base_delay_ms(100)
.max_attempts(5)
.jitter_factor(0.0);
let mut rng = StdRng::seed_from_u64(42);
assert_eq!(backoff.delay(1, &mut rng), Some(100)); assert_eq!(backoff.delay(2, &mut rng), Some(100)); assert_eq!(backoff.delay(3, &mut rng), Some(200)); assert_eq!(backoff.delay(4, &mut rng), Some(300)); assert_eq!(backoff.delay(5, &mut rng), None); }
#[test]
fn test_jitter_application() {
let backoff = ConstantBackoff::new().delay_ms(1000).jitter_factor(1.0);
let mut rng = StdRng::seed_from_u64(42);
let delays: Vec<u64> = (1..10).filter_map(|i| backoff.delay(i, &mut rng)).collect();
let all_different = delays.windows(2).any(|w| w[0] != w[1]);
assert!(all_different, "Full jitter should produce varying delays");
assert!(delays.iter().all(|&d| d <= 1000));
}
}