use std::collections::VecDeque;
use std::time::{Duration, Instant};
pub struct LoopGuard {
starts: VecDeque<Instant>,
window: Duration,
threshold: usize,
}
impl LoopGuard {
pub fn new(window: Duration, threshold: usize) -> Self {
Self {
starts: VecDeque::new(),
window,
threshold,
}
}
pub fn record(&mut self, now: Instant) -> bool {
self.starts.push_back(now);
while let Some(&front) = self.starts.front() {
if now.duration_since(front) > self.window {
self.starts.pop_front();
} else {
break;
}
}
self.starts.len() >= self.threshold
}
pub fn reset(&mut self) {
self.starts.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn guard() -> LoopGuard {
LoopGuard::new(Duration::from_secs(10), 5)
}
#[test]
fn does_not_trip_below_threshold() {
let mut g = guard();
let t0 = Instant::now();
for i in 0..4 {
assert!(!g.record(t0 + Duration::from_millis(i * 100)));
}
}
#[test]
fn trips_at_threshold_within_window() {
let mut g = guard();
let t0 = Instant::now();
let mut tripped = false;
for i in 0..5 {
tripped = g.record(t0 + Duration::from_millis(i * 100));
}
assert!(tripped, "expected the 5th restart in 10s to trip the guard");
}
#[test]
fn does_not_trip_when_spread_beyond_window() {
let mut g = guard();
let t0 = Instant::now();
for i in 0..10 {
assert!(!g.record(t0 + Duration::from_secs(i * 3)));
}
}
#[test]
fn reset_clears_history() {
let mut g = guard();
let t0 = Instant::now();
for i in 0..4 {
g.record(t0 + Duration::from_millis(i * 100));
}
g.reset();
assert!(!g.record(t0 + Duration::from_millis(500)));
}
#[test]
fn evicts_stale_entries_before_counting() {
let mut g = guard();
let t0 = Instant::now();
for i in 0..4 {
assert!(!g.record(t0 + Duration::from_millis(i * 100)));
}
let later = t0 + Duration::from_secs(30);
for i in 0..4 {
assert!(!g.record(later + Duration::from_millis(i * 100)));
}
}
}