Skip to main content

proxima_primitives/sync/
mpsc.rs

1//! `proxima::sync::mpsc` — multi-producer single-consumer channel,
2//! shape-compatible with `tokio::sync::mpsc`. Backed by `async-channel`
3//! because tokio's `Sender::send(&self, T)` shape — async send without
4//! a `&mut` borrow — requires interior synchronization that
5//! `async-channel` provides directly. `futures::channel::mpsc`'s
6//! capacity-per-clone semantics inflate buffers unexpectedly when used
7//! the same way, so we route through `async-channel` instead.
8//!
9//! Two flavors:
10//! - [`channel`]/[`Sender`]/[`Receiver`] — bounded.
11//! - [`unbounded_channel`]/[`UnboundedSender`]/[`UnboundedReceiver`] — unbounded.
12//!
13//! Differences from tokio:
14//! - [`TrySendError`] is split into `Full(T)` / `Closed(T)` rather than
15//!   tokio's enum with the same discriminants; existing callers that
16//!   match on variants port verbatim.
17
18//!
19//! # Non-coverage
20//!
21//! The following tokio APIs are intentionally NOT shimmed today. If a
22//! caller appears, file a request — most are mechanical to add, just
23//! unused so far.
24//!
25//! **Bounded sender side**
26//! - `Sender::reserve() -> Permit` / `Sender::reserve_owned()` — slot
27//!   reservation without send. No internal caller uses the
28//!   reserve-then-send-later pattern; we expose `try_send` instead.
29//! - `Sender::send_timeout(value, duration)` — timeout-bounded send.
30//!   Compose with `proxima_core::time::timeout` at the call site.
31//! - `Sender::closed().await` — wait-until-receiver-dropped sender
32//!   primitive. Compose via [`Sender::is_closed`] polling or
33//!   subscribe to a separate shutdown signal.
34//! - `Sender::downgrade() -> WeakSender` and weak-sender plumbing.
35//!
36//! **Bounded receiver side**
37//! - `Receiver::try_recv() -> Result<T, TryRecvError>` — non-blocking
38//!   poll without await. Callers needing this pattern typically want
39//!   a [`crate::sync::watch`] instead.
40//! - `Receiver::poll_recv(&mut Context)` — manual poll for callers
41//!   embedding the receiver in a custom future. Wrap in a state
42//!   machine if needed.
43//! - `Receiver::recv_many(buf, limit)` — batch drain. Compose via a
44//!   `while let Some(value) = rx.recv().await` loop with a counter.
45//!
46//! **Introspection**
47//! - `Sender::capacity()` / `Sender::max_capacity()` — runtime
48//!   capacity introspection. `async-channel` exposes equivalents but
49//!   we have no caller plumbing them through.
50//! - `Sender::same_channel(&other)` — identity check across clones.
51//! - `Sender::weak_count()` / `Sender::strong_count()` — refcount
52//!   diagnostics.
53//!
54//! **Unbounded variant**
55//! - Mirrors the bounded gaps. `UnboundedReceiver::try_recv` and
56//!   `recv_many` likewise absent.
57
58#[cfg(feature = "std")]
59use futures::executor::block_on;
60
61/// Returned by [`Sender::send`] / [`UnboundedSender::send`] when the
62/// receiver has been dropped. The unsent value is returned so callers
63/// can recover or retry on a fresh channel.
64#[derive(Debug, thiserror::Error)]
65#[error("send on closed channel")]
66pub struct SendError<T>(pub T);
67
68impl<T> SendError<T> {
69    pub fn into_inner(self) -> T {
70        self.0
71    }
72}
73
74/// Returned by [`Sender::try_send`] when the channel cannot accept the
75/// value synchronously.
76#[derive(Debug, thiserror::Error)]
77pub enum TrySendError<T> {
78    /// Channel is at capacity; caller should retry or use `send`.
79    #[error("channel full")]
80    Full(T),
81    /// Receiver has been dropped; the channel is closed for good.
82    #[error("channel closed")]
83    Closed(T),
84}
85
86impl<T> TrySendError<T> {
87    pub fn into_inner(self) -> T {
88        match self {
89            Self::Full(value) | Self::Closed(value) => value,
90        }
91    }
92}
93
94/// Bounded sender. Clone-cheap (atomic refcount); multiple producers
95/// share the same channel buffer (no per-clone capacity inflation).
96#[derive(Debug)]
97pub struct Sender<T> {
98    inner: async_channel::Sender<T>,
99}
100
101impl<T> Clone for Sender<T> {
102    fn clone(&self) -> Self {
103        Self {
104            inner: self.inner.clone(),
105        }
106    }
107}
108
109impl<T> Sender<T> {
110    /// Send a value, awaiting channel capacity if full. Returns the
111    /// value back in [`SendError`] if the receiver has been dropped.
112    pub async fn send(&self, value: T) -> Result<(), SendError<T>> {
113        self.inner
114            .send(value)
115            .await
116            .map_err(|error| SendError(error.0))
117    }
118
119    /// Synchronous send attempt.
120    pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>> {
121        match self.inner.try_send(value) {
122            Ok(()) => Ok(()),
123            Err(async_channel::TrySendError::Full(value)) => Err(TrySendError::Full(value)),
124            Err(async_channel::TrySendError::Closed(value)) => Err(TrySendError::Closed(value)),
125        }
126    }
127
128    pub fn is_closed(&self) -> bool {
129        self.inner.is_closed()
130    }
131
132    /// Close the channel while allowing the receiver to drain accepted values.
133    pub fn close(&self) -> bool {
134        self.inner.close()
135    }
136}
137
138/// Bounded receiver. Drop it to close the channel for all senders.
139#[derive(Debug)]
140pub struct Receiver<T> {
141    inner: async_channel::Receiver<T>,
142}
143
144impl<T> Receiver<T> {
145    /// Await the next value, or `None` if all senders have been
146    /// dropped and the buffer is drained.
147    pub async fn recv(&mut self) -> Option<T> {
148        self.inner.recv().await.ok()
149    }
150
151    /// Wait for the next value on a dedicated synchronous worker.
152    #[cfg(all(feature = "std", not(target_family = "wasm")))]
153    pub fn recv_blocking(&self) -> Option<T> {
154        block_on(self.inner.recv()).ok()
155    }
156
157    pub fn close(&mut self) -> bool {
158        self.inner.close()
159    }
160}
161
162/// Build a bounded channel with the given buffer capacity.
163pub fn channel<T>(capacity: usize) -> (Sender<T>, Receiver<T>) {
164    let (sender, receiver) = async_channel::bounded(capacity.max(1));
165    (Sender { inner: sender }, Receiver { inner: receiver })
166}
167
168/// Unbounded sender. Send is sync because the channel never blocks on
169/// capacity.
170#[derive(Debug)]
171pub struct UnboundedSender<T> {
172    inner: async_channel::Sender<T>,
173}
174
175impl<T> Clone for UnboundedSender<T> {
176    fn clone(&self) -> Self {
177        Self {
178            inner: self.inner.clone(),
179        }
180    }
181}
182
183impl<T> UnboundedSender<T> {
184    /// Send a value. Always succeeds unless the receiver has been
185    /// dropped, in which case the value is returned via [`SendError`].
186    pub fn send(&self, value: T) -> Result<(), SendError<T>> {
187        match self.inner.try_send(value) {
188            Ok(()) => Ok(()),
189            Err(async_channel::TrySendError::Closed(value)) => Err(SendError(value)),
190            Err(async_channel::TrySendError::Full(_)) => {
191                unreachable!("unbounded channel reported full")
192            }
193        }
194    }
195
196    pub fn is_closed(&self) -> bool {
197        self.inner.is_closed()
198    }
199}
200
201/// Unbounded receiver. `recv` resolves to `None` once all senders are
202/// dropped and the buffer is drained.
203#[derive(Debug)]
204pub struct UnboundedReceiver<T> {
205    inner: async_channel::Receiver<T>,
206}
207
208impl<T> UnboundedReceiver<T> {
209    pub async fn recv(&mut self) -> Option<T> {
210        self.inner.recv().await.ok()
211    }
212
213    pub fn close(&mut self) -> bool {
214        self.inner.close()
215    }
216}
217
218/// Build an unbounded channel.
219pub fn unbounded_channel<T>() -> (UnboundedSender<T>, UnboundedReceiver<T>) {
220    let (sender, receiver) = async_channel::unbounded();
221    (
222        UnboundedSender { inner: sender },
223        UnboundedReceiver { inner: receiver },
224    )
225}
226
227// under `--cfg loom`, `async_channel` becomes loom-instrumented too (it
228// depends on `event_listener` / `concurrent-queue`) and requires every
229// call inside a `loom::model()`; these plain tests are unrelated to the
230// Notify/watch loom protocol this crate loom-tests, so they're normal
231// (non-loom) build only.
232#[cfg(all(test, not(loom)))]
233#[allow(clippy::unwrap_used, clippy::expect_used)]
234mod tests {
235    use super::*;
236    use futures::executor::block_on;
237
238    #[test]
239    fn bounded_send_recv_roundtrips() {
240        let (sender, mut receiver) = channel::<u32>(4);
241        block_on(async move {
242            sender.send(7).await.expect("send");
243            assert_eq!(receiver.recv().await, Some(7));
244        });
245    }
246
247    #[test]
248    fn bounded_try_send_returns_full_when_at_capacity() {
249        let (sender, _receiver) = channel::<u32>(1);
250        sender.try_send(1).expect("first slot");
251        match sender.try_send(2) {
252            Err(TrySendError::Full(value)) => assert_eq!(value, 2),
253            other => panic!("expected Full, got {other:?}"),
254        }
255    }
256
257    #[test]
258    fn bounded_send_returns_value_after_receiver_dropped() {
259        let (sender, receiver) = channel::<u32>(1);
260        drop(receiver);
261        block_on(async move {
262            match sender.send(9).await {
263                Err(error) => assert_eq!(error.into_inner(), 9),
264                Ok(()) => panic!("send to dropped receiver should error"),
265            }
266        });
267    }
268
269    #[test]
270    fn mpsc_sender_close_returns_pending_send_and_drains_accepted_values() {
271        let (sender, mut receiver) = channel::<u32>(1);
272        sender.try_send(11).expect("first value should be accepted");
273
274        block_on(async move {
275            let mut pending_send = core::pin::pin!(sender.send(22));
276            let first_poll = futures::future::poll_fn(|context| {
277                core::task::Poll::Ready(pending_send.as_mut().poll(context))
278            })
279            .await;
280            assert!(matches!(first_poll, core::task::Poll::Pending));
281
282            assert!(sender.close());
283            match pending_send.await {
284                Err(error) => assert_eq!(error.into_inner(), 22),
285                Ok(()) => panic!("close should return a send that was waiting for capacity"),
286            }
287            assert_eq!(receiver.recv().await, Some(11));
288            assert_eq!(receiver.recv().await, None);
289        });
290    }
291
292    #[test]
293    fn mpsc_receiver_blocking_drains_after_sender_close() {
294        let (sender, receiver) = channel::<u32>(1);
295        sender.try_send(31).expect("value should be accepted");
296        assert!(sender.close());
297        assert_eq!(receiver.recv_blocking(), Some(31));
298        assert_eq!(receiver.recv_blocking(), None);
299    }
300
301    #[test]
302    fn unbounded_send_recv_roundtrips() {
303        let (sender, mut receiver) = unbounded_channel::<u32>();
304        sender.send(42).expect("send");
305        block_on(async move {
306            assert_eq!(receiver.recv().await, Some(42));
307        });
308    }
309
310    #[test]
311    fn unbounded_send_returns_value_after_receiver_dropped() {
312        let (sender, receiver) = unbounded_channel::<u32>();
313        drop(receiver);
314        match sender.send(3) {
315            Err(error) => assert_eq!(error.into_inner(), 3),
316            Ok(()) => panic!("send to dropped receiver should error"),
317        }
318    }
319}