use somatize_core::event::Event;
use somatize_core::tracking::EventSink;
use std::sync::{Arc, RwLock};
use tokio::sync::broadcast;
pub struct EventBus {
sender: broadcast::Sender<Event>,
sinks: RwLock<Vec<Arc<dyn EventSink>>>,
}
impl EventBus {
pub fn new(capacity: usize) -> Self {
let (sender, _) = broadcast::channel(capacity);
Self {
sender,
sinks: RwLock::new(Vec::new()),
}
}
pub fn add_sink(&self, sink: Arc<dyn EventSink>) {
match self.sinks.write() {
Ok(mut sinks) => sinks.push(sink),
Err(poisoned) => poisoned.into_inner().push(sink),
}
}
pub fn remove_sink(&self, sink: &Arc<dyn EventSink>) {
sink.flush();
let mut sinks = match self.sinks.write() {
Ok(s) => s,
Err(poisoned) => poisoned.into_inner(),
};
sinks.retain(|s| !Arc::ptr_eq(s, sink));
}
pub fn emit(&self, event: Event) -> usize {
for sink in self.snapshot_sinks() {
sink.record(&event);
}
self.sender.send(event).unwrap_or(0)
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.sender.subscribe()
}
pub fn subscriber_count(&self) -> usize {
self.sender.receiver_count()
}
pub fn flush_sinks(&self) {
for sink in self.snapshot_sinks() {
sink.flush();
}
}
fn snapshot_sinks(&self) -> Vec<Arc<dyn EventSink>> {
match self.sinks.read() {
Ok(s) => s.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new(256)
}
}
#[cfg(test)]
mod tests {
use super::*;
use somatize_core::event::PlanSummary;
use std::time::Duration;
#[tokio::test]
async fn emit_without_subscribers_succeeds() {
let bus = EventBus::new(16);
let count = bus.emit(Event::RunStarted {
run_id: "r1".into(),
plan_summary: PlanSummary {
total_nodes: 1,
cached_nodes: 0,
parallel_branches: 0,
},
});
assert_eq!(count, 0);
}
#[tokio::test]
async fn subscriber_receives_events() {
let bus = EventBus::new(16);
let mut rx = bus.subscribe();
bus.emit(Event::RunStarted {
run_id: "r1".into(),
plan_summary: PlanSummary {
total_nodes: 2,
cached_nodes: 0,
parallel_branches: 0,
},
});
bus.emit(Event::RunCompleted {
run_id: "r1".into(),
duration: Duration::from_millis(100),
});
let e1 = rx.recv().await.unwrap();
assert!(matches!(e1, Event::RunStarted { .. }));
let e2 = rx.recv().await.unwrap();
assert!(matches!(e2, Event::RunCompleted { .. }));
}
#[tokio::test]
async fn multiple_subscribers() {
let bus = EventBus::new(16);
let mut rx1 = bus.subscribe();
let mut rx2 = bus.subscribe();
assert_eq!(bus.subscriber_count(), 2);
bus.emit(Event::RunCompleted {
run_id: "r1".into(),
duration: Duration::from_secs(1),
});
let e1 = rx1.recv().await.unwrap();
let e2 = rx2.recv().await.unwrap();
assert!(matches!(e1, Event::RunCompleted { .. }));
assert!(matches!(e2, Event::RunCompleted { .. }));
}
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
struct CountingSink {
records: AtomicUsize,
flushes: AtomicUsize,
}
impl somatize_core::tracking::EventSink for CountingSink {
fn record(&self, _event: &Event) {
self.records.fetch_add(1, Ordering::SeqCst);
}
fn flush(&self) {
self.flushes.fetch_add(1, Ordering::SeqCst);
}
}
fn run_completed(id: &str) -> Event {
Event::RunCompleted {
run_id: id.into(),
duration: Duration::from_millis(1),
}
}
struct ReentrantSink {
bus: std::sync::Weak<EventBus>,
added: AtomicUsize,
}
impl somatize_core::tracking::EventSink for ReentrantSink {
fn record(&self, _event: &Event) {
if self.added.fetch_add(1, Ordering::SeqCst) == 0
&& let Some(bus) = self.bus.upgrade()
{
bus.add_sink(Arc::new(CountingSink::default()));
}
}
fn flush(&self) {}
}
#[test]
fn a_sink_may_touch_the_bus_from_inside_record() {
let bus = Arc::new(EventBus::new(16));
bus.add_sink(Arc::new(ReentrantSink {
bus: Arc::downgrade(&bus),
added: AtomicUsize::new(0),
}));
bus.emit(run_completed("r1"));
bus.emit(run_completed("r2"));
bus.flush_sinks();
}
#[test]
fn sinks_observe_events_synchronously_before_emit_returns() {
let bus = EventBus::new(16);
let sink = Arc::new(CountingSink::default());
bus.add_sink(sink.clone());
bus.emit(run_completed("r1"));
assert_eq!(sink.records.load(Ordering::SeqCst), 1);
}
#[test]
fn remove_sink_flushes_detaches_and_respects_identity() {
let bus = EventBus::new(16);
let sink = Arc::new(CountingSink::default());
let as_dyn: Arc<dyn somatize_core::tracking::EventSink> = sink.clone();
bus.add_sink(as_dyn.clone());
bus.emit(run_completed("r1"));
assert_eq!(sink.flushes.load(Ordering::SeqCst), 0);
let other: Arc<dyn somatize_core::tracking::EventSink> = Arc::new(CountingSink::default());
bus.remove_sink(&other);
bus.emit(run_completed("r2"));
assert_eq!(sink.records.load(Ordering::SeqCst), 2, "still attached");
bus.remove_sink(&as_dyn);
assert_eq!(sink.flushes.load(Ordering::SeqCst), 1, "flushed on removal");
bus.emit(run_completed("r3"));
assert_eq!(
sink.records.load(Ordering::SeqCst),
2,
"no events after removal"
);
bus.remove_sink(&as_dyn);
}
#[test]
fn remove_sink_drops_every_clone_of_a_doubly_registered_arc() {
let bus = EventBus::new(16);
let sink = Arc::new(CountingSink::default());
let as_dyn: Arc<dyn somatize_core::tracking::EventSink> = sink.clone();
bus.add_sink(as_dyn.clone());
bus.add_sink(as_dyn.clone());
bus.emit(run_completed("r1"));
assert_eq!(sink.records.load(Ordering::SeqCst), 2);
bus.remove_sink(&as_dyn);
bus.emit(run_completed("r2"));
assert_eq!(sink.records.load(Ordering::SeqCst), 2);
}
#[test]
fn flush_sinks_flushes_all_registered_sinks() {
let bus = EventBus::new(16);
let a = Arc::new(CountingSink::default());
let b = Arc::new(CountingSink::default());
bus.add_sink(a.clone());
bus.add_sink(b.clone());
bus.flush_sinks();
assert_eq!(a.flushes.load(Ordering::SeqCst), 1);
assert_eq!(b.flushes.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn sinks_stay_lossless_while_subscribers_lag() {
let bus = EventBus::new(4); let sink = Arc::new(CountingSink::default());
bus.add_sink(sink.clone());
let mut rx = bus.subscribe();
for i in 0..100 {
bus.emit(run_completed(&format!("r{i}")));
}
assert_eq!(sink.records.load(Ordering::SeqCst), 100, "sink is lossless");
match rx.recv().await {
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
assert!(n > 0, "subscriber lost {n} events");
}
other => panic!("expected Lagged, got {other:?}"),
}
}
#[tokio::test]
async fn subscriber_after_emit_misses_earlier_events() {
let bus = EventBus::new(16);
bus.emit(Event::RunCompleted {
run_id: "r1".into(),
duration: Duration::from_secs(1),
});
let mut rx = bus.subscribe();
bus.emit(Event::RunCompleted {
run_id: "r2".into(),
duration: Duration::from_secs(2),
});
let event = rx.recv().await.unwrap();
if let Event::RunCompleted { run_id, .. } = event {
assert_eq!(run_id, "r2"); } else {
panic!("wrong event type");
}
}
}