use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub multiplier: f64,
pub jitter_fraction: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(10),
multiplier: 2.0,
jitter_fraction: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub config: RetryConfig,
}
impl RetryPolicy {
pub fn new(config: RetryConfig) -> Self {
Self { config }
}
pub fn next_delay(&self, attempt: u32) -> Option<Duration> {
if attempt > self.config.max_retries {
return None;
}
let base_ms = self.config.initial_backoff.as_millis() as f64
* self.config.multiplier.powi((attempt - 1) as i32);
let cap_ms = self.config.max_backoff.as_millis() as f64;
let capped = base_ms.min(cap_ms);
let jitter_ms = if self.config.jitter_fraction > 0.0 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
let random_factor = (nanos as f64 / u32::MAX as f64) * 2.0 - 1.0;
capped * self.config.jitter_fraction * random_factor
} else {
0.0
};
let total_ms = ((capped + jitter_ms).max(1.0)) as u64;
Some(Duration::from_millis(total_ms))
}
pub fn should_retry(&self, attempt: u32) -> bool {
attempt <= self.config.max_retries
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_retry_delay() {
let policy = RetryPolicy::new(RetryConfig {
max_retries: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(30),
multiplier: 2.0,
jitter_fraction: 0.0,
});
let d1 = policy.next_delay(1).unwrap();
let d2 = policy.next_delay(2).unwrap();
let d3 = policy.next_delay(3).unwrap();
assert_eq!(d1.as_millis(), 100);
assert_eq!(d2.as_millis(), 200);
assert_eq!(d3.as_millis(), 400);
assert!(policy.next_delay(4).is_none());
}
#[test]
fn delay_capped_at_max() {
let policy = RetryPolicy::new(RetryConfig {
max_retries: 10,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_millis(500),
multiplier: 10.0,
jitter_fraction: 0.0,
});
let d5 = policy.next_delay(5).unwrap();
assert!(d5 <= Duration::from_millis(500), "d5={d5:?} exceeds max");
}
#[test]
fn should_retry_boundary() {
let policy = RetryPolicy::new(RetryConfig {
max_retries: 2,
..Default::default()
});
assert!(policy.should_retry(1));
assert!(policy.should_retry(2));
assert!(!policy.should_retry(3));
}
}