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