Skip to main content

nidus_events/
lib.rs

1#![deny(missing_docs)]
2
3//! Event bus abstractions.
4//!
5//! The built-in bus is in-process and in-memory. It is useful for local domain
6//! events, tests, and adapters that bridge to a real broker, but it is not a
7//! durable queue and does not deliver events across processes.
8
9use std::{
10    collections::{BTreeMap, VecDeque},
11    sync::mpsc,
12    sync::{Arc, Mutex, MutexGuard, Weak},
13};
14
15/// Bounded buffer backing a single subscriber's event queue.
16///
17/// The default capacity is unbounded (every published event is retained until
18/// [`EventSubscriber::drain`] is called). A bounded buffer evicts the oldest
19/// event when pushing beyond its capacity, so a slow or absent drainer can
20/// never grow memory without limit.
21#[derive(Clone, Debug)]
22struct SubscriberBuffer<T> {
23    events: SubscriberEvents<T>,
24}
25
26#[derive(Clone, Debug)]
27enum SubscriberEvents<T> {
28    Unbounded(Vec<T>),
29    Bounded {
30        events: VecDeque<T>,
31        capacity: usize,
32    },
33}
34
35impl<T> Default for SubscriberBuffer<T> {
36    fn default() -> Self {
37        Self {
38            events: SubscriberEvents::Unbounded(Vec::new()),
39        }
40    }
41}
42
43impl<T> SubscriberBuffer<T> {
44    fn push(&mut self, event: T) {
45        match &mut self.events {
46            SubscriberEvents::Unbounded(events) => events.push(event),
47            SubscriberEvents::Bounded { events, capacity } => {
48                if *capacity == 0 {
49                    // A zero-capacity subscriber keeps nothing.
50                    return;
51                }
52                if events.len() == *capacity {
53                    // VecDeque makes oldest-event eviction constant-time.
54                    events.pop_front();
55                }
56                events.push_back(event);
57            }
58        }
59    }
60
61    fn drain(&mut self) -> Vec<T> {
62        match &mut self.events {
63            SubscriberEvents::Unbounded(events) => std::mem::take(events),
64            SubscriberEvents::Bounded { events, .. } => std::mem::take(events).into(),
65        }
66    }
67}
68
69type SubscriberQueue<T> = Arc<Mutex<SubscriberBuffer<T>>>;
70type SubscriberHandle<T> = Weak<Mutex<SubscriberBuffer<T>>>;
71type SubscriberList<T> = Arc<Mutex<Vec<SubscriberHandle<T>>>>;
72
73struct LiveSubscribers<T> {
74    first: Option<SubscriberQueue<T>>,
75    additional: Vec<SubscriberQueue<T>>,
76}
77
78impl<T> LiveSubscribers<T> {
79    fn new() -> Self {
80        Self {
81            first: None,
82            additional: Vec::new(),
83        }
84    }
85
86    fn push(&mut self, subscriber: SubscriberQueue<T>) {
87        if self.first.is_none() {
88            self.first = Some(subscriber);
89        } else {
90            // Keep live-subscriber collection allocation-free for the common
91            // zero/one-subscriber case. This vector allocates only when fan-out
92            // actually has a second target.
93            self.additional.push(subscriber);
94        }
95    }
96}
97
98/// In-process typed event bus.
99///
100/// Subscribers receive events published after they subscribe. Events are cloned
101/// for every active subscriber except the final one, which receives the original
102/// value, and remain queued until that subscriber calls [`EventSubscriber::drain`].
103/// Dropped subscribers are pruned on the next publish or subscriber-count check.
104///
105/// ```
106/// use nidus_events::EventBus;
107///
108/// #[derive(Clone, Debug, PartialEq, Eq)]
109/// struct UserCreated { id: u64 }
110///
111/// let bus = EventBus::<UserCreated>::new();
112/// let subscriber = bus.subscribe();
113///
114/// bus.publish(UserCreated { id: 42 });
115/// assert_eq!(subscriber.drain(), vec![UserCreated { id: 42 }]);
116/// ```
117#[derive(Clone, Debug)]
118pub struct EventBus<T> {
119    subscribers: SubscriberList<T>,
120}
121
122/// Context emitted when an event is observed.
123///
124/// Observed publications receive a generated operation ID, a stable event name
125/// supplied by the caller, and any attributes configured on the
126/// [`ObservedEventBus`]. Cloned contexts share their immutable attributes until
127/// [`Self::with_attribute`] enriches one of them.
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub struct ObservedEventContext {
130    operation_id: String,
131    event_name: String,
132    attributes: Arc<BTreeMap<String, String>>,
133}
134
135impl ObservedEventContext {
136    /// Creates observed event context.
137    pub fn new(operation_id: impl Into<String>, event_name: impl Into<String>) -> Self {
138        Self {
139            operation_id: operation_id.into(),
140            event_name: event_name.into(),
141            attributes: Arc::new(BTreeMap::new()),
142        }
143    }
144
145    /// Adds a context attribute.
146    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
147        Arc::make_mut(&mut self.attributes).insert(key.into(), value.into());
148        self
149    }
150
151    /// Returns the operation id.
152    pub fn operation_id(&self) -> &str {
153        &self.operation_id
154    }
155
156    /// Returns the event name.
157    pub fn event_name(&self) -> &str {
158        &self.event_name
159    }
160
161    /// Returns context attributes.
162    pub fn attributes(&self) -> &BTreeMap<String, String> {
163        &self.attributes
164    }
165}
166
167/// Observer hook for event publication.
168///
169/// The hook runs synchronously after the event has been published to in-memory
170/// subscribers. Keep implementations fast and non-blocking, or forward to your
171/// own async/export pipeline.
172pub trait EventObserver<T>: Clone + Send + Sync + 'static
173where
174    T: Clone + Send + Sync + 'static,
175{
176    /// Called after an event is published.
177    fn on_event_published(&self, context: &ObservedEventContext);
178}
179
180impl<T> EventObserver<T> for ()
181where
182    T: Clone + Send + Sync + 'static,
183{
184    fn on_event_published(&self, _context: &ObservedEventContext) {}
185}
186
187/// Observer implementation that sends observed event contexts to a channel.
188///
189/// Use [`event_observer_channel`] when the publish path should only enqueue
190/// telemetry and another thread or task will do slower export work. Sending to
191/// the channel is best-effort: if the receiver has been dropped, publication
192/// still succeeds and the context is discarded.
193#[derive(Clone)]
194pub struct EventObserverChannel {
195    sender: mpsc::Sender<ObservedEventContext>,
196}
197
198impl EventObserverChannel {
199    /// Creates a channel observer from an existing sender.
200    pub fn new(sender: mpsc::Sender<ObservedEventContext>) -> Self {
201        Self { sender }
202    }
203}
204
205impl<T> EventObserver<T> for EventObserverChannel
206where
207    T: Clone + Send + Sync + 'static,
208{
209    fn on_event_published(&self, context: &ObservedEventContext) {
210        let _ = self.sender.send(context.clone());
211    }
212}
213
214/// Creates a channel-backed event observer and its receiver.
215///
216/// The returned observer can be passed to [`ObservedEventBus::new`]. The
217/// receiver yields [`ObservedEventContext`] values in publication order for a
218/// separate exporter thread or task.
219pub fn event_observer_channel() -> (EventObserverChannel, mpsc::Receiver<ObservedEventContext>) {
220    let (sender, receiver) = mpsc::channel();
221    (EventObserverChannel::new(sender), receiver)
222}
223
224/// Event bus wrapper that records publication context.
225///
226/// `ObservedEventBus` adds a tracing span and observer callback around
227/// [`EventBus::publish`]. It does not change delivery semantics: publication is
228/// still in-process, non-durable fan-out to current subscribers.
229///
230/// ```
231/// use std::sync::{Arc, Mutex};
232/// use nidus_events::{EventBus, EventObserver, ObservedEventBus, ObservedEventContext};
233///
234/// #[derive(Clone)]
235/// struct UserCreated;
236///
237/// #[derive(Clone)]
238/// struct Observer(Arc<Mutex<Vec<String>>>);
239///
240/// impl EventObserver<UserCreated> for Observer {
241///     fn on_event_published(&self, context: &ObservedEventContext) {
242///         self.0.lock().unwrap().push(context.event_name().to_owned());
243///     }
244/// }
245///
246/// let events = Arc::new(Mutex::new(Vec::new()));
247/// let observed = ObservedEventBus::new(EventBus::new(), Observer(Arc::clone(&events)))
248///     .context("service", "users-api");
249///
250/// observed.publish_named("user.created", UserCreated);
251/// assert_eq!(events.lock().unwrap().as_slice(), ["user.created"]);
252/// ```
253#[derive(Clone)]
254pub struct ObservedEventBus<T, O = ()>
255where
256    T: Clone + Send + Sync + 'static,
257    O: EventObserver<T>,
258{
259    bus: EventBus<T>,
260    observer: O,
261    attributes: Arc<BTreeMap<String, String>>,
262    operation_id_generator: Arc<dyn Fn() -> String + Send + Sync>,
263}
264
265impl<T, O> ObservedEventBus<T, O>
266where
267    T: Clone + Send + Sync + 'static,
268    O: EventObserver<T>,
269{
270    /// Creates an observed wrapper around an event bus.
271    pub fn new(bus: EventBus<T>, observer: O) -> Self {
272        Self {
273            bus,
274            observer,
275            attributes: Arc::new(BTreeMap::new()),
276            operation_id_generator: Arc::new(|| uuid::Uuid::new_v4().to_string()),
277        }
278    }
279
280    /// Adds a context attribute propagated to future observed publications.
281    pub fn context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
282        // Cloned wrappers retain value semantics while sharing read-mostly
283        // configuration until one clone is enriched.
284        Arc::make_mut(&mut self.attributes).insert(key.into(), value.into());
285        self
286    }
287
288    /// Replaces the operation id generator.
289    pub fn operation_id_generator(
290        mut self,
291        generator: impl Fn() -> String + Send + Sync + 'static,
292    ) -> Self {
293        self.operation_id_generator = Arc::new(generator);
294        self
295    }
296
297    /// Publishes an event with an explicit stable event name.
298    ///
299    /// The event is first published to current subscribers, then the observer is
300    /// called with the generated [`ObservedEventContext`].
301    pub fn publish_named(&self, event_name: impl Into<String>, event: T) {
302        let event_name = event_name.into();
303        let context = self.context_for(event_name);
304        let span = tracing::info_span!(
305            "event.publish",
306            event.name = %context.event_name(),
307            event.operation_id = %context.operation_id()
308        );
309        let _entered = span.enter();
310        self.bus.publish(event);
311        self.observer.on_event_published(&context);
312    }
313
314    /// Returns the wrapped event bus.
315    pub fn bus(&self) -> &EventBus<T> {
316        &self.bus
317    }
318
319    fn context_for(&self, event_name: String) -> ObservedEventContext {
320        ObservedEventContext {
321            operation_id: (self.operation_id_generator)(),
322            event_name,
323            // Attributes are configuration: publication only needs a shared
324            // snapshot, while later enrichment remains copy-on-write.
325            attributes: Arc::clone(&self.attributes),
326        }
327    }
328}
329
330impl<T> EventBus<T>
331where
332    T: Clone,
333{
334    /// Creates an empty event bus.
335    pub fn new() -> Self {
336        Self {
337            subscribers: Arc::new(Mutex::new(Vec::new())),
338        }
339    }
340
341    /// Subscribes to future events.
342    ///
343    /// The returned subscriber does not replay events published before this
344    /// call. Its queue is unbounded; use [`Self::subscribe_with_capacity`] to
345    /// bound memory when the subscriber may drain slowly or never.
346    pub fn subscribe(&self) -> EventSubscriber<T> {
347        self.subscribe_with_buffer(SubscriberBuffer::default())
348    }
349
350    /// Subscribes to future events with a bounded queue.
351    ///
352    /// The subscriber retains at most `capacity` events. When a new event would
353    /// exceed the capacity, the oldest event is evicted, so memory stays bounded
354    /// even if the subscriber never calls [`EventSubscriber::drain`]. A capacity
355    /// of `0` keeps no events (useful when only the [`ObservedEventBus`]
356    /// observer side-effect matters).
357    pub fn subscribe_with_capacity(&self, capacity: usize) -> EventSubscriber<T> {
358        self.subscribe_with_buffer(SubscriberBuffer {
359            events: SubscriberEvents::Bounded {
360                // Preserve the previous lazy-allocation behavior: declaring a
361                // large bound should not allocate until events arrive.
362                events: VecDeque::new(),
363                capacity,
364            },
365        })
366    }
367
368    fn subscribe_with_buffer(&self, buffer: SubscriberBuffer<T>) -> EventSubscriber<T> {
369        let queue = Arc::new(Mutex::new(buffer));
370        lock_unpoisoned(&self.subscribers).push(Arc::downgrade(&queue));
371        EventSubscriber { queue }
372    }
373
374    /// Publishes an event to current subscribers.
375    ///
376    /// The event is cloned for every active subscriber except the final one,
377    /// which receives the original value. Bounded subscribers may evict the
378    /// oldest event to honor their capacity.
379    pub fn publish(&self, event: T) {
380        let LiveSubscribers {
381            first,
382            mut additional,
383        } = self.live_subscribers();
384        let Some(first) = first else {
385            return;
386        };
387
388        let Some(last) = additional.pop() else {
389            lock_unpoisoned(&first).push(event);
390            return;
391        };
392
393        lock_unpoisoned(&first).push(event.clone());
394        for subscriber in additional {
395            lock_unpoisoned(&subscriber).push(event.clone());
396        }
397        lock_unpoisoned(&last).push(event);
398    }
399
400    /// Wraps this bus with an observer.
401    pub fn observed<O>(self, observer: O) -> ObservedEventBus<T, O>
402    where
403        T: Send + Sync + 'static,
404        O: EventObserver<T>,
405    {
406        ObservedEventBus::new(self, observer)
407    }
408
409    /// Returns the number of active subscribers.
410    pub fn subscriber_count(&self) -> usize {
411        let mut subscribers = lock_unpoisoned(&self.subscribers);
412        subscribers.retain(|subscriber| subscriber.upgrade().is_some());
413        subscribers.len()
414    }
415
416    fn live_subscribers(&self) -> LiveSubscribers<T> {
417        let mut subscribers = lock_unpoisoned(&self.subscribers);
418        let mut live = LiveSubscribers::new();
419        subscribers.retain(|subscriber| {
420            if let Some(queue) = subscriber.upgrade() {
421                live.push(queue);
422                true
423            } else {
424                false
425            }
426        });
427        live
428    }
429}
430
431impl<T> Default for EventBus<T>
432where
433    T: Clone,
434{
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440/// Subscription handle for an event bus.
441#[derive(Clone, Debug)]
442pub struct EventSubscriber<T> {
443    queue: SubscriberQueue<T>,
444}
445
446impl<T> EventSubscriber<T> {
447    /// Drains all received events.
448    pub fn drain(&self) -> Vec<T> {
449        lock_unpoisoned(&self.queue).drain()
450    }
451}
452
453fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
454    mutex.lock().unwrap_or_else(|poisoned| {
455        tracing::warn!("event bus mutex poisoned; recovering inner state");
456        poisoned.into_inner()
457    })
458}
459
460#[cfg(test)]
461mod tests {
462    use std::{sync::Arc, thread};
463
464    use super::*;
465    use tracing::Level;
466    use tracing_subscriber::{Layer, fmt::MakeWriter, layer::SubscriberExt};
467
468    #[derive(Clone, Default)]
469    struct SharedLogWriter {
470        output: Arc<Mutex<Vec<u8>>>,
471    }
472
473    impl SharedLogWriter {
474        fn contents(&self) -> String {
475            String::from_utf8(self.output.lock().unwrap().clone()).unwrap()
476        }
477
478        fn clear(&self) {
479            self.output.lock().unwrap().clear();
480        }
481    }
482
483    impl<'writer> MakeWriter<'writer> for SharedLogWriter {
484        type Writer = SharedLogGuard;
485
486        fn make_writer(&'writer self) -> Self::Writer {
487            SharedLogGuard {
488                output: Arc::clone(&self.output),
489            }
490        }
491    }
492
493    struct SharedLogGuard {
494        output: Arc<Mutex<Vec<u8>>>,
495    }
496
497    impl std::io::Write for SharedLogGuard {
498        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
499            self.output.lock().unwrap().extend_from_slice(buf);
500            Ok(buf.len())
501        }
502
503        fn flush(&mut self) -> std::io::Result<()> {
504            Ok(())
505        }
506    }
507
508    #[derive(Clone, Debug, PartialEq, Eq)]
509    struct UserCreated(u64);
510
511    #[test]
512    fn event_bus_recovers_from_poisoned_subscriber_list() {
513        let bus = EventBus::<UserCreated>::new();
514        let subscribers = Arc::clone(&bus.subscribers);
515
516        let panic = thread::spawn(move || {
517            let _subscribers = subscribers.lock().unwrap();
518            panic!("poison subscriber list");
519        });
520        assert!(panic.join().is_err());
521
522        let subscriber = bus.subscribe();
523        bus.publish(UserCreated(42));
524
525        assert_eq!(subscriber.drain(), vec![UserCreated(42)]);
526    }
527
528    #[test]
529    fn event_bus_warns_when_recovering_from_poisoned_subscriber_list() {
530        let bus = EventBus::<UserCreated>::new();
531        let subscribers = Arc::clone(&bus.subscribers);
532        let panic = thread::spawn(move || {
533            let _subscribers = subscribers.lock().unwrap();
534            panic!("poison subscriber list");
535        });
536        assert!(panic.join().is_err());
537
538        let writer = SharedLogWriter::default();
539        let subscriber = tracing_subscriber::registry().with(
540            tracing_subscriber::fmt::layer()
541                .with_writer(writer.clone())
542                .with_ansi(false)
543                .with_target(false)
544                .with_filter(tracing_subscriber::filter::LevelFilter::from_level(
545                    Level::WARN,
546                )),
547        );
548
549        tracing::subscriber::with_default(subscriber, || {
550            for _ in 0..16 {
551                writer.clear();
552                tracing_core::callsite::rebuild_interest_cache();
553                let _subscriber = bus.subscribe();
554                let logs = writer.contents();
555                if logs.contains("event bus mutex poisoned") {
556                    return;
557                }
558                std::thread::yield_now();
559            }
560        });
561
562        let logs = writer.contents();
563        assert!(logs.contains("event bus mutex poisoned"), "{logs}");
564    }
565
566    #[test]
567    fn event_bus_recovers_from_poisoned_subscriber_queue() {
568        let bus = EventBus::<UserCreated>::new();
569        let subscriber = bus.subscribe();
570        let queue = Arc::clone(&subscriber.queue);
571
572        let panic = thread::spawn(move || {
573            let _queue = queue.lock().unwrap();
574            panic!("poison subscriber queue");
575        });
576        assert!(panic.join().is_err());
577
578        bus.publish(UserCreated(42));
579
580        assert_eq!(subscriber.drain(), vec![UserCreated(42)]);
581    }
582
583    #[test]
584    fn bounded_subscriber_drops_oldest_events_beyond_capacity() {
585        let bus = EventBus::<UserCreated>::new();
586        let bounded = bus.subscribe_with_capacity(2);
587
588        bus.publish(UserCreated(1));
589        bus.publish(UserCreated(2));
590        bus.publish(UserCreated(3));
591
592        // Capacity is 2: the oldest event is evicted to keep the buffer
593        // bounded, so a slow/absent drainer can never grow memory unbounded.
594        assert_eq!(bounded.drain(), vec![UserCreated(2), UserCreated(3)]);
595
596        // A second batch after draining continues to respect the cap.
597        bus.publish(UserCreated(4));
598        bus.publish(UserCreated(5));
599        bus.publish(UserCreated(6));
600        assert_eq!(bounded.drain(), vec![UserCreated(5), UserCreated(6)]);
601    }
602
603    #[test]
604    fn zero_capacity_subscriber_never_retains_events() {
605        let bus = EventBus::<UserCreated>::new();
606        let subscriber = bus.subscribe_with_capacity(0);
607
608        bus.publish(UserCreated(1));
609        bus.publish(UserCreated(2));
610
611        assert!(subscriber.drain().is_empty());
612    }
613
614    #[test]
615    fn unbounded_subscriber_keeps_all_events_by_default() {
616        let bus = EventBus::<UserCreated>::new();
617        let subscriber = bus.subscribe();
618
619        for id in 1..=50u64 {
620            bus.publish(UserCreated(id));
621        }
622
623        let drained: Vec<u64> = subscriber
624            .drain()
625            .into_iter()
626            .map(|event| event.0)
627            .collect();
628        assert_eq!(drained, (1..=50).collect::<Vec<_>>());
629    }
630
631    #[test]
632    fn observed_context_shares_configured_attributes_until_enriched() {
633        let observed = EventBus::<UserCreated>::new()
634            .observed(())
635            .operation_id_generator(|| "event-run".to_owned())
636            .context("service", "users-api");
637
638        let enriched_observed = observed.clone().context("region", "sa-east-1");
639        assert!(!Arc::ptr_eq(
640            &enriched_observed.attributes,
641            &observed.attributes
642        ));
643        assert!(!observed.attributes.contains_key("region"));
644        assert_eq!(
645            enriched_observed.attributes.get("region").unwrap(),
646            "sa-east-1"
647        );
648
649        let context = observed.context_for("user.created".to_owned());
650        assert!(Arc::ptr_eq(&context.attributes, &observed.attributes));
651
652        let enriched = context.clone().with_attribute("request_id", "request-42");
653        assert!(!Arc::ptr_eq(&enriched.attributes, &context.attributes));
654        assert_eq!(context.attributes().get("service").unwrap(), "users-api");
655        assert!(!context.attributes().contains_key("request_id"));
656        assert_eq!(
657            enriched.attributes().get("request_id").unwrap(),
658            "request-42"
659        );
660    }
661}