1use std::collections::HashMap;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, RwLock};
8
9use tokio::sync::mpsc;
10use tracing::warn;
11
12use crate::simple_store::SimplePvStore;
13
14pub const DISPATCH_QUEUE_CAPACITY: usize = 1024;
16
17pub trait EventSink: Send + Sync {
41 fn on_event(&self, event: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
43}
44
45pub type EventHandler = Arc<
51 dyn Fn(Arc<SimplePvStore>, String) -> Pin<Box<dyn Future<Output = ()> + Send>>
52 + Send
53 + Sync,
54>;
55
56pub type StartHook =
58 Arc<dyn Fn(Arc<SimplePvStore>) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
59
60struct Dispatch {
62 handler: EventHandler,
63 event: String,
64}
65
66pub struct Events {
72 sinks: RwLock<Vec<Arc<dyn EventSink>>>,
73 handlers: RwLock<HashMap<String, Vec<EventHandler>>>,
74 tx: mpsc::Sender<Dispatch>,
75 rx: RwLock<Option<mpsc::Receiver<Dispatch>>>,
76 dropped: AtomicU64,
77 failed: Arc<AtomicU64>,
78 dispatcher_started: std::sync::atomic::AtomicBool,
82 inflight: Arc<AtomicU64>,
85}
86
87impl Events {
88 pub fn new() -> Self {
89 let (tx, rx) = mpsc::channel(DISPATCH_QUEUE_CAPACITY);
90 Self {
91 sinks: RwLock::new(Vec::new()),
92 handlers: RwLock::new(HashMap::new()),
93 tx,
94 rx: RwLock::new(Some(rx)),
95 dropped: AtomicU64::new(0),
96 failed: Arc::new(AtomicU64::new(0)),
97 dispatcher_started: std::sync::atomic::AtomicBool::new(false),
98 inflight: Arc::new(AtomicU64::new(0)),
99 }
100 }
101
102 pub fn add_sink(&self, sink: Arc<dyn EventSink>) {
107 self.sinks.write().unwrap().push(sink);
108 }
109
110 pub fn add_handler(&self, event: impl Into<String>, handler: EventHandler) {
114 self.handlers
115 .write()
116 .unwrap()
117 .entry(event.into())
118 .or_default()
119 .push(handler);
120 }
121
122 pub fn dropped_count(&self) -> u64 {
124 self.dropped.load(Ordering::Relaxed)
125 }
126
127 pub fn failed_count(&self) -> u64 {
129 self.failed.load(Ordering::Relaxed)
130 }
131
132 pub fn start_dispatcher(&self, store: Arc<SimplePvStore>) {
134 let Some(mut rx) = self.rx.write().unwrap().take() else {
138 tracing::debug!("Events::start_dispatcher called again; already running");
139 return;
140 };
141 let inflight = self.inflight.clone();
142 let failed = self.failed.clone();
143 self.dispatcher_started.store(true, Ordering::SeqCst);
144 tokio::spawn(async move {
145 while let Some(Dispatch { handler, event }) = rx.recv().await {
146 let fut = handler(store.clone(), event.clone());
147 let result =
150 futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(fut)).await;
151 if result.is_err() {
152 warn!("event handler for '{}' panicked", event);
153 failed.fetch_add(1, Ordering::Relaxed);
154 }
155 inflight.fetch_sub(1, Ordering::SeqCst);
156 }
157 });
158 }
159
160 pub async fn post(&self, event: &str) {
171 let sinks = self.sinks.read().unwrap().clone();
175 for sink in &sinks {
176 let fut = async { sink.on_event(event).await };
181 let result = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(fut)).await;
182 if result.is_err() {
183 warn!("event sink for '{}' panicked; continuing fan-out", event);
184 self.failed.fetch_add(1, Ordering::Relaxed);
185 }
186 }
187
188 let handlers = {
189 let map = self.handlers.read().unwrap();
190 map.get(event).cloned().unwrap_or_default()
191 };
192 if !handlers.is_empty() && !self.dispatcher_started.load(Ordering::SeqCst) {
197 warn!(
198 "posted '{}' with {} handler(s) registered but the event dispatcher \
199 has not started — nothing will run them until the server starts \
200 (run()/start()/start_background())",
201 event,
202 handlers.len()
203 );
204 }
205 self.inflight
206 .fetch_add(handlers.len() as u64, Ordering::SeqCst);
207 for handler in handlers {
208 let queued = self.tx.try_send(Dispatch {
209 handler,
210 event: event.to_string(),
211 });
212 if queued.is_err() {
213 self.inflight.fetch_sub(1, Ordering::SeqCst);
214 let n = self.dropped.fetch_add(1, Ordering::Relaxed) + 1;
215 if n.is_power_of_two() {
216 warn!(
217 "event dispatch queue full; dropped handler for '{}' ({} dropped so far)",
218 event, n
219 );
220 }
221 }
222 }
223 }
224
225 pub async fn drain(&self) {
233 const DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
234 if !self.dispatcher_started.load(Ordering::SeqCst)
239 && self.inflight.load(Ordering::SeqCst) > 0
240 {
241 panic!(
242 "Events::drain() called with {} handler invocation(s) queued but the \
243 dispatcher was never started — nothing will ever run them. Start the \
244 server (run() / start() / start_background(), or \
245 Events::start_dispatcher) before posting events you intend to drain.",
246 self.inflight.load(Ordering::SeqCst)
247 );
248 }
249 let deadline = tokio::time::Instant::now() + DRAIN_TIMEOUT;
250 while self.inflight.load(Ordering::SeqCst) > 0 {
251 if tokio::time::Instant::now() >= deadline {
252 panic!(
253 "Events::drain() timed out after {DRAIN_TIMEOUT:?} with {} handler(s) still in flight — \
254 the dispatcher likely stopped consuming (e.g. a handler future \
255 that never returns, or a panic no longer being caught)",
256 self.inflight.load(Ordering::SeqCst)
257 );
258 }
259 tokio::task::yield_now().await;
260 }
261 }
262}
263
264impl Default for Events {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use std::sync::Mutex;
274 use std::sync::atomic::{AtomicUsize, Ordering};
275
276 fn test_store() -> Arc<crate::simple_store::SimplePvStore> {
278 use crate::pva_server::PvaServer;
279 let server = PvaServer::builder().ai("T:X", 0.0).build();
280 server.store().clone()
281 }
282
283 #[tokio::test]
284 async fn handlers_run_on_the_dispatcher_not_inline() {
285 let store = test_store();
286 let events = Events::new();
287 let ran = Arc::new(AtomicUsize::new(0));
288
289 let r = ran.clone();
290 events.add_handler(
291 "GO",
292 Arc::new(move |_store, _event| {
293 let r = r.clone();
294 Box::pin(async move {
295 r.fetch_add(1, Ordering::SeqCst);
296 })
297 }),
298 );
299 events.start_dispatcher(store);
300
301 events.post("GO").await;
302 assert_eq!(ran.load(Ordering::SeqCst), 0, "handler ran inline");
304
305 events.drain().await;
306 assert_eq!(ran.load(Ordering::SeqCst), 1);
307 }
308
309 #[tokio::test]
310 async fn handlers_are_serialized_in_registration_order() {
311 let store = test_store();
312 let events = Events::new();
313 let log = Arc::new(Mutex::new(Vec::new()));
314
315 for label in ["first", "second", "third"] {
316 let log = log.clone();
317 events.add_handler(
318 "GO",
319 Arc::new(move |_store, _event| {
320 let log = log.clone();
321 let label = label.to_string();
322 Box::pin(async move {
323 log.lock().unwrap().push(format!("{label}:enter"));
324 tokio::task::yield_now().await;
325 log.lock().unwrap().push(format!("{label}:exit"));
326 })
327 }),
328 );
329 }
330 events.start_dispatcher(store);
331
332 events.post("GO").await;
333 events.drain().await;
334
335 assert_eq!(
337 log.lock().unwrap().as_slice(),
338 &[
339 "first:enter".to_string(),
340 "first:exit".to_string(),
341 "second:enter".to_string(),
342 "second:exit".to_string(),
343 "third:enter".to_string(),
344 "third:exit".to_string(),
345 ]
346 );
347 }
348
349 #[tokio::test]
350 async fn only_handlers_for_the_posted_event_run() {
351 let store = test_store();
352 let events = Events::new();
353 let a = Arc::new(AtomicUsize::new(0));
354 let b = Arc::new(AtomicUsize::new(0));
355
356 let ac = a.clone();
357 events.add_handler("A", Arc::new(move |_s, _e| {
358 let ac = ac.clone();
359 Box::pin(async move { ac.fetch_add(1, Ordering::SeqCst); })
360 }));
361 let bc = b.clone();
362 events.add_handler("B", Arc::new(move |_s, _e| {
363 let bc = bc.clone();
364 Box::pin(async move { bc.fetch_add(1, Ordering::SeqCst); })
365 }));
366 events.start_dispatcher(store);
367
368 events.post("A").await;
369 events.drain().await;
370
371 assert_eq!(a.load(Ordering::SeqCst), 1);
372 assert_eq!(b.load(Ordering::SeqCst), 0);
373 }
374
375 #[tokio::test]
376 async fn handler_receives_the_event_name() {
377 let store = test_store();
378 let events = Events::new();
379 let seen = Arc::new(Mutex::new(Vec::new()));
380
381 let s = seen.clone();
382 events.add_handler("SHUTTER", Arc::new(move |_store, event| {
383 let s = s.clone();
384 Box::pin(async move { s.lock().unwrap().push(event); })
385 }));
386 events.start_dispatcher(store);
387
388 events.post("SHUTTER").await;
389 events.drain().await;
390
391 assert_eq!(seen.lock().unwrap().as_slice(), &["SHUTTER".to_string()]);
392 }
393
394 #[tokio::test]
395 async fn handler_can_write_the_store() {
396 let store = test_store();
397 let events = Events::new();
398
399 events.add_handler("BUMP", Arc::new(|store, _event| {
400 Box::pin(async move {
401 store
402 .set_value("T:X", spvirit_types::ScalarValue::F64(42.0))
403 .await;
404 })
405 }));
406 events.start_dispatcher(store.clone());
407
408 events.post("BUMP").await;
409 events.drain().await;
410
411 assert_eq!(
412 store.get_value("T:X").await,
413 Some(spvirit_types::ScalarValue::F64(42.0))
414 );
415 }
416
417 #[tokio::test]
418 async fn full_queue_drops_and_counts() {
419 let store = test_store();
420 let events = Events::new();
421 let gate = Arc::new(tokio::sync::Notify::new());
423 let g = gate.clone();
424 events.add_handler("FLOOD", Arc::new(move |_s, _e| {
425 let g = g.clone();
426 Box::pin(async move { g.notified().await; })
427 }));
428 events.start_dispatcher(store);
429
430 for _ in 0..(DISPATCH_QUEUE_CAPACITY + 50) {
437 events.post("FLOOD").await;
438 }
439
440 assert!(
441 events.dropped_count() > 0,
442 "expected drops once the queue filled, got {}",
443 events.dropped_count()
444 );
445
446 gate.notify_waiters();
448 }
449
450 #[tokio::test]
451 async fn dispatcher_survives_a_panicking_handler() {
452 let store = test_store();
453 let events = Events::new();
454 let after = Arc::new(AtomicUsize::new(0));
455
456 events.add_handler("BOOM", Arc::new(|_s, _e| {
457 Box::pin(async { panic!("handler blew up"); })
458 }));
459 let a = after.clone();
460 events.add_handler("BOOM", Arc::new(move |_s, _e| {
461 let a = a.clone();
462 Box::pin(async move { a.fetch_add(1, Ordering::SeqCst); })
463 }));
464 events.start_dispatcher(store);
465
466 events.post("BOOM").await;
467 events.drain().await;
468
469 assert_eq!(
470 after.load(Ordering::SeqCst),
471 1,
472 "handler after the panicking one must still run"
473 );
474 assert_eq!(events.failed_count(), 1);
475 }
476
477 struct RecordingSink {
478 seen: Mutex<Vec<String>>,
479 }
480
481 impl EventSink for RecordingSink {
482 fn on_event(&self, event: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
483 let event = event.to_string();
484 Box::pin(async move {
485 self.seen.lock().unwrap().push(event);
486 })
487 }
488 }
489
490 #[tokio::test]
491 async fn post_calls_sinks_in_registration_order() {
492 let a = Arc::new(RecordingSink { seen: Mutex::new(Vec::new()) });
493 let b = Arc::new(RecordingSink { seen: Mutex::new(Vec::new()) });
494 let events = Events::new();
495 events.add_sink(a.clone());
496 events.add_sink(b.clone());
497
498 events.post("SHUTTER").await;
499
500 assert_eq!(a.seen.lock().unwrap().as_slice(), &["SHUTTER".to_string()]);
501 assert_eq!(b.seen.lock().unwrap().as_slice(), &["SHUTTER".to_string()]);
502 }
503
504 #[tokio::test]
505 async fn post_with_no_sinks_is_a_noop() {
506 let events = Events::new();
507 events.post("NOBODY:LISTENING").await;
508 }
509
510 #[tokio::test]
511 #[should_panic(expected = "the dispatcher was never started")]
512 async fn drain_without_a_dispatcher_fails_immediately_and_says_why() {
513 let events = Events::new();
517 events.add_handler(
518 "GO",
519 Arc::new(|_s, _e| Box::pin(async {})),
520 );
521 events.post("GO").await;
522 let t0 = std::time::Instant::now();
523 let hit = std::panic::AssertUnwindSafe(events.drain());
524 let result = futures::FutureExt::catch_unwind(hit).await;
525 assert!(
526 t0.elapsed() < std::time::Duration::from_secs(1),
527 "drain() must fail fast when the dispatcher never started, took {:?}",
528 t0.elapsed()
529 );
530 std::panic::resume_unwind(result.expect_err("drain() must panic"));
531 }
532
533 #[tokio::test]
534 async fn drain_with_nothing_queued_is_fine_without_a_dispatcher() {
535 let events = Events::new();
538 events.post("NOBODY").await;
539 events.drain().await;
540 }
541
542 #[tokio::test]
543 async fn a_panicking_sink_does_not_truncate_the_fan_out() {
544 struct BoomSink;
545 impl EventSink for BoomSink {
546 fn on_event(&self, _event: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
547 Box::pin(async { panic!("sink blew up") })
548 }
549 }
550
551 let store = test_store();
552 let events = Events::new();
553 let before = Arc::new(RecordingSink { seen: Mutex::new(Vec::new()) });
554 let after = Arc::new(RecordingSink { seen: Mutex::new(Vec::new()) });
555 events.add_sink(before.clone());
556 events.add_sink(Arc::new(BoomSink));
557 events.add_sink(after.clone());
558
559 let handler_ran = Arc::new(AtomicUsize::new(0));
560 let h = handler_ran.clone();
561 events.add_handler(
562 "BOOM",
563 Arc::new(move |_s, _e| {
564 let h = h.clone();
565 Box::pin(async move {
566 h.fetch_add(1, Ordering::SeqCst);
567 })
568 }),
569 );
570 events.start_dispatcher(store);
571
572 events.post("BOOM").await;
574 events.drain().await;
575
576 assert_eq!(before.seen.lock().unwrap().as_slice(), &["BOOM".to_string()]);
577 assert_eq!(
578 after.seen.lock().unwrap().as_slice(),
579 &["BOOM".to_string()],
580 "a sink after the panicking one must still see the event"
581 );
582 assert_eq!(
583 handler_ran.load(Ordering::SeqCst),
584 1,
585 "a panicking sink must not stop handlers from being queued"
586 );
587 assert_eq!(events.failed_count(), 1);
588 }
589
590 #[tokio::test]
591 async fn a_handler_may_post_another_event() {
592 let store = test_store();
593 let events = Arc::new(Events::new());
594 let log = Arc::new(Mutex::new(Vec::new()));
595
596 let l = log.clone();
597 let ev = events.clone();
598 events.add_handler("FIRST", Arc::new(move |_s, _e| {
599 let l = l.clone();
600 let ev = ev.clone();
601 Box::pin(async move {
602 l.lock().unwrap().push("first:enter".to_string());
603 ev.post("SECOND").await;
604 l.lock().unwrap().push("first:exit".to_string());
605 })
606 }));
607
608 let l = log.clone();
609 events.add_handler("SECOND", Arc::new(move |_s, _e| {
610 let l = l.clone();
611 Box::pin(async move { l.lock().unwrap().push("second".to_string()); })
612 }));
613
614 events.start_dispatcher(store);
615 events.post("FIRST").await;
616 events.drain().await;
617
618 assert_eq!(
619 log.lock().unwrap().as_slice(),
620 &[
621 "first:enter".to_string(),
622 "first:exit".to_string(),
623 "second".to_string(),
624 ],
625 "nested handler must queue behind the posting handler, not run inside it"
626 );
627 }
628
629 #[tokio::test]
630 async fn a_sink_may_post_another_event_without_deadlocking() {
631 struct LateSink {
636 fired: Arc<AtomicUsize>,
637 }
638 impl EventSink for LateSink {
639 fn on_event(&self, _event: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
640 Box::pin(async move {
641 self.fired.fetch_add(1, Ordering::SeqCst);
642 })
643 }
644 }
645
646 struct Reposter {
647 events: Mutex<Option<std::sync::Weak<Events>>>,
648 fired: AtomicUsize,
649 late_fired: Arc<AtomicUsize>,
650 }
651 impl EventSink for Reposter {
652 fn on_event(&self, event: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
653 let event = event.to_string();
654 Box::pin(async move {
655 if event != "OUTER" {
656 return;
657 }
658 self.fired.fetch_add(1, Ordering::SeqCst);
659 let ev = {
662 let g = self.events.lock().unwrap();
663 g.as_ref().and_then(|w| w.upgrade())
664 };
665 if let Some(ev) = ev {
666 ev.post("INNER").await;
667 ev.add_sink(Arc::new(LateSink {
675 fired: self.late_fired.clone(),
676 }));
677 }
678 })
679 }
680 }
681
682 let events = Arc::new(Events::new());
683 let late_fired = Arc::new(AtomicUsize::new(0));
684 let sink = Arc::new(Reposter {
685 events: Mutex::new(Some(Arc::downgrade(&events))),
686 fired: AtomicUsize::new(0),
687 late_fired: late_fired.clone(),
688 });
689 events.add_sink(sink.clone());
690
691 const CALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
700 let ev = events.clone();
701 let (done_tx, done_rx) = std::sync::mpsc::channel();
702 let handle = std::thread::spawn(move || {
703 let rt = tokio::runtime::Builder::new_current_thread()
704 .build()
705 .expect("current_thread runtime for the sink call-out");
706 rt.block_on(ev.post("OUTER"));
707 let _ = done_tx.send(());
708 });
709 if done_rx.recv_timeout(CALL_TIMEOUT).is_err() {
710 panic!(
711 "sink call-out deadlocked — `post()` is holding a lock across the call-out"
712 );
713 }
714 handle.join().expect("post(\"OUTER\") thread panicked");
717
718 assert_eq!(sink.fired.load(Ordering::SeqCst), 1);
719 assert_eq!(
720 late_fired.load(Ordering::SeqCst),
721 0,
722 "late sink registered but not yet posted to"
723 );
724
725 events.post("PROBE").await;
730 assert_eq!(
731 late_fired.load(Ordering::SeqCst),
732 1,
733 "sink registered from inside a call-out must take effect"
734 );
735 }
736}