somatize_runtime/
event_bus.rs1use somatize_core::event::Event;
7use somatize_core::tracking::EventSink;
8use std::sync::{Arc, RwLock};
9use tokio::sync::broadcast;
10
11pub struct EventBus {
23 sender: broadcast::Sender<Event>,
24 sinks: RwLock<Vec<Arc<dyn EventSink>>>,
25}
26
27impl EventBus {
28 pub fn new(capacity: usize) -> Self {
30 let (sender, _) = broadcast::channel(capacity);
31 Self {
32 sender,
33 sinks: RwLock::new(Vec::new()),
34 }
35 }
36
37 pub fn add_sink(&self, sink: Arc<dyn EventSink>) {
39 match self.sinks.write() {
40 Ok(mut sinks) => sinks.push(sink),
41 Err(poisoned) => poisoned.into_inner().push(sink),
42 }
43 }
44
45 pub fn remove_sink(&self, sink: &Arc<dyn EventSink>) {
48 sink.flush();
49 let mut sinks = match self.sinks.write() {
50 Ok(s) => s,
51 Err(poisoned) => poisoned.into_inner(),
52 };
53 sinks.retain(|s| !Arc::ptr_eq(s, sink));
54 }
55
56 pub fn emit(&self, event: Event) -> usize {
60 for sink in self.snapshot_sinks() {
61 sink.record(&event);
62 }
63 self.sender.send(event).unwrap_or(0)
64 }
65
66 pub fn subscribe(&self) -> broadcast::Receiver<Event> {
68 self.sender.subscribe()
69 }
70
71 pub fn subscriber_count(&self) -> usize {
73 self.sender.receiver_count()
74 }
75
76 pub fn flush_sinks(&self) {
78 for sink in self.snapshot_sinks() {
79 sink.flush();
80 }
81 }
82
83 fn snapshot_sinks(&self) -> Vec<Arc<dyn EventSink>> {
90 match self.sinks.read() {
91 Ok(s) => s.clone(),
92 Err(poisoned) => poisoned.into_inner().clone(),
93 }
94 }
95}
96
97impl Default for EventBus {
98 fn default() -> Self {
99 Self::new(256)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use somatize_core::event::PlanSummary;
107 use std::time::Duration;
108
109 #[tokio::test]
110 async fn emit_without_subscribers_succeeds() {
111 let bus = EventBus::new(16);
112 let count = bus.emit(Event::RunStarted {
113 run_id: "r1".into(),
114 plan_summary: PlanSummary {
115 total_nodes: 1,
116 cached_nodes: 0,
117 parallel_branches: 0,
118 },
119 });
120 assert_eq!(count, 0);
121 }
122
123 #[tokio::test]
124 async fn subscriber_receives_events() {
125 let bus = EventBus::new(16);
126 let mut rx = bus.subscribe();
127
128 bus.emit(Event::RunStarted {
129 run_id: "r1".into(),
130 plan_summary: PlanSummary {
131 total_nodes: 2,
132 cached_nodes: 0,
133 parallel_branches: 0,
134 },
135 });
136 bus.emit(Event::RunCompleted {
137 run_id: "r1".into(),
138 duration: Duration::from_millis(100),
139 });
140
141 let e1 = rx.recv().await.unwrap();
142 assert!(matches!(e1, Event::RunStarted { .. }));
143
144 let e2 = rx.recv().await.unwrap();
145 assert!(matches!(e2, Event::RunCompleted { .. }));
146 }
147
148 #[tokio::test]
149 async fn multiple_subscribers() {
150 let bus = EventBus::new(16);
151 let mut rx1 = bus.subscribe();
152 let mut rx2 = bus.subscribe();
153
154 assert_eq!(bus.subscriber_count(), 2);
155
156 bus.emit(Event::RunCompleted {
157 run_id: "r1".into(),
158 duration: Duration::from_secs(1),
159 });
160
161 let e1 = rx1.recv().await.unwrap();
162 let e2 = rx2.recv().await.unwrap();
163 assert!(matches!(e1, Event::RunCompleted { .. }));
164 assert!(matches!(e2, Event::RunCompleted { .. }));
165 }
166
167 use std::sync::atomic::{AtomicUsize, Ordering};
168
169 #[derive(Default)]
171 struct CountingSink {
172 records: AtomicUsize,
173 flushes: AtomicUsize,
174 }
175
176 impl somatize_core::tracking::EventSink for CountingSink {
177 fn record(&self, _event: &Event) {
178 self.records.fetch_add(1, Ordering::SeqCst);
179 }
180 fn flush(&self) {
181 self.flushes.fetch_add(1, Ordering::SeqCst);
182 }
183 }
184
185 fn run_completed(id: &str) -> Event {
186 Event::RunCompleted {
187 run_id: id.into(),
188 duration: Duration::from_millis(1),
189 }
190 }
191
192 struct ReentrantSink {
198 bus: std::sync::Weak<EventBus>,
199 added: AtomicUsize,
200 }
201
202 impl somatize_core::tracking::EventSink for ReentrantSink {
203 fn record(&self, _event: &Event) {
204 if self.added.fetch_add(1, Ordering::SeqCst) == 0
206 && let Some(bus) = self.bus.upgrade()
207 {
208 bus.add_sink(Arc::new(CountingSink::default()));
209 }
210 }
211 fn flush(&self) {}
212 }
213
214 #[test]
215 fn a_sink_may_touch_the_bus_from_inside_record() {
216 let bus = Arc::new(EventBus::new(16));
217 bus.add_sink(Arc::new(ReentrantSink {
218 bus: Arc::downgrade(&bus),
219 added: AtomicUsize::new(0),
220 }));
221
222 bus.emit(run_completed("r1"));
225 bus.emit(run_completed("r2"));
226
227 bus.flush_sinks();
228 }
229
230 #[test]
231 fn sinks_observe_events_synchronously_before_emit_returns() {
232 let bus = EventBus::new(16);
233 let sink = Arc::new(CountingSink::default());
234 bus.add_sink(sink.clone());
235
236 bus.emit(run_completed("r1"));
237 assert_eq!(sink.records.load(Ordering::SeqCst), 1);
239 }
240
241 #[test]
242 fn remove_sink_flushes_detaches_and_respects_identity() {
243 let bus = EventBus::new(16);
244 let sink = Arc::new(CountingSink::default());
245 let as_dyn: Arc<dyn somatize_core::tracking::EventSink> = sink.clone();
246 bus.add_sink(as_dyn.clone());
247
248 bus.emit(run_completed("r1"));
249 assert_eq!(sink.flushes.load(Ordering::SeqCst), 0);
250
251 let other: Arc<dyn somatize_core::tracking::EventSink> = Arc::new(CountingSink::default());
253 bus.remove_sink(&other);
254 bus.emit(run_completed("r2"));
255 assert_eq!(sink.records.load(Ordering::SeqCst), 2, "still attached");
256
257 bus.remove_sink(&as_dyn);
259 assert_eq!(sink.flushes.load(Ordering::SeqCst), 1, "flushed on removal");
260 bus.emit(run_completed("r3"));
261 assert_eq!(
262 sink.records.load(Ordering::SeqCst),
263 2,
264 "no events after removal"
265 );
266
267 bus.remove_sink(&as_dyn);
269 }
270
271 #[test]
272 fn remove_sink_drops_every_clone_of_a_doubly_registered_arc() {
273 let bus = EventBus::new(16);
276 let sink = Arc::new(CountingSink::default());
277 let as_dyn: Arc<dyn somatize_core::tracking::EventSink> = sink.clone();
278 bus.add_sink(as_dyn.clone());
279 bus.add_sink(as_dyn.clone());
280
281 bus.emit(run_completed("r1"));
282 assert_eq!(sink.records.load(Ordering::SeqCst), 2);
283
284 bus.remove_sink(&as_dyn);
285 bus.emit(run_completed("r2"));
286 assert_eq!(sink.records.load(Ordering::SeqCst), 2);
287 }
288
289 #[test]
290 fn flush_sinks_flushes_all_registered_sinks() {
291 let bus = EventBus::new(16);
292 let a = Arc::new(CountingSink::default());
293 let b = Arc::new(CountingSink::default());
294 bus.add_sink(a.clone());
295 bus.add_sink(b.clone());
296 bus.flush_sinks();
297 assert_eq!(a.flushes.load(Ordering::SeqCst), 1);
298 assert_eq!(b.flushes.load(Ordering::SeqCst), 1);
299 }
300
301 #[tokio::test]
302 async fn sinks_stay_lossless_while_subscribers_lag() {
303 let bus = EventBus::new(4); let sink = Arc::new(CountingSink::default());
307 bus.add_sink(sink.clone());
308 let mut rx = bus.subscribe();
309
310 for i in 0..100 {
311 bus.emit(run_completed(&format!("r{i}")));
312 }
313 assert_eq!(sink.records.load(Ordering::SeqCst), 100, "sink is lossless");
314 match rx.recv().await {
315 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
316 assert!(n > 0, "subscriber lost {n} events");
317 }
318 other => panic!("expected Lagged, got {other:?}"),
319 }
320 }
321
322 #[tokio::test]
323 async fn subscriber_after_emit_misses_earlier_events() {
324 let bus = EventBus::new(16);
325 bus.emit(Event::RunCompleted {
326 run_id: "r1".into(),
327 duration: Duration::from_secs(1),
328 });
329
330 let mut rx = bus.subscribe();
331 bus.emit(Event::RunCompleted {
332 run_id: "r2".into(),
333 duration: Duration::from_secs(2),
334 });
335
336 let event = rx.recv().await.unwrap();
337 if let Event::RunCompleted { run_id, .. } = event {
338 assert_eq!(run_id, "r2"); } else {
340 panic!("wrong event type");
341 }
342 }
343}