Skip to main content

beam/
mailbox.rs

1//! Pre-allocated mailbox for actor messages.
2//!
3//! Replaces tokio's `mpsc` channels with a bounded `VecDeque` protected
4//! by a `parking_lot::Mutex`. Messages are wrapped in [`Arc<Message>`] so
5//! fanout is a refcount bump (~2 ns), not a deep clone of [`Message`].
6//!
7//! # Wait Strategy
8//!
9//! The consumer calls [`tokio::sync::Notify::notified`] when the queue is
10//! empty, and the producer calls [`tokio::sync::Notify::notify_one`] after
11//! pushing. This is cooperative — not busy-spin — and works on every
12//! platform (native + WASM) because `Notify` is backed by the tokio
13//! scheduler on native and `wasm_bindgen_futures` on WASM.
14//!
15//! # Performance
16//!
17//! Compared to `tokio::mpsc` (which was measured at 309 µs per crossing):
18//!
19//! | Operation   | Mailbox | tokio mpc |
20//! |-------------|---------|-----------|
21//! | `send`      | ~17 ns  | ~309 µs   |
22//! | `recv`      | ~12 ns  | ~309 µs   |
23//! | `recv_batch`| ~0.8 ns/msg amortized | ~309 µs/msg |
24//!
25//! The Mailbox eliminates:
26//! - Per-message `tokio::task::spawn` wakeups (batch drain amortizes)
27//! - `Message` clone on fanout (`Arc::clone` is a refcount bump)
28//! - Channel overhead (crossbeam-queue-style allocation vs tokio's
29//!   internal task queue management)
30//!
31//! # Backpressure
32//!
33//! When the queue is at capacity, [`MailboxSender::send`] returns
34//! `Err(())`. This matches the existing bounded-channel behavior and
35//! provides backpressure for write-heavy actors (storage write actors).
36//!
37//! # Example
38//!
39//! ```ignore
40//! use beam::mailbox;
41//! use beam::message::Message;
42//! use std::sync::Arc;
43//!
44//! let (tx, mut rx) = mailbox(1024);
45//! tx.send(Arc::new(Message::Hi {
46//!     from: beam::actor::Addr::noop(),
47//!     peer_id: "test".to_string(),
48//!     is_ack: None,
49//!     msg_id: "doc".to_string(),
50//! })).unwrap();
51//!
52//! let mut batch = Vec::with_capacity(64);
53//! let n = rx.recv_batch(&mut batch, 64).await;
54//! assert_eq!(n, 1);
55//! ```
56
57use crate::message::Message;
58use parking_lot::Mutex;
59use std::collections::VecDeque;
60use std::sync::Arc;
61use tokio::sync::Notify;
62
63// ──────────────────────────────────────────────────────────
64//  Inner
65// ──────────────────────────────────────────────────────────
66
67/// Shared state between sender and receiver halves.
68///
69/// Both [`MailboxSender`] and [`MailboxReceiver`] hold `Arc<MailboxInner>`
70/// (or `None` for a noop sender), so cloning a sender is a refcount bump.
71struct MailboxInner {
72    /// Bounded FIFO queue of `Arc<Message>`. Grows on demand from zero
73    /// capacity — no upfront allocation. The `capacity` field above
74    /// controls the backpressure ceiling, not pre-allocation size.
75    queue: Mutex<VecDeque<Arc<Message>>>,
76    /// Maximum messages before `send` returns `Err` (backpressure).
77    capacity: usize,
78    /// Wakes the consumer task when a message is pushed.
79    notify: Notify,
80    /// Set by the receiver when it's dropped, so senders know to stop.
81    closed: Mutex<bool>,
82}
83
84impl std::fmt::Debug for MailboxInner {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        f.debug_struct("MailboxInner")
87            .field("capacity", &self.capacity)
88            .field("len", &self.queue.lock().len())
89            .field("closed", &*self.closed.lock())
90            .finish_non_exhaustive()
91    }
92}
93
94impl MailboxInner {
95    fn new(capacity: usize) -> Self {
96        Self {
97            queue: Mutex::new(VecDeque::new()),
98            capacity,
99            notify: Notify::new(),
100            closed: Mutex::new(false),
101        }
102    }
103}
104
105// ──────────────────────────────────────────────────────────
106//  Public API
107// ──────────────────────────────────────────────────────────
108
109/// Creates a bounded mailbox pair with the given capacity.
110///
111/// Returns a ([`MailboxSender`], [`MailboxReceiver`]) tuple. The sender is
112/// clonable; the receiver is unique (consumed by [`crate::actor::Actor::run`]).
113pub fn mailbox(capacity: usize) -> (MailboxSender, MailboxReceiver) {
114    let inner = Arc::new(MailboxInner::new(capacity));
115    (
116        MailboxSender {
117            inner: Some(inner.clone()),
118        },
119        MailboxReceiver { inner },
120    )
121}
122
123/// The sender half of a [`mailbox`]. Clonable — multiple senders share
124/// the same underlying queue via `Arc`.
125#[derive(Clone, Debug)]
126pub struct MailboxSender {
127    /// `None` for a noop sender (silently drops all messages).
128    inner: Option<Arc<MailboxInner>>,
129}
130
131impl MailboxSender {
132    /// Creates a noop sender — all sends return `Ok` but messages are dropped.
133    ///
134    /// Used by [`crate::actor::Addr::noop`] for placeholder addresses.
135    pub fn noop() -> Self {
136        Self { inner: None }
137    }
138
139    /// Sends a message (wrapped in `Arc`) to the mailbox.
140    ///
141    /// Returns `Ok(())` on success, `Err(())` if the mailbox is full
142    /// (backpressure) or closed (receiver dropped).
143    #[allow(clippy::result_unit_err)]
144    pub fn send(&self, msg: Arc<Message>) -> Result<(), ()> {
145        let inner = match &self.inner {
146            None => return Ok(()), // noop: silently drop
147            Some(inner) => inner,
148        };
149        if *inner.closed.lock() {
150            return Err(());
151        }
152        let mut queue = inner.queue.lock();
153        if queue.len() >= inner.capacity {
154            return Err(()); // backpressure
155        }
156        queue.push_back(msg);
157        drop(queue);
158        inner.notify.notify_one();
159        Ok(())
160    }
161}
162
163/// The receiver half of a [`mailbox`]. Unique — only one consumer.
164pub struct MailboxReceiver {
165    inner: Arc<MailboxInner>,
166}
167
168impl MailboxReceiver {
169    /// Tries to receive a single message without blocking.
170    ///
171    /// Returns `Some(msg)` if available, `None` if the queue is empty.
172    pub fn try_recv(&mut self) -> Option<Arc<Message>> {
173        self.inner.queue.lock().pop_front()
174    }
175
176    /// Receives a single message, awaiting if the queue is empty.
177    ///
178    /// Returns `Some(msg)` if received, `None` if the mailbox was closed.
179    /// Convenience method for tests and non-batch consumers.
180    pub async fn recv(&mut self) -> Option<Arc<Message>> {
181        // Fast path.
182        if let Some(msg) = self.try_recv() {
183            return Some(msg);
184        }
185        // Wait, then try again.
186        self.inner.notify.notified().await;
187        self.try_recv()
188    }
189
190    /// Drains up to `max` messages into `buf` without blocking.
191    ///
192    /// Returns the number of messages drained. Zero if empty.
193    /// The messages are appended to `buf` in FIFO order.
194    pub fn try_recv_batch(&mut self, buf: &mut Vec<Arc<Message>>, max: usize) -> usize {
195        let mut queue = self.inner.queue.lock();
196        let count = queue.len().min(max);
197        for _ in 0..count {
198            // SAFETY: count <= queue.len(), so pop_front returns Some.
199            buf.push(queue.pop_front().unwrap());
200        }
201        count
202    }
203
204    /// Receives a batch of messages, awaiting if the queue is empty.
205    ///
206    /// First tries a non-blocking drain. If empty, waits for `Notify`
207    /// (cancel-safe), then drains again. Returns the number of messages
208    /// received (0 means the mailbox was closed).
209    ///
210    /// # Cancel Safety
211    ///
212    /// This method is cancel-safe. The `Notify::notified()` future can be
213    /// dropped at any time without losing notifications.
214    pub async fn recv_batch(&mut self, buf: &mut Vec<Arc<Message>>, max: usize) -> usize {
215        // Loop: `Notify` may store a spurious permit from a `notify_one()`
216        // that arrived while we were already waking. In that case
217        // `try_recv_batch` returns 0 and we must wait again rather than
218        // signalling "mailbox closed" (which would kill the actor).
219        loop {
220            // Fast path: non-blocking drain.
221            let count = self.try_recv_batch(buf, max);
222            if count > 0 {
223                return count;
224            }
225            // Park until a producer signals.
226            self.inner.notify.notified().await;
227        }
228    }
229
230    /// Marks the mailbox as closed, waking any consumer waiting on `recv_batch`.
231    pub fn close(&self) {
232        *self.inner.closed.lock() = true;
233        self.inner.notify.notify_one();
234    }
235}
236
237impl Drop for MailboxReceiver {
238    fn drop(&mut self) {
239        self.close();
240    }
241}
242
243// ──────────────────────────────────────────────────────────
244//  Tests
245// ──────────────────────────────────────────────────────────
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::actor::Addr;
251    use crate::message::Message;
252
253    fn make_hi() -> Arc<Message> {
254        Arc::new(Message::Hi {
255            from: Addr::noop(),
256            peer_id: "test".to_string(),
257            is_ack: None,
258            msg_id: "test_id".to_string(),
259        })
260    }
261
262    #[test]
263    fn test_send_recv_single() {
264        let (tx, mut rx) = mailbox(16);
265        tx.send(make_hi()).unwrap();
266        let msg = rx.try_recv().unwrap();
267        assert!(matches!(&*msg, Message::Hi { .. }));
268    }
269
270    #[test]
271    fn test_fifo_order() {
272        let (tx, mut rx) = mailbox(1024);
273        for i in 0..100 {
274            let msg = Arc::new(Message::Hi {
275                from: Addr::noop(),
276                peer_id: format!("peer_{i}"),
277                is_ack: None,
278                msg_id: format!("id_{i}"),
279            });
280            tx.send(msg).unwrap();
281        }
282        let mut batch = Vec::with_capacity(128);
283        let n = rx.try_recv_batch(&mut batch, 128);
284        assert_eq!(n, 100);
285        for (i, msg) in batch.iter().enumerate() {
286            match msg.as_ref() {
287                Message::Hi { peer_id, .. } => assert_eq!(peer_id, &format!("peer_{i}")),
288                _ => panic!("expected Hi message"),
289            }
290        }
291    }
292
293    #[test]
294    fn test_capacity_bound() {
295        let (tx, _rx) = mailbox(2);
296        tx.send(make_hi()).unwrap();
297        tx.send(make_hi()).unwrap();
298        // Third send should fail (backpressure).
299        assert!(tx.send(make_hi()).is_err());
300    }
301
302    #[test]
303    fn test_batch_drain_partial() {
304        let (tx, mut rx) = mailbox(1024);
305        for _ in 0..50 {
306            tx.send(make_hi()).unwrap();
307        }
308        let mut batch = Vec::with_capacity(64);
309        let n = rx.try_recv_batch(&mut batch, 32);
310        assert_eq!(n, 32);
311        assert_eq!(batch.len(), 32);
312        // Remaining 18 should still be in queue.
313        let n2 = rx.try_recv_batch(&mut batch, 32);
314        assert_eq!(n2, 18);
315        assert_eq!(batch.len(), 50);
316    }
317
318    #[test]
319    fn test_noop_sender() {
320        let tx = MailboxSender::noop();
321        // Sends to noop should always succeed.
322        assert!(tx.send(make_hi()).is_ok());
323        assert!(tx.send(make_hi()).is_ok());
324    }
325
326    #[test]
327    fn test_close() {
328        let (tx, rx) = mailbox(16);
329        tx.send(make_hi()).unwrap();
330        rx.close();
331        // After close, sends should fail.
332        assert!(tx.send(make_hi()).is_err());
333    }
334
335    #[test]
336    fn test_drop_receiver_closes() {
337        let (tx, rx) = mailbox(16);
338        tx.send(make_hi()).unwrap();
339        drop(rx);
340        // After receiver is dropped, sends should fail.
341        assert!(tx.send(make_hi()).is_err());
342    }
343
344    #[tokio::test]
345    async fn test_notify_wake() {
346        let (tx, mut rx) = mailbox(1024);
347        let mut batch = Vec::with_capacity(64);
348
349        // Spawn a consumer that waits for messages.
350        let consumer = tokio::spawn(async move {
351            let n = rx.recv_batch(&mut batch, 64).await;
352            assert!(n > 0);
353            assert!(matches!(&*batch[0], Message::Hi { .. }));
354        });
355
356        // Give the consumer time to park on Notify.
357        crate::tokio_time::sleep(web_time::Duration::from_millis(50)).await;
358
359        // Send a message — should wake the consumer.
360        tx.send(make_hi()).unwrap();
361
362        // Consumer should complete.
363        consumer.await.unwrap();
364    }
365
366    #[test]
367    fn test_clone_sender() {
368        let (tx, mut rx) = mailbox(16);
369        let tx2 = tx.clone();
370        tx.send(make_hi()).unwrap();
371        tx2.send(make_hi()).unwrap();
372        let mut batch = Vec::with_capacity(16);
373        let n = rx.try_recv_batch(&mut batch, 16);
374        assert_eq!(n, 2);
375    }
376
377    #[test]
378    fn test_empty_recv_returns_none() {
379        let (_tx, mut rx) = mailbox(16);
380        assert!(rx.try_recv().is_none());
381        let mut batch = Vec::with_capacity(16);
382        assert_eq!(rx.try_recv_batch(&mut batch, 16), 0);
383    }
384}