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