use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_retries: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub backoff_multiplier: f64,
}
impl RetryPolicy {
#[must_use]
pub fn disabled() -> Self {
Self {
max_retries: 0,
initial_backoff: Duration::ZERO,
max_backoff: Duration::ZERO,
backoff_multiplier: 1.0,
}
}
#[must_use]
pub fn conservative() -> Self {
Self {
max_retries: 2,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(2),
backoff_multiplier: 2.0,
}
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.max_retries > 0
}
#[must_use]
pub fn backoff_for(&self, attempt: u32) -> Duration {
if attempt == 0 {
return Duration::ZERO;
}
let exp = self.backoff_multiplier.powi((attempt - 1) as i32);
let millis = self.initial_backoff.as_secs_f64() * 1000.0 * exp;
let capped = millis.min(self.max_backoff.as_secs_f64() * 1000.0);
Duration::from_millis(capped as u64)
}
}
impl Default for RetryPolicy {
fn default() -> Self {
Self::disabled()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_disabled() {
assert!(!RetryPolicy::default().is_enabled());
assert_eq!(RetryPolicy::default().backoff_for(1), Duration::ZERO);
assert_eq!(RetryPolicy::conservative().backoff_for(0), Duration::ZERO);
}
#[test]
fn conservative_is_bounded() {
let policy = RetryPolicy::conservative();
assert!(policy.is_enabled());
assert_eq!(policy.max_retries, 2);
}
#[test]
fn backoff_grows_then_caps() {
let policy = RetryPolicy {
max_retries: 10,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_millis(500),
backoff_multiplier: 2.0,
};
assert_eq!(policy.backoff_for(1), Duration::from_millis(100));
assert_eq!(policy.backoff_for(2), Duration::from_millis(200));
assert_eq!(policy.backoff_for(3), Duration::from_millis(400));
assert_eq!(policy.backoff_for(4), Duration::from_millis(500));
}
}