proxima_primitives/sync/broadcast.rs
1//! `proxima::sync::broadcast` — multi-producer multi-consumer
2//! broadcast channel, shape-compatible with `tokio::sync::broadcast`.
3//! Backed by `async_broadcast` configured for overflow semantics:
4//! producers never block; if the buffer fills, the oldest message is
5//! dropped and the affected receivers see `RecvError::Lagged(skipped)`
6//! on their next `recv`.
7//!
8//! Differences from tokio:
9//! - `error::RecvError::Lagged(u64)` is named to match tokio's
10//! discriminant. (Underlying crate calls it `Overflowed`.)
11//! - `send` returns `Ok(subscriber_count)` matching tokio's signature.
12//!
13//! # Non-coverage
14//!
15//! - `Sender::send` returns `Ok(subscriber_count)` but the count is
16//! sampled at send time and is eventually-consistent with concurrent
17//! subscribe/drop operations. tokio's version has the same property
18//! but we call it out here because a sharp caller may rely on the
19//! exact-snapshot semantics that neither implementation provides.
20//! - `Receiver::resubscribe() -> Receiver<T>` — fresh subscription
21//! anchored at the current end of stream. Not exposed today; if
22//! needed, clone the `Sender` and call `subscribe()`.
23//! - `Receiver::len()` / `Receiver::is_empty()` — receiver-local
24//! buffer introspection. `async-broadcast` has equivalents but no
25//! internal caller plumbs them.
26//! - `Receiver::same_channel(&other)` — identity check.
27//! - `Sender::receiver_count()` IS exposed but the value is a
28//! snapshot; we expose it because `recording/broadcast.rs` uses it
29//! to decide whether to bother broadcasting (best-effort).
30//! - `Sender::weak_count()` and weak-sender plumbing — no caller.
31
32use async_broadcast::{InactiveReceiver, Sender as InnerSender};
33
34pub mod error {
35 /// Error returned from [`super::Receiver::recv`] when the channel
36 /// is no longer producing values, or when this receiver skipped
37 /// past some messages because it was too slow.
38 #[derive(Debug, thiserror::Error)]
39 pub enum RecvError {
40 /// All senders have been dropped and the buffer is drained.
41 #[error("broadcast channel closed")]
42 Closed,
43 /// The buffer overflowed and this receiver skipped `n` messages.
44 /// The next `recv` will return the next available value.
45 #[error("broadcast receiver lagged behind by {0} messages")]
46 Lagged(u64),
47 }
48}
49
50/// Error returned from [`Sender::send`] when no receivers are
51/// listening. The value is returned so callers can retry on a fresh
52/// channel.
53#[derive(Debug, thiserror::Error)]
54#[error("broadcast channel has no receivers")]
55pub struct SendError<T>(pub T);
56
57/// Producer side. Cheaply cloneable.
58#[derive(Debug)]
59pub struct Sender<T: Clone> {
60 inner: InnerSender<T>,
61 /// Kept alive so the channel stays open while at least one Sender
62 /// exists, even if every Receiver has been dropped. Mirrors
63 /// tokio's "channel stays open until all Senders drop" rule.
64 _liveness: InactiveReceiver<T>,
65}
66
67impl<T: Clone> Clone for Sender<T> {
68 fn clone(&self) -> Self {
69 Self {
70 inner: self.inner.clone(),
71 _liveness: self._liveness.clone(),
72 }
73 }
74}
75
76impl<T: Clone> Sender<T> {
77 /// Send a value to every active receiver. Returns the subscriber
78 /// count at the time of the send. Returns `SendError(value)` if
79 /// no receivers are listening.
80 pub fn send(&self, value: T) -> Result<usize, SendError<T>> {
81 if self.inner.receiver_count() == 0 {
82 return Err(SendError(value));
83 }
84 match self.inner.try_broadcast(value) {
85 Ok(_displaced) => Ok(self.inner.receiver_count()),
86 Err(async_broadcast::TrySendError::Closed(value)) => Err(SendError(value)),
87 Err(async_broadcast::TrySendError::Inactive(value)) => Err(SendError(value)),
88 Err(async_broadcast::TrySendError::Full(value)) => {
89 // overflow mode is set in `channel()`, so Full should
90 // not occur; treat as logic error returning value back.
91 Err(SendError(value))
92 }
93 }
94 }
95
96 /// Build a fresh receiver subscribed at the current end of the
97 /// stream. Messages broadcast BEFORE this call are not delivered
98 /// to the new receiver.
99 pub fn subscribe(&self) -> Receiver<T> {
100 Receiver {
101 inner: self.inner.new_receiver(),
102 }
103 }
104
105 /// Returns the number of currently-active receivers.
106 pub fn receiver_count(&self) -> usize {
107 self.inner.receiver_count()
108 }
109}
110
111/// Consumer side. Each receiver has its own cursor across the buffer.
112#[derive(Debug)]
113pub struct Receiver<T: Clone> {
114 inner: async_broadcast::Receiver<T>,
115}
116
117impl<T: Clone> Receiver<T> {
118 /// Await the next message. Returns `Closed` when all senders are
119 /// dropped and the buffer is drained; `Lagged(n)` if the receiver
120 /// skipped `n` messages due to buffer overflow.
121 pub async fn recv(&mut self) -> Result<T, error::RecvError> {
122 match self.inner.recv().await {
123 Ok(value) => Ok(value),
124 Err(async_broadcast::RecvError::Closed) => Err(error::RecvError::Closed),
125 Err(async_broadcast::RecvError::Overflowed(skipped)) => {
126 Err(error::RecvError::Lagged(skipped))
127 }
128 }
129 }
130}
131
132impl<T: Clone> Clone for Receiver<T> {
133 fn clone(&self) -> Self {
134 Self {
135 inner: self.inner.clone(),
136 }
137 }
138}
139
140/// Build a broadcast channel with `capacity` slots. Producers never
141/// block on a full buffer; the oldest message is dropped and the
142/// affected receivers observe `Lagged` on their next `recv`.
143pub fn channel<T: Clone>(capacity: usize) -> (Sender<T>, Receiver<T>) {
144 let (mut sender, receiver) = async_broadcast::broadcast(capacity.max(1));
145 sender.set_overflow(true);
146 sender.set_await_active(false);
147 let liveness = receiver.clone().deactivate();
148 (
149 Sender {
150 inner: sender,
151 _liveness: liveness,
152 },
153 Receiver { inner: receiver },
154 )
155}
156
157// under `--cfg loom`, `async_broadcast` becomes loom-instrumented too (it
158// depends on `event_listener`) and requires every call inside a
159// `loom::model()`; these plain tests are unrelated to the Notify/watch
160// loom protocol this crate loom-tests, so they're normal (non-loom)
161// build only.
162#[cfg(all(test, not(loom)))]
163#[allow(clippy::unwrap_used, clippy::expect_used)]
164mod tests {
165 use super::*;
166 use futures::executor::block_on;
167
168 #[test]
169 fn send_to_active_receiver_roundtrips() {
170 let (sender, mut receiver) = channel::<u32>(4);
171 block_on(async move {
172 sender.send(7).expect("send");
173 assert_eq!(receiver.recv().await.expect("recv"), 7);
174 });
175 }
176
177 #[test]
178 fn second_subscriber_only_sees_messages_sent_after_subscribe() {
179 let (sender, mut alpha) = channel::<u32>(4);
180 sender.send(1).expect("send 1");
181 let mut beta = sender.subscribe();
182 sender.send(2).expect("send 2");
183 block_on(async move {
184 assert_eq!(alpha.recv().await.expect("alpha first"), 1);
185 assert_eq!(alpha.recv().await.expect("alpha second"), 2);
186 assert_eq!(beta.recv().await.expect("beta"), 2);
187 });
188 }
189
190 #[test]
191 fn send_succeeds_when_no_receivers_but_inactive_holds_open() {
192 let (sender, receiver) = channel::<u32>(4);
193 drop(receiver);
194 // No active receivers; send returns SendError so callers can recover.
195 match sender.send(1) {
196 Err(SendError(value)) => assert_eq!(value, 1),
197 Ok(_) => panic!("expected SendError"),
198 }
199 }
200
201 #[test]
202 fn overflow_yields_lagged_on_recv() {
203 let (sender, mut receiver) = channel::<u32>(2);
204 sender.send(1).expect("send 1");
205 sender.send(2).expect("send 2");
206 sender.send(3).expect("send 3 (overflows 1)");
207 block_on(async move {
208 match receiver.recv().await {
209 Err(error::RecvError::Lagged(skipped)) => assert!(skipped >= 1),
210 other => panic!("expected Lagged, got {other:?}"),
211 }
212 });
213 }
214}