use std::time::Duration;
pub const FIRST: Duration = Duration::from_millis(25);
pub const CEILING: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Default)]
pub struct AcceptBackoff {
consecutive: u32,
}
impl AcceptBackoff {
pub const fn new() -> Self {
Self { consecutive: 0 }
}
pub fn accepted(&mut self) {
self.consecutive = 0;
}
#[must_use = "an accept failure must be backed off, never ignored"]
pub fn failed(&mut self) -> Duration {
self.consecutive = self.consecutive.saturating_add(1);
if self.consecutive.is_power_of_two() {
crate::runtime::log::errlog_sev_printf(
crate::runtime::log::ErrlogSevEnum::Major,
&format!(
"accept failed {} time(s) in a row; the server is not taking \
connections and keeps retrying",
self.consecutive
),
);
}
let shift = (self.consecutive - 1).min(32);
FIRST
.saturating_mul(1u32.checked_shl(shift).unwrap_or(u32::MAX))
.min(CEILING)
}
pub fn consecutive_failures(&self) -> u32 {
self.consecutive
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_first_failure_waits_and_does_not_spin() {
let mut b = AcceptBackoff::new();
assert_eq!(b.failed(), FIRST);
}
#[test]
fn the_delay_doubles_then_saturates_at_the_ceiling() {
let mut b = AcceptBackoff::new();
let seen: Vec<Duration> = (0..10).map(|_| b.failed()).collect();
assert_eq!(seen[0], FIRST);
assert_eq!(seen[1], FIRST * 2);
assert_eq!(seen[2], FIRST * 4);
for w in seen.windows(2) {
assert!(w[1] >= w[0], "backoff went backwards: {seen:?}");
}
assert_eq!(*seen.last().unwrap(), CEILING);
}
#[test]
fn one_success_resets_the_history() {
let mut b = AcceptBackoff::new();
for _ in 0..64 {
let _ = b.failed();
}
assert_eq!(b.consecutive_failures(), 64);
b.accepted();
assert_eq!(b.consecutive_failures(), 0);
assert_eq!(b.failed(), FIRST);
}
#[test]
fn the_loop_still_retries_long_after_the_old_give_up_budget() {
let mut b = AcceptBackoff::new();
for _ in 0..1_000 {
let d = b.failed();
assert!(
d > Duration::ZERO && d <= CEILING,
"a failure past the old budget must still yield a bounded wait, got {d:?}"
);
}
assert_eq!(b.consecutive_failures(), 1_000);
}
#[test]
fn a_saturated_counter_still_waits_the_ceiling() {
let mut b = AcceptBackoff {
consecutive: u32::MAX - 1,
};
assert_eq!(b.failed(), CEILING);
assert_eq!(b.failed(), CEILING, "saturating_add must not wrap to 0");
assert_eq!(b.consecutive_failures(), u32::MAX);
}
#[test]
fn the_console_announcement_is_first_then_powers_of_two() {
let announced: Vec<u32> = (1..=64u32).filter(|n| n.is_power_of_two()).collect();
assert_eq!(announced, vec![1, 2, 4, 8, 16, 32, 64]);
}
}