use std::time::Duration;
use time::OffsetDateTime;
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub base_delay: Duration,
pub max_delay: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 5,
base_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(300),
}
}
}
impl RetryPolicy {
pub fn next_attempt_at(&self, attempt: u32) -> OffsetDateTime {
let delay = self.delay_for_attempt(attempt);
OffsetDateTime::now_utc() + delay
}
pub fn delay_for_attempt(&self, attempt: u32) -> time::Duration {
let multiplier = 2u32.saturating_pow(attempt.saturating_sub(1));
let delay = self.base_delay.saturating_mul(multiplier);
let capped = delay.min(self.max_delay);
time::Duration::new(capped.as_secs() as i64, capped.subsec_nanos() as i32)
}
pub fn should_retry(&self, attempt: u32) -> bool {
attempt < self.max_attempts
}
pub fn backoff_duration(&self, attempt: u32) -> Duration {
let multiplier = 2u32.saturating_pow(attempt.saturating_sub(1));
let delay = self.base_delay.saturating_mul(multiplier);
delay.min(self.max_delay)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_attempts, 5);
assert_eq!(policy.base_delay, Duration::from_secs(1));
assert_eq!(policy.max_delay, Duration::from_secs(300));
}
#[test]
fn exponential_backoff() {
let policy = RetryPolicy {
max_attempts: 10,
base_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(300),
};
assert_eq!(
policy.delay_for_attempt(1),
time::Duration::seconds(1) );
assert_eq!(
policy.delay_for_attempt(2),
time::Duration::seconds(2) );
assert_eq!(
policy.delay_for_attempt(3),
time::Duration::seconds(4) );
assert_eq!(
policy.delay_for_attempt(4),
time::Duration::seconds(8) );
}
#[test]
fn backoff_capped_at_max() {
let policy = RetryPolicy {
max_attempts: 20,
base_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
};
assert_eq!(policy.delay_for_attempt(10), time::Duration::seconds(60));
}
#[test]
fn should_retry() {
let policy = RetryPolicy {
max_attempts: 3,
..Default::default()
};
assert!(policy.should_retry(1)); assert!(policy.should_retry(2)); assert!(!policy.should_retry(3)); }
}