pamoja_bus/lib.rs
1//! An in-memory typed publish/subscribe event bus.
2//!
3//! [`BroadcastBus`] implements the core [`EventBus`] trait
4//! over a bounded broadcast channel: every event published is delivered to every
5//! current subscriber. Producers such as sensors and transports publish events,
6//! and consumers await them, all statically typed to one event type per bus.
7//!
8//! [`EventPublisher`] is the handle for a part that only announces. It has no
9//! queue of its own, it clones into every task and callback that publishes, and
10//! publishing from it never waits, so it can announce while a subscriber on the
11//! same bus is waiting for its next event.
12//!
13//! The bus is bounded, so a subscriber that falls far enough behind drops the
14//! events it missed and resumes from the most recent ones, and
15//! [`BroadcastBus::missed`] counts what it lost. This keeps a slow consumer from
16//! holding memory without bound, which matters on constrained devices.
17//!
18//! # Examples
19//!
20//! A pump controller's events reach a logger and a dashboard: each subscriber hears every
21//! event published after it joined, in order.
22//!
23//! ```
24//! use pamoja_bus::BroadcastBus;
25//! use pamoja_core::EventBus;
26//!
27//! # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
28//! let bus = BroadcastBus::new(16);
29//! let mut logger = bus.subscribe();
30//! let mut dashboard = bus.subscribe();
31//!
32//! bus.publish("pump started").await?;
33//! bus.publish("pressure low").await?;
34//! assert_eq!(logger.next_event().await?, Some("pump started"));
35//! assert_eq!(logger.next_event().await?, Some("pressure low"));
36//! assert_eq!(dashboard.next_event().await?, Some("pump started"));
37//! # Ok::<(), pamoja_core::Error>(())
38//! # }).unwrap();
39//! ```
40
41use pamoja_core::{EventBus, Result};
42use tokio::sync::broadcast;
43
44/// The largest buffer a bus sets aside; a larger capacity is lowered to it.
45///
46/// The buffer is allocated in full when the bus is made, so a mistaken capacity
47/// would otherwise cost its memory at once, or fail the allocation outright.
48pub const MAX_CAPACITY: usize = 1 << 20;
49
50/// Brings a requested capacity into the range a bus accepts.
51fn bounded(capacity: usize) -> usize {
52 capacity.clamp(1, MAX_CAPACITY)
53}
54
55/// A typed publish/subscribe bus that broadcasts each event to all subscribers.
56///
57/// Every handle can both publish and receive, and it receives what it publishes
58/// itself. Use [`subscribe`](BroadcastBus::subscribe) to add an independent
59/// consumer; an event published after a handle subscribes is delivered to it. A
60/// subscriber only sees events published after it subscribed, mirroring a live
61/// pub/sub channel.
62///
63/// Each handle holds a publisher of its own, so the bus stays open while any
64/// handle exists and [`next_event`](EventBus::next_event) on a `BroadcastBus`
65/// never returns `None`: it waits until an event arrives. A wait given up, by a
66/// timeout or a `select!`, takes no event.
67///
68/// # Examples
69///
70/// ```
71/// use pamoja_core::EventBus;
72/// use pamoja_bus::BroadcastBus;
73///
74/// # async fn run() -> pamoja_core::Result<()> {
75/// let bus = BroadcastBus::new(16);
76/// let mut subscriber = bus.subscribe();
77/// bus.publish("reading").await?;
78/// assert_eq!(subscriber.next_event().await?, Some("reading"));
79/// # Ok(())
80/// # }
81/// ```
82pub struct BroadcastBus<E> {
83 sender: broadcast::Sender<E>,
84 receiver: broadcast::Receiver<E>,
85 missed: u64,
86}
87
88impl<E: Clone> BroadcastBus<E> {
89 /// Creates a bus buffering unread events for each subscriber.
90 ///
91 /// # Arguments
92 ///
93 /// * `capacity` - the per-subscriber buffer depth, rounded up to the next
94 /// power of two; a subscriber further behind than the buffer holds drops
95 /// the events it missed. Values below one are raised to one, and values
96 /// above [`MAX_CAPACITY`] are lowered to it.
97 ///
98 /// # Returns
99 ///
100 /// A bus with one handle that can publish and receive.
101 pub fn new(capacity: usize) -> Self {
102 let (sender, receiver) = broadcast::channel(bounded(capacity));
103 Self {
104 sender,
105 receiver,
106 missed: 0,
107 }
108 }
109
110 /// Creates another handle to the same bus with its own independent subscription.
111 ///
112 /// # Returns
113 ///
114 /// A handle that receives events published after this call and can also publish.
115 pub fn subscribe(&self) -> Self {
116 Self {
117 sender: self.sender.clone(),
118 receiver: self.sender.subscribe(),
119 missed: 0,
120 }
121 }
122
123 /// Creates a publish-only handle to the same bus.
124 ///
125 /// # Returns
126 ///
127 /// A handle that publishes without borrowing this one, so it can announce from
128 /// another task while this handle waits in [`next_event`](EventBus::next_event).
129 pub fn publisher(&self) -> EventPublisher<E> {
130 EventPublisher {
131 sender: self.sender.clone(),
132 }
133 }
134
135 /// Counts the events this handle lost by falling behind.
136 ///
137 /// # Returns
138 ///
139 /// How many events were dropped from this handle's buffer before it read
140 /// them, since it subscribed.
141 pub fn missed(&self) -> u64 {
142 self.missed
143 }
144}
145
146impl<E: Clone> EventBus for BroadcastBus<E> {
147 type Event = E;
148
149 async fn publish(&self, event: Self::Event) -> Result<()> {
150 // `send` errors only when there are no receivers; this handle holds its
151 // own, so a publish always succeeds.
152 let _ = self.sender.send(event);
153 Ok(())
154 }
155
156 async fn next_event(&mut self) -> Result<Option<Self::Event>> {
157 loop {
158 match self.receiver.recv().await {
159 Ok(event) => return Ok(Some(event)),
160 Err(broadcast::error::RecvError::Closed) => return Ok(None),
161 Err(broadcast::error::RecvError::Lagged(skipped)) => {
162 self.missed = self.missed.saturating_add(skipped);
163 }
164 }
165 }
166 }
167}
168
169/// A publish-only handle to a bus.
170///
171/// A publisher has no buffer to fill, so a part that only announces holds one
172/// rather than a [`BroadcastBus`]. Cloning it is cheap, and publishing never
173/// waits: a subscriber that falls behind loses events rather than holding up the
174/// publisher.
175///
176/// # Examples
177///
178/// ```
179/// use pamoja_core::EventBus;
180/// use pamoja_bus::EventPublisher;
181///
182/// # async fn run() -> pamoja_core::Result<()> {
183/// let power = EventPublisher::new(8);
184/// let mut control = power.subscribe();
185/// let mut logger = power.subscribe();
186/// assert_eq!(power.publish("battery.low"), 2);
187/// assert_eq!(control.next_event().await?, Some("battery.low"));
188/// assert_eq!(logger.next_event().await?, Some("battery.low"));
189/// # Ok(())
190/// # }
191/// ```
192#[derive(Clone)]
193pub struct EventPublisher<E> {
194 sender: broadcast::Sender<E>,
195}
196
197impl<E: Clone> EventPublisher<E> {
198 /// Creates a bus with no subscribers yet, and a publisher on it.
199 ///
200 /// # Arguments
201 ///
202 /// * `capacity` - the per-subscriber buffer depth, rounded up to the next
203 /// power of two. Values below one are raised to one, and values above
204 /// [`MAX_CAPACITY`] are lowered to it.
205 ///
206 /// # Returns
207 ///
208 /// A publisher to take subscribers from and publish to them.
209 pub fn new(capacity: usize) -> Self {
210 let (sender, _) = broadcast::channel(bounded(capacity));
211 Self { sender }
212 }
213
214 /// Hands an event to every current subscriber.
215 ///
216 /// # Arguments
217 ///
218 /// * `event` - the event to broadcast.
219 ///
220 /// # Returns
221 ///
222 /// How many subscribers the event was handed to. An event published while no
223 /// one is subscribed is dropped, and the count is 0.
224 pub fn publish(&self, event: E) -> usize {
225 self.sender.send(event).unwrap_or(0)
226 }
227
228 /// Subscribes to the bus.
229 ///
230 /// # Returns
231 ///
232 /// A handle that receives events published after this call and can also publish.
233 pub fn subscribe(&self) -> BroadcastBus<E> {
234 BroadcastBus {
235 sender: self.sender.clone(),
236 receiver: self.sender.subscribe(),
237 missed: 0,
238 }
239 }
240
241 /// Takes another publisher on the same bus, for another part that announces.
242 ///
243 /// # Returns
244 ///
245 /// A publisher with a lifetime of its own, the same as a clone of this one.
246 pub fn publisher(&self) -> EventPublisher<E> {
247 self.clone()
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use std::time::Duration;
254
255 use super::*;
256
257 #[tokio::test]
258 async fn delivers_a_published_event() {
259 let mut bus = BroadcastBus::new(8);
260 bus.publish(1).await.expect("publish");
261 assert_eq!(bus.next_event().await.expect("next"), Some(1));
262 }
263
264 #[tokio::test]
265 async fn fans_out_to_every_subscriber() {
266 let bus = BroadcastBus::new(8);
267 let mut first = bus.subscribe();
268 let mut second = bus.subscribe();
269
270 bus.publish("event").await.expect("publish");
271
272 assert_eq!(first.next_event().await.expect("next"), Some("event"));
273 assert_eq!(second.next_event().await.expect("next"), Some("event"));
274 }
275
276 #[tokio::test]
277 async fn a_lagging_subscriber_skips_dropped_events_and_counts_them() {
278 let bus = BroadcastBus::new(2);
279 let mut subscriber = bus.subscribe();
280
281 for value in 0..5 {
282 bus.publish(value).await.expect("publish");
283 }
284
285 let mut seen = Vec::new();
286 seen.push(subscriber.next_event().await.expect("next").expect("event"));
287 seen.push(subscriber.next_event().await.expect("next").expect("event"));
288 assert_eq!(seen, vec![3, 4]);
289 assert_eq!(subscriber.missed(), 3);
290 }
291
292 #[tokio::test]
293 async fn the_capacity_is_rounded_up_to_a_power_of_two() {
294 let bus = BroadcastBus::new(3);
295 let mut subscriber = bus.subscribe();
296
297 for value in 0..5 {
298 bus.publish(value).await.expect("publish");
299 }
300
301 assert_eq!(subscriber.next_event().await.expect("next"), Some(1));
302 assert_eq!(subscriber.missed(), 1);
303 }
304
305 #[tokio::test]
306 async fn a_capacity_beyond_the_maximum_is_lowered_rather_than_allocated() {
307 let bus = BroadcastBus::new(usize::MAX);
308 let mut subscriber = bus.subscribe();
309
310 bus.publish(1u8).await.expect("publish");
311
312 assert_eq!(subscriber.next_event().await.expect("next"), Some(1));
313 assert_eq!(bounded(usize::MAX), MAX_CAPACITY);
314 assert_eq!(bounded(0), 1);
315 }
316
317 #[tokio::test]
318 async fn a_publisher_counts_the_subscribers_it_hands_an_event_to() {
319 let power = EventPublisher::new(8);
320 let mut control = power.subscribe();
321 let mut logger = power.subscribe();
322
323 assert_eq!(power.publish("battery.low"), 2);
324
325 assert_eq!(
326 control.next_event().await.expect("next"),
327 Some("battery.low")
328 );
329 assert_eq!(
330 logger.next_event().await.expect("next"),
331 Some("battery.low")
332 );
333 }
334
335 #[tokio::test]
336 async fn an_event_published_to_no_subscribers_is_dropped() {
337 let power = EventPublisher::new(8);
338 assert_eq!(power.publish("before"), 0);
339
340 let mut late = power.subscribe();
341 assert_eq!(power.publish("after"), 1);
342 assert_eq!(late.next_event().await.expect("next"), Some("after"));
343 }
344
345 #[tokio::test]
346 async fn a_publisher_announces_while_a_subscriber_waits() {
347 let mut subscriber = BroadcastBus::new(4);
348 let publisher = subscriber.publisher();
349
350 let waiting = tokio::spawn(async move { subscriber.next_event().await });
351 tokio::task::yield_now().await;
352
353 assert_eq!(publisher.publish(7), 1);
354 let event = waiting.await.expect("the waiting task").expect("next");
355 assert_eq!(event, Some(7));
356 }
357
358 #[tokio::test]
359 async fn the_bus_stays_open_while_a_subscriber_holds_it() {
360 let power: EventPublisher<u8> = EventPublisher::new(2);
361 let mut subscriber = power.subscribe();
362 drop(power);
363
364 let wait = tokio::time::timeout(Duration::from_millis(20), subscriber.next_event()).await;
365 assert!(wait.is_err(), "the wait ended with {wait:?}");
366 }
367}