use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub enum Backoff {
Fixed(Duration),
Exponential {
base: Duration,
factor: u32,
max: Option<Duration>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct RetryPolicy {
pub max_retries: usize,
pub backoff: Backoff,
}
impl RetryPolicy {
pub fn fixed(max_retries: usize, delay: Duration) -> Self {
RetryPolicy {
max_retries,
backoff: Backoff::Fixed(delay),
}
}
pub fn exponential(max_retries: usize, base: Duration, factor: u32) -> Self {
RetryPolicy {
max_retries,
backoff: Backoff::Exponential {
base,
factor,
max: None,
},
}
}
pub fn with_max_delay(mut self, max: Duration) -> Self {
if let Backoff::Exponential { max: ref mut m, .. } = self.backoff {
*m = Some(max);
}
self
}
pub(crate) fn delay(&self, retry_index: u32) -> Duration {
match &self.backoff {
Backoff::Fixed(d) => *d,
Backoff::Exponential { base, factor, max } => {
let mult = (*factor as u128).saturating_pow(retry_index);
let millis = base.as_millis().saturating_mul(mult);
let delay = Duration::from_millis(millis.min(u64::MAX as u128) as u64);
match max {
Some(cap) => delay.min(*cap),
None => delay,
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn fixed_delay_is_constant() {
let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
assert_eq!(policy.max_retries, 3);
assert_eq!(policy.delay(0), Duration::from_millis(100));
assert_eq!(policy.delay(1), Duration::from_millis(100));
assert_eq!(policy.delay(5), Duration::from_millis(100));
}
#[test]
fn exponential_delay_grows() {
let policy = RetryPolicy::exponential(5, Duration::from_millis(100), 2);
assert_eq!(policy.delay(0), Duration::from_millis(100));
assert_eq!(policy.delay(1), Duration::from_millis(200));
assert_eq!(policy.delay(2), Duration::from_millis(400));
assert_eq!(policy.delay(3), Duration::from_millis(800));
}
#[test]
fn exponential_delay_is_capped() {
let policy = RetryPolicy::exponential(10, Duration::from_millis(100), 2)
.with_max_delay(Duration::from_millis(500));
assert_eq!(policy.delay(0), Duration::from_millis(100));
assert_eq!(policy.delay(2), Duration::from_millis(400));
assert_eq!(policy.delay(3), Duration::from_millis(500));
assert_eq!(policy.delay(10), Duration::from_millis(500));
}
#[test]
fn exponential_delay_does_not_overflow() {
let policy = RetryPolicy::exponential(usize::MAX, Duration::from_secs(1), 10)
.with_max_delay(Duration::from_secs(30));
assert_eq!(policy.delay(1000), Duration::from_secs(30));
}
#[test]
fn with_max_delay_is_noop_for_fixed() {
let policy = RetryPolicy::fixed(2, Duration::from_millis(50))
.with_max_delay(Duration::from_millis(10));
assert_eq!(policy.delay(0), Duration::from_millis(50));
}
}