use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResiliencePolicy {
pub min_interval: Duration,
pub attempt_timeout: Duration,
pub max_retries: u32,
pub backoff_base: Duration,
pub backoff_cap: Duration,
pub deadline: Duration,
pub breaker_threshold: u32,
pub breaker_cooldown: Duration,
}
impl Default for ResiliencePolicy {
fn default() -> Self {
ResiliencePolicy {
min_interval: Duration::from_secs(1),
attempt_timeout: Duration::from_secs(20),
max_retries: 4,
backoff_base: Duration::from_millis(500),
backoff_cap: Duration::from_secs(30),
deadline: Duration::from_secs(90),
breaker_threshold: 5,
breaker_cooldown: Duration::from_secs(300),
}
}
}
impl ResiliencePolicy {
pub fn backoff_ceiling(&self, retry: u32) -> Duration {
let factor = 1u32.checked_shl(retry).unwrap_or(u32::MAX);
self.backoff_base
.checked_mul(factor)
.unwrap_or(Duration::MAX)
.min(self.backoff_cap)
}
pub fn backoff(&self, retry: u32, jitter: f64) -> Duration {
let jitter = if jitter.is_nan() {
0.0
} else {
jitter.clamp(0.0, 1.0)
};
self.backoff_ceiling(retry).mul_f64(jitter)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy() -> ResiliencePolicy {
ResiliencePolicy {
backoff_base: Duration::from_millis(100),
backoff_cap: Duration::from_secs(5),
..ResiliencePolicy::default()
}
}
#[test]
fn ceiling_doubles_then_caps() {
let p = policy();
let ceilings: Vec<_> = (0..8).map(|r| p.backoff_ceiling(r).as_millis()).collect();
assert_eq!(ceilings, [100, 200, 400, 800, 1600, 3200, 5000, 5000]);
}
#[test]
fn huge_retry_counts_saturate_at_the_cap() {
let p = policy();
assert_eq!(p.backoff_ceiling(31), p.backoff_cap);
assert_eq!(p.backoff_ceiling(u32::MAX), p.backoff_cap);
}
#[test]
fn jitter_scales_within_the_ceiling() {
let p = policy();
assert_eq!(p.backoff(3, 0.0), Duration::ZERO);
assert_eq!(p.backoff(3, 0.5), Duration::from_millis(400));
assert_eq!(p.backoff(3, 7.0), p.backoff_ceiling(3));
assert_eq!(p.backoff(3, f64::NAN), Duration::ZERO);
}
}