use std::time::Duration;
pub const MIN_BACKOFF: Duration = Duration::from_millis(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Backoff {
initial: Duration,
max: Duration,
next: Duration,
}
impl Backoff {
pub fn new(initial: Duration, max: Duration) -> Self {
let max = max.max(MIN_BACKOFF);
let initial = initial.clamp(MIN_BACKOFF, max);
Self {
initial,
max,
next: initial,
}
}
pub fn take(&mut self) -> Duration {
let delay = self.next;
self.next = self.next.saturating_mul(2).min(self.max);
delay
}
pub fn take_at_least(&mut self, floor: Duration) -> Duration {
self.take().max(floor.min(self.max))
}
pub fn after_connection_ended(&mut self, delivered: bool) {
if delivered {
self.next = self.initial;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod backoff_schedule {
use super::*;
fn millis(ms: u64) -> Duration {
Duration::from_millis(ms)
}
fn take_n(backoff: &mut Backoff, n: usize) -> Vec<Duration> {
(0..n).map(|_| backoff.take()).collect()
}
#[test]
fn the_delay_doubles_up_to_the_ceiling_and_then_holds() {
let mut backoff = Backoff::new(millis(10), millis(80));
assert_eq!(
take_n(&mut backoff, 6),
vec![
millis(10),
millis(20),
millis(40),
millis(80),
millis(80),
millis(80)
],
"a schedule that stops escalating hammers a host that is \
already unwell"
);
}
#[test]
fn the_ceiling_is_a_clamp_rather_than_a_step() {
let mut backoff = Backoff::new(millis(30), millis(50));
assert_eq!(
take_n(&mut backoff, 3),
vec![millis(30), millis(50), millis(50)]
);
}
#[test]
fn a_reset_returns_to_the_initial_delay_not_to_zero() {
let mut backoff = Backoff::new(millis(10), millis(80));
take_n(&mut backoff, 3);
backoff.after_connection_ended(true);
assert_eq!(
take_n(&mut backoff, 2),
vec![millis(10), millis(20)],
"a reset restarts the schedule; it does not remove the wait"
);
}
#[test]
fn only_a_connection_that_delivered_something_resets_the_schedule() {
let mut escalating = Backoff::new(millis(10), millis(80));
let mut recovering = Backoff::new(millis(10), millis(80));
let (mut escalated, mut recovered) = (Vec::new(), Vec::new());
for _ in 0..4 {
escalating.after_connection_ended(false);
escalated.push(escalating.take());
recovering.after_connection_ended(true);
recovered.push(recovering.take());
}
assert_eq!(
escalated,
vec![millis(10), millis(20), millis(40), millis(80)]
);
assert_eq!(
recovered,
vec![millis(10), millis(10), millis(10), millis(10)]
);
}
#[test]
fn a_ceiling_below_the_initial_delay_is_still_honoured() {
let mut backoff = Backoff::new(Duration::from_secs(60), millis(1));
assert_eq!(take_n(&mut backoff, 2), vec![millis(1), millis(1)]);
backoff.after_connection_ended(true);
assert_eq!(backoff.take(), millis(1), "a reset must respect it too");
}
#[test]
fn a_zero_initial_delay_still_grows() {
let mut backoff = Backoff::new(Duration::ZERO, millis(80));
assert_eq!(
take_n(&mut backoff, 3),
vec![millis(1), millis(2), millis(4)]
);
}
#[test]
fn a_floor_extends_the_delay_and_never_shortens_it_drift_r11() {
let mut backoff = Backoff::new(Duration::from_secs(1), Duration::from_secs(60));
assert_eq!(
backoff.take_at_least(Duration::from_secs(30)),
Duration::from_secs(30)
);
assert_eq!(backoff.take_at_least(millis(500)), Duration::from_secs(2));
assert_eq!(
backoff.take_at_least(Duration::ZERO),
Duration::from_secs(4)
);
}
#[test]
fn a_floor_past_the_ceiling_waits_the_ceiling_drift_r11() {
let mut backoff = Backoff::new(Duration::from_secs(1), Duration::from_secs(60));
assert_eq!(
backoff.take_at_least(Duration::from_secs(600)),
Duration::from_secs(60)
);
assert_eq!(
backoff.take_at_least(Duration::MAX),
Duration::from_secs(60)
);
}
#[test]
fn a_floor_does_not_move_the_schedule() {
let mut floored = Backoff::new(millis(10), millis(80));
let mut plain = floored;
floored.take_at_least(millis(70));
plain.take();
assert_eq!(take_n(&mut floored, 3), take_n(&mut plain, 3));
}
#[test]
fn a_huge_delay_saturates_rather_than_panicking() {
let mut backoff = Backoff::new(Duration::MAX, Duration::MAX);
assert_eq!(take_n(&mut backoff, 2), vec![Duration::MAX, Duration::MAX]);
}
}
}