diff --git a/crates/xy-supervisor/src/retry_window.rs b/crates/xy-supervisor/src/retry_window.rs new file mode 100644 index 0000000..0ac14dd --- /dev/null +++ b/crates/xy-supervisor/src/retry_window.rs @@ -0,0 +1,80 @@ +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +#[derive(Debug, Clone)] +pub struct RetryWindow { + window: Duration, + cap: u32, + events: VecDeque, +} + +impl RetryWindow { + pub fn new(window: Duration, cap: u32) -> Self { + Self { + window, + cap, + events: VecDeque::new(), + } + } + + pub fn record(&mut self, now: Instant) { + self.events.push_back(now); + self.prune(now); + } + + pub fn cap_reached(&mut self, now: Instant) -> bool { + self.prune(now); + self.events.len() as u32 >= self.cap + } + + pub fn count(&mut self, now: Instant) -> u32 { + self.prune(now); + self.events.len() as u32 + } + + fn prune(&mut self, now: Instant) { + while let Some(&front) = self.events.front() { + if now.duration_since(front) > self.window { + self.events.pop_front(); + } else { + break; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn below_cap_not_reached() { + let mut w = RetryWindow::new(Duration::from_secs(60), 3); + let t = Instant::now(); + w.record(t); + w.record(t); + assert!(!w.cap_reached(t)); + } + + #[test] + fn at_cap_reached() { + let mut w = RetryWindow::new(Duration::from_secs(60), 3); + let t = Instant::now(); + w.record(t); + w.record(t); + w.record(t); + assert!(w.cap_reached(t)); + } + + #[test] + fn old_events_pruned() { + let mut w = RetryWindow::new(Duration::from_secs(60), 3); + let t0 = Instant::now(); + w.record(t0); + w.record(t0); + w.record(t0); + let t1 = t0 + Duration::from_secs(61); + assert_eq!(w.count(t1), 0); + assert!(!w.cap_reached(t1)); + } +}