use std::collections::HashSet;
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::broadcast;
use crate::event::{Event, EventKind};
use crate::session::SESSION_WATCHER;
#[derive(Debug)]
pub struct EventStream {
rx: broadcast::Receiver<Event>,
names: HashSet<String>,
}
impl EventStream {
pub(crate) fn new(rx: broadcast::Receiver<Event>, names: HashSet<String>) -> Self {
Self { rx, names }
}
pub async fn next(&mut self) -> Option<Event> {
loop {
match self.rx.recv().await {
Ok(ev) if ev.kind.is_session_level() || self.names.contains(&*ev.watcher) => {
return Some(ev)
}
Ok(_) => continue,
Err(broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!(skipped, "EventStream lagged; events were dropped");
return Some(Event {
watcher: Arc::from(SESSION_WATCHER),
timestamp: SystemTime::now(),
kind: EventKind::Lagged { skipped },
});
}
Err(broadcast::error::RecvError::Closed) => return None,
}
}
}
pub fn merge(mut self, other: EventStream) -> EventStream {
self.names.extend(other.names);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::EventKind;
use std::sync::Arc;
use std::time::SystemTime;
use visual_cortex_vision::DetectorOutput;
fn ev(name: &str) -> Event {
Event {
watcher: Arc::from(name),
timestamp: SystemTime::now(),
kind: EventKind::Matched {
output: DetectorOutput::Bool(true),
},
}
}
#[tokio::test]
async fn filters_to_subscribed_watcher_names() {
let (tx, rx) = broadcast::channel(16);
let mut stream = EventStream::new(rx, HashSet::from(["a".to_string()]));
tx.send(ev("b")).unwrap();
tx.send(ev("a")).unwrap();
let got = stream.next().await.unwrap();
assert_eq!(&*got.watcher, "a");
}
#[tokio::test]
async fn merge_unions_names() {
let (tx, rx) = broadcast::channel(16);
let a = EventStream::new(rx, HashSet::from(["a".to_string()]));
let b = EventStream::new(tx.subscribe(), HashSet::from(["b".to_string()]));
let mut merged = a.merge(b);
tx.send(ev("b")).unwrap();
tx.send(ev("a")).unwrap();
assert_eq!(&*merged.next().await.unwrap().watcher, "b");
assert_eq!(&*merged.next().await.unwrap().watcher, "a");
}
#[tokio::test]
async fn session_level_events_bypass_name_filter() {
let (tx, rx) = broadcast::channel(16);
let mut stream = EventStream::new(rx, HashSet::from(["a".to_string()]));
tx.send(Event {
watcher: Arc::from("$session"),
timestamp: SystemTime::now(),
kind: EventKind::TargetLost,
})
.unwrap();
let got = stream.next().await.unwrap();
assert!(matches!(got.kind, EventKind::TargetLost));
}
#[tokio::test]
async fn returns_none_when_sender_dropped() {
let (tx, rx) = broadcast::channel(16);
let mut stream = EventStream::new(rx, HashSet::from(["a".to_string()]));
drop(tx);
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn lag_surfaces_as_event_then_stream_continues() {
let (tx, rx) = broadcast::channel(2);
let mut stream = EventStream::new(rx, HashSet::from(["a".to_string()]));
for _ in 0..5 {
tx.send(ev("a")).unwrap();
}
let first = stream.next().await.unwrap();
assert!(
matches!(first.kind, EventKind::Lagged { skipped: 3 }),
"expected Lagged {{ skipped: 3 }}, got {:?}",
first.kind
);
assert_eq!(&*stream.next().await.unwrap().watcher, "a");
assert_eq!(&*stream.next().await.unwrap().watcher, "a");
}
#[tokio::test]
async fn watcher_stopped_reaches_its_subscriber() {
let (tx, rx) = broadcast::channel(16);
let mut stream = EventStream::new(rx, HashSet::from(["boom".to_string()]));
tx.send(Event {
watcher: Arc::from("boom"),
timestamp: SystemTime::now(),
kind: EventKind::WatcherStopped,
})
.unwrap();
let got = stream.next().await.unwrap();
assert!(matches!(got.kind, EventKind::WatcherStopped));
assert_eq!(&*got.watcher, "boom");
}
#[tokio::test]
async fn lagged_is_not_swallowed_by_name_filter() {
let (tx, rx) = broadcast::channel(2);
let mut stream = EventStream::new(rx, HashSet::from(["b".to_string()]));
for _ in 0..5 {
tx.send(ev("a")).unwrap();
}
let first = stream.next().await.unwrap();
assert!(matches!(first.kind, EventKind::Lagged { skipped: 3 }));
}
}