use std::collections::VecDeque;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::engine::BurstDetectorCfg;
#[derive(Debug)]
pub struct BurstDetector {
cfg: BurstDetectorCfg,
events: Mutex<VecDeque<Instant>>,
}
impl BurstDetector {
pub fn new(cfg: BurstDetectorCfg) -> Self {
Self { cfg, events: Mutex::new(VecDeque::new()) }
}
pub fn observe(&self) -> bool {
if !self.cfg.enabled { return false; }
let now = Instant::now();
let window = Duration::from_secs(self.cfg.window_seconds as u64);
let mut q = self.events.lock().expect("burst mutex poisoned");
q.push_back(now);
while let Some(&front) = q.front() {
if now.duration_since(front) > window {
q.pop_front();
} else {
break;
}
}
q.len() as u32 >= self.cfg.threshold
}
pub fn in_burst(&self) -> bool {
if !self.cfg.enabled { return false; }
let now = Instant::now();
let window = Duration::from_secs(self.cfg.window_seconds as u64);
let q = self.events.lock().expect("burst mutex poisoned");
q.iter()
.rev()
.take_while(|&&t| now.duration_since(t) <= window)
.count() as u32
>= self.cfg.threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(threshold: u32, window_seconds: u32) -> BurstDetectorCfg {
BurstDetectorCfg {
enabled: true,
window_seconds,
threshold,
}
}
#[test]
fn below_threshold_no_burst() {
let b = BurstDetector::new(cfg(5, 60));
for _ in 0..4 {
assert!(!b.observe());
}
}
#[test]
fn at_threshold_triggers_burst() {
let b = BurstDetector::new(cfg(3, 60));
assert!(!b.observe());
assert!(!b.observe());
assert!(b.observe()); assert!(b.in_burst());
}
#[test]
fn window_expiry_resets_burst() {
let b = BurstDetector::new(cfg(2, 0)); b.observe();
std::thread::sleep(Duration::from_millis(5));
assert!(!b.observe());
}
#[test]
fn disabled_never_fires() {
let mut c = cfg(1, 60);
c.enabled = false;
let b = BurstDetector::new(c);
for _ in 0..10 {
assert!(!b.observe());
}
}
}