use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BoundedBackoff {
initial: Duration,
max: Duration,
stable_run_threshold: Duration,
current: Duration,
}
impl BoundedBackoff {
pub fn new(initial: Duration, max: Duration, stable_run_threshold: Duration) -> Self {
let initial = if initial.is_zero() {
Duration::from_millis(1)
} else {
initial
};
Self {
initial,
max: max.max(initial),
stable_run_threshold,
current: initial,
}
}
pub fn current(&self) -> Duration {
self.current
}
pub fn record_failure(&mut self, ran_for: Duration) -> Duration {
self.current = if ran_for >= self.stable_run_threshold {
self.initial
} else {
self.current.saturating_mul(2).min(self.max)
};
self.current
}
pub fn reset(&mut self) {
self.current = self.initial;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn backoff() -> BoundedBackoff {
BoundedBackoff::new(
Duration::from_secs(1),
Duration::from_secs(30),
Duration::from_secs(60),
)
}
#[test]
fn doubles_on_repeated_quick_failures() {
let mut backoff = backoff();
assert_eq!(
backoff.record_failure(Duration::from_secs(1)),
Duration::from_secs(2)
);
assert_eq!(
backoff.record_failure(Duration::from_secs(1)),
Duration::from_secs(4)
);
assert_eq!(
backoff.record_failure(Duration::from_secs(1)),
Duration::from_secs(8)
);
}
#[test]
fn caps_at_max_and_never_exceeds_it() {
let mut backoff = backoff();
for _ in 0..10 {
backoff.record_failure(Duration::from_millis(1));
}
assert_eq!(backoff.current(), Duration::from_secs(30));
}
#[test]
fn resets_to_initial_after_a_stable_run() {
let mut backoff = backoff();
backoff.record_failure(Duration::from_secs(1));
backoff.record_failure(Duration::from_secs(1));
assert_eq!(backoff.current(), Duration::from_secs(4));
assert_eq!(
backoff.record_failure(Duration::from_secs(90)),
Duration::from_secs(1)
);
}
#[test]
fn zero_initial_delay_does_not_stall_growth_forever() {
let mut backoff = BoundedBackoff::new(
Duration::ZERO,
Duration::from_secs(10),
Duration::from_secs(60),
);
assert_eq!(backoff.current(), Duration::from_millis(1));
backoff.record_failure(Duration::from_millis(1));
assert!(backoff.current() > Duration::from_millis(1));
}
#[test]
fn explicit_reset_returns_to_initial() {
let mut backoff = backoff();
backoff.record_failure(Duration::from_millis(1));
backoff.record_failure(Duration::from_millis(1));
backoff.reset();
assert_eq!(backoff.current(), Duration::from_secs(1));
}
}