use std::time::{Duration, Instant};
use tokio::net::{UnixListener, UnixStream};
const INITIAL_DELAY: Duration = Duration::from_millis(5);
const MAX_DELAY: Duration = Duration::from_secs(1);
const LOG_INTERVAL: Duration = Duration::from_secs(30);
#[async_trait::async_trait]
pub(crate) trait ConnectionSource: Send + Sync {
async fn accept(&self) -> std::io::Result<UnixStream>;
}
#[async_trait::async_trait]
impl ConnectionSource for UnixListener {
async fn accept(&self) -> std::io::Result<UnixStream> {
Self::accept(self).await.map(|(stream, _addr)| stream)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FailureStep {
pub delay: Duration,
pub log: bool,
pub outage: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Recovery {
pub failures: u32,
pub duration: Duration,
pub outage: Duration,
pub announce: bool,
}
#[derive(Debug, Default)]
pub(crate) struct AcceptBackoff {
burst: Option<Burst>,
last_logged: Option<Instant>,
}
#[derive(Debug)]
struct Burst {
started: Instant,
failures: u32,
next_delay: Duration,
announced: bool,
reported_to: Instant,
}
impl AcceptBackoff {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn on_failure(&mut self, now: Instant) -> FailureStep {
let burst = self.burst.get_or_insert(Burst {
started: now,
failures: 0,
next_delay: INITIAL_DELAY,
announced: false,
reported_to: now,
});
burst.failures += 1;
let delay = burst.next_delay;
burst.next_delay = (delay * 2).min(MAX_DELAY);
let log = self
.last_logged
.is_none_or(|at| now.saturating_duration_since(at) >= LOG_INTERVAL);
if log {
self.last_logged = Some(now);
burst.announced = true;
}
let outage = now.saturating_duration_since(burst.reported_to);
burst.reported_to = now;
FailureStep { delay, log, outage }
}
pub(crate) fn on_success(&mut self, now: Instant) -> Option<Recovery> {
let burst = self.burst.take()?;
Some(Recovery {
failures: burst.failures,
duration: now.saturating_duration_since(burst.started),
outage: now.saturating_duration_since(burst.reported_to),
announce: burst.announced,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[tokio::test]
async fn a_unix_listener_yields_the_connection_a_client_makes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("s.sock");
let listener = UnixListener::bind(&path).unwrap();
let _client = UnixStream::connect(&path).await.unwrap();
let accepted =
tokio::time::timeout(Duration::from_secs(2), ConnectionSource::accept(&listener))
.await
.expect("accept should not hang or recurse");
assert!(accepted.is_ok());
}
fn at(base: Instant, millis: u64) -> Instant {
base + Duration::from_millis(millis)
}
#[test]
fn delay_doubles_from_the_initial_value_up_to_the_cap() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
let delays: Vec<Duration> = (0..12)
.map(|i| backoff.on_failure(at(base, i)).delay)
.collect();
assert_eq!(delays[0], INITIAL_DELAY);
assert_eq!(delays[1], INITIAL_DELAY * 2);
assert_eq!(delays[2], INITIAL_DELAY * 4);
assert_eq!(delays[8], MAX_DELAY);
assert_eq!(delays[11], MAX_DELAY);
assert!(delays.windows(2).all(|pair| pair[0] <= pair[1]));
}
#[test]
fn a_success_ends_the_burst_and_the_next_failure_starts_from_the_initial_delay() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
for i in 0..5 {
backoff.on_failure(at(base, i));
}
assert!(backoff.on_success(at(base, 100)).is_some());
let step = backoff.on_failure(at(base, 200));
assert_eq!(step.delay, INITIAL_DELAY);
}
#[test]
fn the_log_throttle_spans_bursts() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
assert!(backoff.on_failure(at(base, 0)).log);
assert!(backoff.on_success(at(base, 10)).unwrap().announce);
assert!(!backoff.on_failure(at(base, 1_000)).log);
assert!(!backoff.on_success(at(base, 1_010)).unwrap().announce);
assert!(backoff.on_failure(at(base, 30_000)).log);
assert!(backoff.on_success(at(base, 30_010)).unwrap().announce);
}
#[test]
fn a_success_with_no_burst_in_progress_reports_nothing() {
assert_eq!(AcceptBackoff::new().on_success(Instant::now()), None);
}
#[test]
fn only_the_first_failure_in_a_log_interval_is_logged() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
assert!(backoff.on_failure(base).log);
assert!(!backoff.on_failure(at(base, 1_000)).log);
assert!(!backoff.on_failure(at(base, 29_999)).log);
assert!(backoff.on_failure(at(base, 30_000)).log);
assert!(!backoff.on_failure(at(base, 30_001)).log);
}
#[test]
fn outage_is_reported_incrementally_and_sums_to_the_whole_burst() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
let first = backoff.on_failure(at(base, 1_000));
assert_eq!(first.outage, Duration::ZERO, "nothing has elapsed yet");
let second = backoff.on_failure(at(base, 1_005));
let third = backoff.on_failure(at(base, 1_015));
let recovery = backoff.on_success(at(base, 1_045)).unwrap();
assert_eq!(second.outage, Duration::from_millis(5));
assert_eq!(third.outage, Duration::from_millis(10));
assert_eq!(recovery.outage, Duration::from_millis(30));
assert_eq!(
first.outage + second.outage + third.outage + recovery.outage,
Duration::from_millis(45),
"the credits cover the burst exactly once"
);
}
#[test]
fn recovery_reports_the_failure_count_and_burst_duration() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
for i in 0..4 {
backoff.on_failure(at(base, i * 10));
}
let recovery = backoff.on_success(at(base, 100)).unwrap();
assert_eq!(recovery.failures, 4);
assert_eq!(recovery.duration, Duration::from_millis(100));
}
#[test]
fn a_clock_that_runs_backwards_never_panics_or_goes_negative() {
let base = Instant::now();
let mut backoff = AcceptBackoff::new();
backoff.on_failure(at(base, 100));
let step = backoff.on_failure(base);
assert_eq!(step.outage, Duration::ZERO);
assert_eq!(backoff.on_success(base).unwrap().outage, Duration::ZERO);
}
}