use parking_lot::Mutex;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use tokio::sync::broadcast;
const RECENT_CAP: usize = 4096;
const TOP_CAP: usize = 1024;
#[derive(Debug, Clone, serde::Serialize)]
pub struct BlockEvent {
pub ts: u64,
pub qname: String,
pub action: &'static str,
pub zone: String,
}
#[derive(Clone)]
pub struct BlockEvents {
inner: Arc<Inner>,
}
struct Inner {
recent: Mutex<VecDeque<BlockEvent>>,
top: Mutex<HashMap<String, u64>>,
tx: broadcast::Sender<BlockEvent>,
}
impl Default for BlockEvents {
fn default() -> Self {
Self::new()
}
}
impl BlockEvents {
pub fn new() -> Self {
let (tx, _) = broadcast::channel(1024);
Self {
inner: Arc::new(Inner {
recent: Mutex::new(VecDeque::with_capacity(RECENT_CAP)),
top: Mutex::new(HashMap::with_capacity(TOP_CAP)),
tx,
}),
}
}
pub fn record(&self, event: BlockEvent) {
{
let mut ring = self.inner.recent.lock();
if ring.len() == RECENT_CAP {
ring.pop_front();
}
ring.push_back(event.clone());
}
{
let mut top = self.inner.top.lock();
*top.entry(event.qname.clone()).or_insert(0) += 1;
if top.len() > TOP_CAP {
if let Some(victim) = top
.iter()
.min_by_key(|(_, c)| **c)
.map(|(k, _)| k.clone())
{
top.remove(&victim);
}
}
}
let _ = self.inner.tx.send(event);
}
pub fn recent(&self, n: usize) -> Vec<BlockEvent> {
let ring = self.inner.recent.lock();
let len = ring.len();
let skip = len.saturating_sub(n);
ring.iter().skip(skip).cloned().collect()
}
pub fn subscribe(&self) -> broadcast::Receiver<BlockEvent> {
self.inner.tx.subscribe()
}
pub fn top_blocked(&self, n: usize) -> Vec<(String, u64)> {
let top = self.inner.top.lock();
let mut v: Vec<(String, u64)> = top.iter().map(|(k, c)| (k.clone(), *c)).collect();
v.sort_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
v.truncate(n);
v
}
pub fn reset(&self) {
self.inner.recent.lock().clear();
self.inner.top.lock().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(name: &str, zone: &str) -> BlockEvent {
BlockEvent {
ts: 0,
qname: name.into(),
action: "nxdomain",
zone: zone.into(),
}
}
#[test]
fn ring_caps_and_orders() {
let events = BlockEvents::new();
for i in 0..(RECENT_CAP + 10) {
events.record(ev(&format!("d{i}.test"), "z"));
}
let recent = events.recent(5);
assert_eq!(recent.len(), 5);
let expected_first = format!("d{}.test", RECENT_CAP + 5);
assert_eq!(recent[0].qname, expected_first);
}
#[test]
fn top_blocked_sorted_and_capped() {
let events = BlockEvents::new();
for _ in 0..3 {
events.record(ev("a.test", "z"));
}
events.record(ev("b.test", "z"));
events.record(ev("c.test", "z"));
events.record(ev("c.test", "z"));
let top = events.top_blocked(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].0, "a.test");
assert_eq!(top[0].1, 3);
assert_eq!(top[1].0, "c.test");
assert_eq!(top[1].1, 2);
}
#[tokio::test]
async fn broadcast_delivers() {
let events = BlockEvents::new();
let mut rx = events.subscribe();
events.record(ev("x.test", "z"));
let got = rx.recv().await.unwrap();
assert_eq!(got.qname, "x.test");
}
}