use std::time::Duration;
#[derive(Debug, Clone)]
pub struct BackoffConfig {
pub max_retries: u32,
pub initial_delay: Duration,
pub multiplier: f64,
pub max_delay: Duration,
pub jitter: JitterStrategy,
pub retryable_statuses: Vec<u16>,
pub respect_retry_after: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JitterStrategy {
None,
Full,
Equal,
Decorrelated,
}
impl BackoffConfig {
pub fn none() -> Self {
Self {
max_retries: 0,
..Self::standard()
}
}
pub fn standard() -> Self {
Self {
max_retries: 3,
initial_delay: Duration::from_secs(1),
multiplier: 2.0,
max_delay: Duration::from_secs(60),
jitter: JitterStrategy::Full,
retryable_statuses: vec![429, 500, 502, 503, 504],
respect_retry_after: true,
}
}
pub fn aggressive() -> Self {
Self {
max_retries: 5,
initial_delay: Duration::from_millis(500),
multiplier: 2.0,
max_delay: Duration::from_secs(120),
jitter: JitterStrategy::Full,
retryable_statuses: vec![429, 500, 502, 503, 504],
respect_retry_after: true,
}
}
pub fn interactive() -> Self {
Self {
max_retries: 2,
initial_delay: Duration::from_millis(500),
multiplier: 1.5,
max_delay: Duration::from_secs(10),
jitter: JitterStrategy::Full,
retryable_statuses: vec![429, 500, 502, 503, 504],
respect_retry_after: true,
}
}
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
let base = self.initial_delay.as_secs_f64() * self.multiplier.powi(attempt as i32);
let capped = base.min(self.max_delay.as_secs_f64());
let jittered = match self.jitter {
JitterStrategy::None => capped,
JitterStrategy::Full => fastrand::f64() * capped,
JitterStrategy::Equal => capped / 2.0 + fastrand::f64() * (capped / 2.0),
JitterStrategy::Decorrelated => {
fastrand::f64() * capped
}
};
Duration::from_secs_f64(jittered)
}
}
impl Default for BackoffConfig {
fn default() -> Self {
Self::none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backoff_delay_exponential() {
let config = BackoffConfig {
max_retries: 5,
initial_delay: Duration::from_secs(1),
multiplier: 2.0,
max_delay: Duration::from_secs(60),
jitter: JitterStrategy::None,
retryable_statuses: vec![429],
respect_retry_after: false,
};
let d0 = config.delay_for_attempt(0);
let d1 = config.delay_for_attempt(1);
let d2 = config.delay_for_attempt(2);
let d3 = config.delay_for_attempt(3);
assert_eq!(d0, Duration::from_secs(1));
assert_eq!(d1, Duration::from_secs(2));
assert_eq!(d2, Duration::from_secs(4));
assert_eq!(d3, Duration::from_secs(8));
}
#[test]
fn test_backoff_delay_capped_at_max() {
let config = BackoffConfig {
max_retries: 10,
initial_delay: Duration::from_secs(1),
multiplier: 2.0,
max_delay: Duration::from_secs(5),
jitter: JitterStrategy::None,
retryable_statuses: vec![429],
respect_retry_after: false,
};
let d = config.delay_for_attempt(3);
assert_eq!(d, Duration::from_secs(5));
let d = config.delay_for_attempt(10);
assert_eq!(d, Duration::from_secs(5));
}
#[test]
fn test_backoff_jitter_full_in_range() {
let config = BackoffConfig {
max_retries: 3,
initial_delay: Duration::from_secs(1),
multiplier: 2.0,
max_delay: Duration::from_secs(60),
jitter: JitterStrategy::Full,
retryable_statuses: vec![429],
respect_retry_after: false,
};
for _ in 0..100 {
let d = config.delay_for_attempt(0);
assert!(d <= Duration::from_secs(1), "delay {:?} > 1s", d);
}
for _ in 0..100 {
let d = config.delay_for_attempt(1);
assert!(d <= Duration::from_secs(2), "delay {:?} > 2s", d);
}
}
#[test]
fn test_backoff_none_preset() {
let config = BackoffConfig::none();
assert_eq!(config.max_retries, 0);
}
#[test]
fn test_backoff_standard_preset() {
let config = BackoffConfig::standard();
assert_eq!(config.max_retries, 3);
assert_eq!(config.initial_delay, Duration::from_secs(1));
assert_eq!(config.multiplier, 2.0);
assert!(config.retryable_statuses.contains(&429));
assert!(config.retryable_statuses.contains(&503));
}
}