use core::time::Duration;
use super::Failure;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RetryPolicy {
pub max_retries: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub multiplier: u32,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(30),
multiplier: 2,
}
}
}
impl RetryPolicy {
#[must_use]
pub fn none() -> Self {
Self {
max_retries: 0,
..Self::default()
}
}
#[must_use]
pub const fn allows(&self, retries: u32, failure: &Failure) -> bool {
retries < self.max_retries && failure.is_retryable()
}
#[must_use]
pub fn delay(&self, retry: u32, failure: &Failure) -> Duration {
failure
.retry_after
.unwrap_or_else(|| {
self.initial_delay
.saturating_mul(self.multiplier.saturating_pow(retry))
})
.min(self.max_delay)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_grows_up_to_the_cap() {
let policy = RetryPolicy::default();
let failure = Failure::network();
assert_eq!(policy.delay(0, &failure), Duration::from_millis(500));
assert_eq!(policy.delay(2, &failure), Duration::from_secs(2));
assert_eq!(policy.delay(u32::MAX, &failure), policy.max_delay);
}
#[test]
fn a_requested_delay_replaces_the_backoff_under_the_cap() {
let policy = RetryPolicy::default();
let soon = Failure::transient().with_retry_after(Some(Duration::from_secs(4)));
assert_eq!(policy.delay(0, &soon), Duration::from_secs(4));
let late = Failure::transient().with_retry_after(Some(Duration::from_secs(600)));
assert_eq!(policy.delay(0, &late), policy.max_delay);
}
#[test]
fn only_retryable_failures_within_the_limit_are_allowed() {
let policy = RetryPolicy::default();
assert!(policy.allows(0, &Failure::network()));
assert!(policy.allows(2, &Failure::transient()));
assert!(!policy.allows(3, &Failure::network()));
assert!(!policy.allows(0, &Failure::permanent()));
assert!(!policy.allows(0, &Failure::quota()));
assert!(!RetryPolicy::none().allows(0, &Failure::network()));
}
}