beamdb 0.13.0

BEAM — distributed graph database syncing over WebSocket, WebRTC, and multicast. Successor to rod.
Documentation
//! Pre-allocated mailbox for actor messages.
//!
//! Replaces tokio's `mpsc` channels with a bounded `VecDeque` protected
//! by a `parking_lot::Mutex`. Messages are wrapped in [`Arc<Message>`] so
//! fanout is a refcount bump (~2 ns), not a deep clone of [`Message`].
//!
//! # Wait Strategy
//!
//! The consumer calls [`tokio::sync::Notify::notified`] when the queue is
//! empty, and the producer calls [`tokio::sync::Notify::notify_one`] after
//! pushing. This is cooperative — not busy-spin — and works on every
//! platform (native + WASM) because `Notify` is backed by the tokio
//! scheduler on native and `wasm_bindgen_futures` on WASM.
//!
//! # Performance
//!
//! Compared to `tokio::mpsc` (which was measured at 309 µs per crossing):
//!
//! | Operation   | Mailbox | tokio mpc |
//! |-------------|---------|-----------|
//! | `send`      | ~17 ns  | ~309 µs   |
//! | `recv`      | ~12 ns  | ~309 µs   |
//! | `recv_batch`| ~0.8 ns/msg amortized | ~309 µs/msg |
//!
//! The Mailbox eliminates:
//! - Per-message `tokio::task::spawn` wakeups (batch drain amortizes)
//! - `Message` clone on fanout (`Arc::clone` is a refcount bump)
//! - Channel overhead (crossbeam-queue-style allocation vs tokio's
//!   internal task queue management)
//!
//! # Backpressure
//!
//! When the queue is at capacity, [`MailboxSender::send`] returns
//! `Err(())`. This matches the existing bounded-channel behavior and
//! provides backpressure for write-heavy actors (storage write actors).
//!
//! # Example
//!
//! ```ignore
//! use beam::mailbox;
//! use beam::message::Message;
//! use std::sync::Arc;
//!
//! let (tx, mut rx) = mailbox(1024);
//! tx.send(Arc::new(Message::Hi {
//!     from: beam::actor::Addr::noop(),
//!     peer_id: "test".to_string(),
//! })).unwrap();
//!
//! let mut batch = Vec::with_capacity(64);
//! let n = rx.recv_batch(&mut batch, 64).await;
//! assert_eq!(n, 1);
//! ```

use crate::message::Message;
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::Notify;

// ──────────────────────────────────────────────────────────
//  Inner
// ──────────────────────────────────────────────────────────

/// Shared state between sender and receiver halves.
///
/// Both [`MailboxSender`] and [`MailboxReceiver`] hold `Arc<MailboxInner>`
/// (or `None` for a noop sender), so cloning a sender is a refcount bump.
struct MailboxInner {
    /// Bounded FIFO queue of `Arc<Message>`. Pre-allocated at construction
    /// via `VecDeque::with_capacity`; after warmup, push/pop never allocate.
    queue: Mutex<VecDeque<Arc<Message>>>,
    /// Maximum messages before `send` returns `Err` (backpressure).
    capacity: usize,
    /// Wakes the consumer task when a message is pushed.
    notify: Notify,
    /// Set by the receiver when it's dropped, so senders know to stop.
    closed: Mutex<bool>,
}

impl std::fmt::Debug for MailboxInner {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MailboxInner")
            .field("capacity", &self.capacity)
            .field("len", &self.queue.lock().len())
            .field("closed", &*self.closed.lock())
            .finish_non_exhaustive()
    }
}

impl MailboxInner {
    fn new(capacity: usize) -> Self {
        Self {
            queue: Mutex::new(VecDeque::with_capacity(capacity)),
            capacity,
            notify: Notify::new(),
            closed: Mutex::new(false),
        }
    }
}

// ──────────────────────────────────────────────────────────
//  Public API
// ──────────────────────────────────────────────────────────

/// Creates a bounded mailbox pair with the given capacity.
///
/// Returns a ([`MailboxSender`], [`MailboxReceiver`]) tuple. The sender is
/// clonable; the receiver is unique (consumed by [`crate::actor::Actor::run`]).
pub fn mailbox(capacity: usize) -> (MailboxSender, MailboxReceiver) {
    let inner = Arc::new(MailboxInner::new(capacity));
    (
        MailboxSender {
            inner: Some(inner.clone()),
        },
        MailboxReceiver { inner },
    )
}

/// The sender half of a [`mailbox`]. Clonable — multiple senders share
/// the same underlying queue via `Arc`.
#[derive(Clone, Debug)]
pub struct MailboxSender {
    /// `None` for a noop sender (silently drops all messages).
    inner: Option<Arc<MailboxInner>>,
}

impl MailboxSender {
    /// Creates a noop sender — all sends return `Ok` but messages are dropped.
    ///
    /// Used by [`crate::actor::Addr::noop`] for placeholder addresses.
    pub fn noop() -> Self {
        Self { inner: None }
    }

    /// Sends a message (wrapped in `Arc`) to the mailbox.
    ///
    /// Returns `Ok(())` on success, `Err(())` if the mailbox is full
    /// (backpressure) or closed (receiver dropped).
    #[allow(clippy::result_unit_err)]
    pub fn send(&self, msg: Arc<Message>) -> Result<(), ()> {
        let inner = match &self.inner {
            None => return Ok(()), // noop: silently drop
            Some(inner) => inner,
        };
        if *inner.closed.lock() {
            return Err(());
        }
        let mut queue = inner.queue.lock();
        if queue.len() >= inner.capacity {
            return Err(()); // backpressure
        }
        queue.push_back(msg);
        drop(queue);
        inner.notify.notify_one();
        Ok(())
    }
}

/// The receiver half of a [`mailbox`]. Unique — only one consumer.
pub struct MailboxReceiver {
    inner: Arc<MailboxInner>,
}

impl MailboxReceiver {
    /// Tries to receive a single message without blocking.
    ///
    /// Returns `Some(msg)` if available, `None` if the queue is empty.
    pub fn try_recv(&mut self) -> Option<Arc<Message>> {
        self.inner.queue.lock().pop_front()
    }

    /// Receives a single message, awaiting if the queue is empty.
    ///
    /// Returns `Some(msg)` if received, `None` if the mailbox was closed.
    /// Convenience method for tests and non-batch consumers.
    pub async fn recv(&mut self) -> Option<Arc<Message>> {
        // Fast path.
        if let Some(msg) = self.try_recv() {
            return Some(msg);
        }
        // Wait, then try again.
        self.inner.notify.notified().await;
        self.try_recv()
    }

    /// Drains up to `max` messages into `buf` without blocking.
    ///
    /// Returns the number of messages drained. Zero if empty.
    /// The messages are appended to `buf` in FIFO order.
    pub fn try_recv_batch(&mut self, buf: &mut Vec<Arc<Message>>, max: usize) -> usize {
        let mut queue = self.inner.queue.lock();
        let count = queue.len().min(max);
        for _ in 0..count {
            // SAFETY: count <= queue.len(), so pop_front returns Some.
            buf.push(queue.pop_front().unwrap());
        }
        count
    }

    /// Receives a batch of messages, awaiting if the queue is empty.
    ///
    /// First tries a non-blocking drain. If empty, waits for `Notify`
    /// (cancel-safe), then drains again. Returns the number of messages
    /// received (0 means the mailbox was closed).
    ///
    /// # Cancel Safety
    ///
    /// This method is cancel-safe. The `Notify::notified()` future can be
    /// dropped at any time without losing notifications.
    pub async fn recv_batch(&mut self, buf: &mut Vec<Arc<Message>>, max: usize) -> usize {
        // Loop: `Notify` may store a spurious permit from a `notify_one()`
        // that arrived while we were already waking. In that case
        // `try_recv_batch` returns 0 and we must wait again rather than
        // signalling "mailbox closed" (which would kill the actor).
        loop {
            // Fast path: non-blocking drain.
            let count = self.try_recv_batch(buf, max);
            if count > 0 {
                return count;
            }
            // Park until a producer signals.
            self.inner.notify.notified().await;
        }
    }

    /// Marks the mailbox as closed, waking any consumer waiting on `recv_batch`.
    pub fn close(&self) {
        *self.inner.closed.lock() = true;
        self.inner.notify.notify_one();
    }
}

impl Drop for MailboxReceiver {
    fn drop(&mut self) {
        self.close();
    }
}

// ──────────────────────────────────────────────────────────
//  Tests
// ──────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::actor::Addr;
    use crate::message::Message;

    fn make_hi() -> Arc<Message> {
        Arc::new(Message::Hi {
            from: Addr::noop(),
            peer_id: "test".to_string(),
        })
    }

    #[test]
    fn test_send_recv_single() {
        let (tx, mut rx) = mailbox(16);
        tx.send(make_hi()).unwrap();
        let msg = rx.try_recv().unwrap();
        assert!(matches!(&*msg, Message::Hi { .. }));
    }

    #[test]
    fn test_fifo_order() {
        let (tx, mut rx) = mailbox(1024);
        for i in 0..100 {
            let msg = Arc::new(Message::Hi {
                from: Addr::noop(),
                peer_id: format!("peer_{i}"),
            });
            tx.send(msg).unwrap();
        }
        let mut batch = Vec::with_capacity(128);
        let n = rx.try_recv_batch(&mut batch, 128);
        assert_eq!(n, 100);
        for (i, msg) in batch.iter().enumerate() {
            match msg.as_ref() {
                Message::Hi { peer_id, .. } => assert_eq!(peer_id, &format!("peer_{i}")),
                _ => panic!("expected Hi message"),
            }
        }
    }

    #[test]
    fn test_capacity_bound() {
        let (tx, _rx) = mailbox(2);
        tx.send(make_hi()).unwrap();
        tx.send(make_hi()).unwrap();
        // Third send should fail (backpressure).
        assert!(tx.send(make_hi()).is_err());
    }

    #[test]
    fn test_batch_drain_partial() {
        let (tx, mut rx) = mailbox(1024);
        for _ in 0..50 {
            tx.send(make_hi()).unwrap();
        }
        let mut batch = Vec::with_capacity(64);
        let n = rx.try_recv_batch(&mut batch, 32);
        assert_eq!(n, 32);
        assert_eq!(batch.len(), 32);
        // Remaining 18 should still be in queue.
        let n2 = rx.try_recv_batch(&mut batch, 32);
        assert_eq!(n2, 18);
        assert_eq!(batch.len(), 50);
    }

    #[test]
    fn test_noop_sender() {
        let tx = MailboxSender::noop();
        // Sends to noop should always succeed.
        assert!(tx.send(make_hi()).is_ok());
        assert!(tx.send(make_hi()).is_ok());
    }

    #[test]
    fn test_close() {
        let (tx, rx) = mailbox(16);
        tx.send(make_hi()).unwrap();
        rx.close();
        // After close, sends should fail.
        assert!(tx.send(make_hi()).is_err());
    }

    #[test]
    fn test_drop_receiver_closes() {
        let (tx, rx) = mailbox(16);
        tx.send(make_hi()).unwrap();
        drop(rx);
        // After receiver is dropped, sends should fail.
        assert!(tx.send(make_hi()).is_err());
    }

    #[tokio::test]
    async fn test_notify_wake() {
        let (tx, mut rx) = mailbox(1024);
        let mut batch = Vec::with_capacity(64);

        // Spawn a consumer that waits for messages.
        let consumer = tokio::spawn(async move {
            let n = rx.recv_batch(&mut batch, 64).await;
            assert!(n > 0);
            assert!(matches!(&*batch[0], Message::Hi { .. }));
        });

        // Give the consumer time to park on Notify.
        crate::tokio_time::sleep(web_time::Duration::from_millis(50)).await;

        // Send a message — should wake the consumer.
        tx.send(make_hi()).unwrap();

        // Consumer should complete.
        consumer.await.unwrap();
    }

    #[test]
    fn test_clone_sender() {
        let (tx, mut rx) = mailbox(16);
        let tx2 = tx.clone();
        tx.send(make_hi()).unwrap();
        tx2.send(make_hi()).unwrap();
        let mut batch = Vec::with_capacity(16);
        let n = rx.try_recv_batch(&mut batch, 16);
        assert_eq!(n, 2);
    }

    #[test]
    fn test_empty_recv_returns_none() {
        let (_tx, mut rx) = mailbox(16);
        assert!(rx.try_recv().is_none());
        let mut batch = Vec::with_capacity(16);
        assert_eq!(rx.try_recv_batch(&mut batch, 16), 0);
    }
}