Skip to main content

rskit_stream/
broadcaster.rs

1//! Bounded fan-out broadcaster source.
2//!
3//! [`Broadcaster<T>`] turns "observe a backend" into a *bounded,
4//! cancellable stream of typed events* fanned out to many independent subscribers.
5//! Each subscriber owns a private bounded channel:
6//! a subscriber that falls further behind than the buffer loses interim events (backpressure by drop)
7//! but never blocks the broadcaster or its peers.
8//! Disconnected subscribers are pruned lazily on the next broadcast.
9//!
10//! This is the canonical owner for the "watch a source → typed change stream" shape that recurs across config reloads,
11//! service discovery, cache invalidation, and secret rotation.
12//! Consumers that must not miss events should treat any received item as a signal to re-run their full load pipeline.
13
14use std::fmt;
15use std::pin::Pin;
16use std::sync::Arc;
17
18use futures::Stream;
19use parking_lot::Mutex;
20use tokio::sync::mpsc;
21use tokio_stream::wrappers::ReceiverStream;
22use tokio_util::sync::CancellationToken;
23
24/// A bounded, owned, cancellable stream of broadcast events.
25///
26/// Yielded by [`Broadcaster::subscribe`]; terminates when the originating `CancellationToken` fires
27/// or the broadcaster is dropped.
28pub type BroadcastStream<T> = Pin<Box<dyn Stream<Item = T> + Send + 'static>>;
29
30/// Default per-subscriber buffer used by [`Broadcaster::new`].
31///
32/// A subscriber lagging more than this many unconsumed events drops the overflow rather than stalling the broadcaster.
33pub const DEFAULT_BROADCAST_BUFFER: usize = 64;
34
35/// A bounded, cancellable fan-out broadcaster.
36///
37/// Cheaply cloneable; every clone shares the same subscriber set,
38/// so an event broadcast through any clone reaches all live subscribers.
39/// `T` must be `Clone` (each subscriber receives its own copy)
40/// and `Send + 'static` to cross the per-subscriber channel.
41pub struct Broadcaster<T> {
42    senders: Arc<Mutex<Vec<mpsc::Sender<T>>>>,
43    buffer: usize,
44}
45
46impl<T> Clone for Broadcaster<T> {
47    fn clone(&self) -> Self {
48        Self {
49            senders: Arc::clone(&self.senders),
50            buffer: self.buffer,
51        }
52    }
53}
54
55impl<T> fmt::Debug for Broadcaster<T> {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.debug_struct("Broadcaster")
58            .field("buffer", &self.buffer)
59            .field("subscribers", &self.subscriber_count())
60            .finish_non_exhaustive()
61    }
62}
63
64impl<T> Default for Broadcaster<T>
65where
66    T: Clone + Send + 'static,
67{
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl<T> Broadcaster<T>
74where
75    T: Clone + Send + 'static,
76{
77    /// Create a broadcaster with the [`DEFAULT_BROADCAST_BUFFER`] per-subscriber buffer.
78    #[must_use]
79    pub fn new() -> Self {
80        Self::with_buffer(DEFAULT_BROADCAST_BUFFER)
81    }
82
83    /// Create a broadcaster with an explicit per-subscriber buffer.
84    ///
85    /// A `buffer` of zero is clamped to one so every subscriber can hold at least one in-flight event.
86    #[must_use]
87    pub fn with_buffer(buffer: usize) -> Self {
88        Self {
89            senders: Arc::new(Mutex::new(Vec::new())),
90            buffer: buffer.max(1),
91        }
92    }
93}
94
95impl<T> Broadcaster<T> {
96    /// The per-subscriber buffer capacity.
97    #[must_use]
98    pub const fn buffer(&self) -> usize {
99        self.buffer
100    }
101
102    /// The number of currently registered subscribers.
103    ///
104    /// Disconnected subscribers are pruned lazily on [`broadcast`](Self::broadcast),
105    /// so this count may transiently include subscribers whose streams have been dropped but not
106    /// yet pruned.
107    #[must_use]
108    pub fn subscriber_count(&self) -> usize {
109        self.senders.lock().len()
110    }
111
112    /// Register a new subscriber and return its bounded, cancellable stream.
113    ///
114    /// The returned stream yields events broadcast after subscription and terminates once `cancel` fires
115    /// or the broadcaster is dropped.
116    /// Pass a fresh `CancellationToken` when no external cancellation is required.
117    pub fn subscribe(&self, cancel: CancellationToken) -> BroadcastStream<T>
118    where
119        T: Send + 'static,
120    {
121        use futures::StreamExt as _;
122
123        let (tx, rx) = mpsc::channel(self.buffer);
124        self.senders.lock().push(tx);
125        Box::pin(ReceiverStream::new(rx).take_until(cancel.cancelled_owned()))
126    }
127
128    /// Fan `item` out to every live subscriber.
129    ///
130    /// Each subscriber receives an independent clone. A subscriber whose buffer is full is retained
131    /// but skips this event (bounded drop backpressure);
132    /// a subscriber whose stream has been dropped is pruned.
133    pub fn broadcast(&self, item: &T)
134    where
135        T: Clone,
136    {
137        self.senders
138            .lock()
139            .retain(|tx| match tx.try_send(item.clone()) {
140                Ok(()) | Err(mpsc::error::TrySendError::Full(_)) => true,
141                Err(mpsc::error::TrySendError::Closed(_)) => false,
142            });
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use futures::StreamExt as _;
150
151    #[tokio::test]
152    async fn delivers_to_all_subscribers() {
153        let bc = Broadcaster::<u32>::new();
154        let cancel = CancellationToken::new();
155        let mut a = bc.subscribe(cancel.clone());
156        let mut b = bc.subscribe(cancel.clone());
157
158        bc.broadcast(&7);
159
160        assert_eq!(a.next().await, Some(7));
161        assert_eq!(b.next().await, Some(7));
162    }
163
164    #[tokio::test]
165    async fn stream_terminates_on_cancel() {
166        let bc = Broadcaster::<u32>::new();
167        let cancel = CancellationToken::new();
168        let mut sub = bc.subscribe(cancel.clone());
169
170        bc.broadcast(&1);
171        assert_eq!(sub.next().await, Some(1));
172
173        cancel.cancel();
174        assert_eq!(sub.next().await, None);
175    }
176
177    #[tokio::test]
178    async fn dropped_subscriber_is_pruned() {
179        let bc = Broadcaster::<u32>::new();
180        let cancel = CancellationToken::new();
181        let sub = bc.subscribe(cancel);
182        assert_eq!(bc.subscriber_count(), 1);
183
184        drop(sub);
185        // Prune happens lazily on the next broadcast.
186        bc.broadcast(&1);
187        assert_eq!(bc.subscriber_count(), 0);
188    }
189
190    #[tokio::test]
191    async fn full_subscriber_drops_overflow_without_blocking() {
192        let bc = Broadcaster::<u32>::with_buffer(2);
193        let cancel = CancellationToken::new();
194        let mut sub = bc.subscribe(cancel.clone());
195
196        // Send more than the buffer; overflow is dropped, not blocked.
197        for i in 0..5 {
198            bc.broadcast(&i);
199        }
200
201        let mut received = Vec::new();
202        // Only the first `buffer` events are retained.
203        while let Ok(Some(v)) =
204            tokio::time::timeout(std::time::Duration::from_millis(50), sub.next()).await
205        {
206            received.push(v);
207        }
208        assert_eq!(received, vec![0, 1]);
209    }
210
211    #[tokio::test]
212    async fn zero_buffer_is_clamped_to_one() {
213        let bc = Broadcaster::<u32>::with_buffer(0);
214        assert_eq!(bc.buffer(), 1);
215    }
216
217    #[tokio::test]
218    async fn clones_share_subscriber_set() {
219        let bc = Broadcaster::<u32>::new();
220        let cancel = CancellationToken::new();
221        let mut sub = bc.subscribe(cancel.clone());
222
223        let clone = bc.clone();
224        clone.broadcast(&42);
225
226        assert_eq!(sub.next().await, Some(42));
227    }
228
229    #[test]
230    fn default_and_debug_report_buffer_and_subscribers() {
231        let bc = Broadcaster::<u32>::default();
232
233        assert_eq!(bc.buffer(), DEFAULT_BROADCAST_BUFFER);
234        assert!(format!("{bc:?}").contains("Broadcaster"));
235    }
236}