Skip to main content

byteflow/scheduler/mailbox/
mod.rs

1//! Per-flow inbox: bounded hop queue + parked waiter (anti lost-wakeup).
2//!
3//! # Memory contract
4//!
5//! Unbounded `VecDeque` growth is not a capacity API — it is an OOM path
6//! when many flows share few workers. Every mailbox is constructed with a
7//! [`MailboxConfig`]: a validated [`MailboxCapacity`], a [`MailboxBytes`]
8//! budget, and an [`OverflowPolicy`]. Logical bound ≠ physical allocation;
9//! the queue grows geometrically up to the limit (see [`queue`]).
10//!
11//! Both bounds are load-bearing. A hop count alone stopped being a memory
12//! bound once hops could carry `Str` / `Bytes`: at the default capacity,
13//! 256 hops is ~12 KiB of scalars or ~256 MiB of 1 MiB blobs. Whichever
14//! bound is reached first refuses the hop, and [`MailboxFull::reason`] says
15//! which one it was.
16//!
17//! There is **no** `Block` policy. Blocking an OS worker on a full inbox
18//! would stall every other flow on that thread. Overflow is Reject /
19//! DropNewest / DropOldest. Scheduler-level [`Mailbox::park_sender`]
20//! (`WAITING_SEND`) parks the **sender flow** in this mailbox and wakes
21//! **one** waiter per freed slot (no wake storm).
22//!
23//! # Wake
24//!
25//! `park` and `push` share one mutex (lost-wakeup invariant — keep this
26//! comment and the race diagram). A hop that does not match a selective
27//! waiter is queued (if the bound allows) and the waiter stays parked.
28//! Overflow that **drops** a hop never produces [`Delivery::Handoff`].
29//!
30//! See `docs/mailbox.md`.
31
32mod capacity;
33mod metrics;
34mod policy;
35mod queue;
36
37use std::collections::VecDeque;
38use std::sync::Mutex;
39
40use crate::bytecode::Value;
41
42use super::error::RuntimeError;
43use super::process::{Flow, FlowId};
44use super::sync_lock;
45
46pub use capacity::{MailboxBytes, MailboxCapacity};
47pub use metrics::MailboxStats;
48pub use policy::{MailboxConfig, OverflowPolicy};
49
50use queue::{EnqueueEffect, MailboxQueue};
51
52/// Selective wait criterion installed while a flow is parked in its mailbox.
53///
54/// Used by classic `Receive`, `ReceiveMatch`, and `Ask`. Matching always
55/// **skips** (never drops) non-matching hops already in the queue.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) enum WaitFilter {
58    /// `Receive`: consume the oldest value regardless of contents.
59    Any,
60    /// `ReceiveMatch`: oldest `Message` whose application `tag` matches.
61    Tag(u16),
62    /// `Ask`: reply belonging to one specific request.
63    ///
64    /// `expect_request_id` is the RPC correlation key.
65    /// `expect_sender`, when present, additionally constrains reply origin.
66    /// Ask uses `Some(resolved_target_FlowId)` so a hop with the same
67    /// `request_id` from an unrelated flow cannot complete the RPC.
68    /// Compare against FlowId, never CapId (S2 after FlowCap).
69    Correlation {
70        expect_request_id: u64,
71        expect_sender: Option<u64>,
72    },
73}
74
75impl WaitFilter {
76    #[inline]
77    pub(crate) fn matches(&self, value: &Value) -> bool {
78        match *self {
79            Self::Any => true,
80            Self::Tag(expected_tag) => match value.as_message() {
81                Some(m) => m.tag == expected_tag,
82                None => false,
83            },
84            Self::Correlation {
85                expect_request_id,
86                expect_sender,
87            } => match value.as_message() {
88                Some(m) => {
89                    let id_ok = m.request_id == expect_request_id;
90                    let sender_ok = match expect_sender {
91                        Some(s) => m.sender == s,
92                        None => true,
93                    };
94                    id_ok && sender_ok
95                }
96                None => false,
97            },
98        }
99    }
100}
101
102/// A flow's inbox, plus (when the owning flow is blocked on
103/// `Receive` / `ReceiveMatch` / `Ask` with nothing to read) the parked flow itself.
104///
105/// # Why the flow lives *inside* its own mailbox while waiting
106///
107/// Internally, a flow is reachable through its [`FlowId`](crate::FlowId), which
108/// resolves (via the runtime's flow directory) to this `Mailbox`. Bytecode
109/// does **not** address by Pid anymore (FlowCap): `Send` / `Ask` resolve a
110/// Cap to a FlowId first, then look up here. Host [`crate::Runtime::send`]
111/// still uses FlowId directly (trusted).
112///
113/// Storing the blocked `Box<Flow>` directly in `MailboxInner::parked`, behind
114/// the same mutex that guards the message queue, turns "deliver a message and
115/// wake the receiver if it was waiting" into a single critical section — which
116/// is what actually prevents the classic lost-wakeup race:
117///
118/// ```text
119/// racing without a shared lock:
120///   receiver: queue.pop() -> None
121///   sender:   queue.push(msg); wake(receiver)   // receiver isn't parked yet!
122///   receiver: park()                            // ...and now sleeps forever
123///
124/// with both steps under one mutex (what this type does):
125///   receiver: lock; queue.pop() -> None; store self in `parked`; unlock
126///   sender:   lock; parked.take() -> Some(receiver); unlock; wake(receiver)
127/// ```
128/// Because "check the queue" and "become parked" happen atomically with
129/// respect to "push and check for a parked receiver", there is no window
130/// where a message can be pushed without either landing in the queue for a
131/// later `Receive` or immediately waking an already-parked one.
132///
133/// Wake only happens when a hop is **accepted and matches** the waiter.
134/// A flow that is already runnable (message queued, nobody parked) does
135/// not generate extra scheduler work — no wake storm on every `Send`.
136///
137/// # Selective wait (`ReceiveMatch` / `Ask`)
138///
139/// When parked with a selective (non-`Any`) filter, only a hop that
140/// satisfies the filter wakes the flow. Other hops are appended to the
141/// queue (subject to the bound) and the waiter stays parked (FIFO skip,
142/// never drop matching semantics).
143///
144/// # Bound
145///
146/// `push` may return [`MailboxFull`] when the policy is Reject and the
147/// logical capacity is already occupied **and** nobody matching is parked.
148/// A parked waiter that matches the hop still takes a **handoff** — that
149/// hop never occupies a queue slot.
150pub struct Mailbox {
151    inner: Mutex<MailboxInner>,
152    config: MailboxConfig,
153}
154
155struct MailboxInner {
156    queue: MailboxQueue,
157    parked: Option<Box<Flow>>,
158    /// Active while `parked` is `Some`. Ignored when nobody is waiting.
159    parked_filter: WaitFilter,
160    /// Bumped on every park install, so a deadline armed for an earlier
161    /// wait can be told apart from the current one. See [`WaitEpoch`].
162    wait_epoch: u64,
163    stats: MailboxStats,
164    /// Bytecode senders waiting for a free slot (`WAITING_SEND`).
165    /// Woken one-at-a-time from [`Mailbox::admit_waiting_sender`].
166    waiting_senders: VecDeque<WaitingSender>,
167    /// Set by [`Mailbox::close`] during finalize so a late `park_sender`
168    /// cannot land after `drain_waiting_senders` and leak the sender.
169    closed: bool,
170}
171
172struct WaitingSender {
173    flow: Box<Flow>,
174    message: Value,
175}
176
177/// Outcome of pushing a message.
178///
179/// `Queued*` means the hop (or a replacement under DropOldest) lives in
180/// the inbox for a later `Receive`. [`Handoff`](Delivery::Handoff) means a parked flow was
181/// waiting for **this** hop — the caller (`worker::deliver` /
182/// [`crate::Runtime::send`]) must `resume_with` and re-enqueue the flow.
183/// Dropped variants never wake a waiter.
184pub enum Delivery {
185    Queued,
186    QueuedDropOldest,
187    DroppedNewest,
188    Handoff(Box<Flow>),
189}
190
191/// Which of a mailbox's two bounds refused a hop.
192///
193/// Reported so an operator can tell "this flow is not draining its inbox"
194/// ([`Self::MessageLimit`]) from "this flow is being sent payloads too
195/// large for its budget" ([`Self::ByteLimit`]) without instrumenting the
196/// sender. Those have different fixes: raise capacity / speed up the
197/// receiver versus raise the byte budget / shrink the payload.
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum MailboxFullReason {
200    /// [`MailboxCapacity`] hops are already queued.
201    MessageLimit,
202    /// Accepting the hop would exceed the [`MailboxBytes`] budget. Also
203    /// reported when a single hop is larger than the entire budget, which
204    /// no eviction policy can make room for.
205    ByteLimit,
206}
207
208impl std::fmt::Display for MailboxFullReason {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            MailboxFullReason::MessageLimit => write!(f, "hop count limit"),
212            MailboxFullReason::ByteLimit => write!(f, "byte budget"),
213        }
214    }
215}
216
217/// Inbox at one of its logical bounds under [`OverflowPolicy::Reject`].
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub struct MailboxFull {
220    reason: MailboxFullReason,
221}
222
223impl MailboxFull {
224    #[inline]
225    pub const fn reason(self) -> MailboxFullReason {
226        self.reason
227    }
228}
229
230impl std::fmt::Display for MailboxFull {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        write!(f, "mailbox full ({})", self.reason)
233    }
234}
235
236impl std::error::Error for MailboxFull {}
237
238/// Identifies **one specific park** of a flow in its mailbox.
239///
240/// # Why a timeout needs a token
241///
242/// Cancellation of a `ReceiveTimeout` deadline is lazy: the timer thread
243/// does not remove its entry when a hop wakes the receiver early, it just
244/// finds nobody parked when it eventually fires. That reasoning only holds
245/// if the flow never parks *again* before the old deadline — and in a
246/// receive loop it always does:
247///
248/// ```text
249///   t=0    ReceiveTimeout(r5, 100ms)  -> park A, timer(100ms) armed
250///   t=20   hop arrives                -> handoff, park A over, flow runs
251///   t=30   Receive(r7)                -> park B  (no timeout)
252///   t=100  timer for park A fires     -> takes park B!
253///                                        writes Unit into r5, not r7
254/// ```
255///
256/// That is a spurious wake *and* a write to the previous wait's register.
257/// So [`Mailbox::park`] returns the epoch of the park it installed, the
258/// timer carries it, and [`Mailbox::take_parked_at`] only hands the flow
259/// over while that epoch is still current.
260///
261/// There is no public constructor: an epoch can only come from parking,
262/// so a timeout cannot present one for a wait that never happened.
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
264pub struct WaitEpoch(u64);
265
266impl WaitEpoch {
267    /// Raw counter value, for logs and metrics only. Never compare epochs
268    /// from two different mailboxes: the counter is per-inbox.
269    #[inline]
270    pub const fn get(self) -> u64 {
271        self.0
272    }
273}
274
275impl std::fmt::Display for WaitEpoch {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        write!(f, "wait#{}", self.0)
278    }
279}
280
281impl Mailbox {
282    pub fn new() -> Self {
283        Self::with_config(MailboxConfig::DEFAULT)
284    }
285
286    pub fn with_config(config: MailboxConfig) -> Self {
287        Mailbox {
288            inner: Mutex::new(MailboxInner {
289                queue: MailboxQueue::new(config.capacity().get(), config.bytes().get()),
290                parked: None,
291                parked_filter: WaitFilter::Any,
292                wait_epoch: 0,
293                stats: MailboxStats::default(),
294                waiting_senders: VecDeque::new(),
295                closed: false,
296            }),
297            config,
298        }
299    }
300
301    #[inline]
302    pub fn config(&self) -> MailboxConfig {
303        self.config
304    }
305
306    /// Snapshot of enqueue/dequeue/drop counters plus current occupancy
307    /// (taken under the mailbox lock, so the depth and the counters
308    /// describe the same instant).
309    pub fn stats(&self) -> Result<MailboxStats, RuntimeError> {
310        let inner = sync_lock::lock(&self.inner, "Mailbox::stats")?;
311        let mut stats = inner.stats;
312        stats.queued_messages = inner.queue.len();
313        stats.queued_bytes = inner.queue.bytes();
314        Ok(stats)
315    }
316
317    /// Push `value` under the mailbox config.
318    ///
319    /// 1. If a matching waiter is parked → [`Delivery::Handoff`] (does not
320    ///    consume a queue slot).
321    /// 2. Else enqueue / overflow according to [`OverflowPolicy`].
322    /// 3. [`MailboxFull`] only for Reject when the queue is already at one
323    ///    of its logical bounds (see [`MailboxFullReason`]) — plus the one
324    ///    case no policy can absorb: a hop larger than the whole byte
325    ///    budget.
326    ///
327    /// Mutex poison → [`RuntimeError`] (fail-closed).
328    pub fn push(&self, value: Value) -> Result<Result<Delivery, MailboxFull>, RuntimeError> {
329        let mut inner = sync_lock::lock(&self.inner, "Mailbox::push")?;
330        if let Some(flow) = inner.parked.take() {
331            if inner.parked_filter.matches(&value) {
332                inner.parked_filter = WaitFilter::Any;
333                inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
334                inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
335                return Ok(Ok(Delivery::Handoff(flow)));
336            }
337            inner.parked = Some(flow);
338            return Ok(enqueue_locked(
339                &mut inner,
340                value,
341                self.config.overflow(),
342            ));
343        }
344        Ok(enqueue_locked(
345            &mut inner,
346            value,
347            self.config.overflow(),
348        ))
349    }
350
351    /// Non-blocking pop of the front hop (classic `Receive`).
352    pub fn try_pop(&self) -> Result<Option<Value>, RuntimeError> {
353        self.try_pop_filter(WaitFilter::Any)
354    }
355
356    /// Non-blocking selective pop by application `tag` (`ReceiveMatch`).
357    pub fn try_pop_match(&self, tag: u16) -> Result<Option<Value>, RuntimeError> {
358        self.try_pop_filter(WaitFilter::Tag(tag))
359    }
360
361    /// Non-blocking pop under an arbitrary [`WaitFilter`] (FIFO skip).
362    pub(crate) fn try_pop_filter(
363        &self,
364        filter: WaitFilter,
365    ) -> Result<Option<Value>, RuntimeError> {
366        let mut inner = sync_lock::lock(&self.inner, "Mailbox::try_pop_filter")?;
367        let got = inner.queue.take(filter);
368        if got.is_some() {
369            inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
370        }
371        Ok(got)
372    }
373
374    /// Atomically re-check the queue and, if still empty, store `flow`
375    /// as parked (classic `Receive` — any hop wakes).
376    ///
377    /// Outer `Result` is infrastructure (mutex poison). Inner `Result` is
378    /// the lost-wakeup race: `Err(flow)` means a message arrived between
379    /// the worker's `try_pop` and this call — resume immediately with the
380    /// stashed pending message rather than parking forever.
381    ///
382    /// `Ok(Ok(epoch))` identifies the park that was installed. A caller
383    /// arming a `ReceiveTimeout` deadline must carry that [`WaitEpoch`] to
384    /// [`Mailbox::take_parked_at`], or a late deadline will wake whatever
385    /// wait happens to be current instead.
386    pub fn park(&self, flow: Box<Flow>) -> Result<Result<WaitEpoch, Box<Flow>>, RuntimeError> {
387        self.park_filter(flow, WaitFilter::Any)
388    }
389
390    /// Like [`park`](Self::park), but only a hop with `Message.tag == tag`
391    /// ends the wait. Non-matching hops already in the queue are left
392    /// untouched (FIFO skip).
393    pub fn park_match(
394        &self,
395        flow: Box<Flow>,
396        tag: u16,
397    ) -> Result<Result<WaitEpoch, Box<Flow>>, RuntimeError> {
398        self.park_filter(flow, WaitFilter::Tag(tag))
399    }
400
401    /// Park under an arbitrary filter. Re-checks the queue under the same
402    /// mutex before installing the waiter (anti lost-wakeup).
403    pub(crate) fn park_filter(
404        &self,
405        flow: Box<Flow>,
406        filter: WaitFilter,
407    ) -> Result<Result<WaitEpoch, Box<Flow>>, RuntimeError> {
408        let mut inner = sync_lock::lock(&self.inner, "Mailbox::park_filter")?;
409        if let Some(value) = inner.queue.take(filter) {
410            inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
411            drop(inner);
412            return Ok(Err(with_pending(flow, value)));
413        }
414        // Wrapping, not saturating: a saturated counter would make every
415        // later epoch compare equal, silently restoring the stale-deadline
416        // bug this exists to prevent. Reuse needs 2^64 parks on one inbox.
417        inner.wait_epoch = inner.wait_epoch.wrapping_add(1);
418        let epoch = WaitEpoch(inner.wait_epoch);
419        inner.parked_filter = filter;
420        inner.parked = Some(flow);
421        Ok(Ok(epoch))
422    }
423
424    /// Take the parked flow back out **only if** `epoch` is still the
425    /// current wait, used by the timer when a `ReceiveTimeout` deadline
426    /// fires.
427    ///
428    /// `None` means the deadline lost the race and must do nothing: either
429    /// a hop already woke that wait, or the flow has since parked on a
430    /// *different* `Receive` that this deadline does not own (see
431    /// [`WaitEpoch`]).
432    ///
433    /// The selective filter is reset only when the flow is actually taken.
434    /// Clearing it on a stale call would downgrade a live `ReceiveMatch` /
435    /// `Ask` waiter to "any hop wakes me".
436    pub fn take_parked_at(&self, epoch: WaitEpoch) -> Result<Option<Box<Flow>>, RuntimeError> {
437        let mut inner = sync_lock::lock(&self.inner, "Mailbox::take_parked_at")?;
438        if inner.wait_epoch != epoch.0 {
439            return Ok(None);
440        }
441        match inner.parked.take() {
442            Some(flow) => {
443                inner.parked_filter = WaitFilter::Any;
444                Ok(Some(flow))
445            }
446            None => Ok(None),
447        }
448    }
449
450    /// Take the current parked receiver regardless of epoch (lifecycle kill).
451    pub(crate) fn take_parked(&self) -> Result<Option<Box<Flow>>, RuntimeError> {
452        let mut inner = sync_lock::lock(&self.inner, "Mailbox::take_parked")?;
453        inner.parked_filter = WaitFilter::Any;
454        Ok(inner.parked.take())
455    }
456
457    /// Enqueue a hop even when the inbox is at a bound (system `DOWN`).
458    pub(crate) fn push_system(&self, value: Value) -> Result<Delivery, RuntimeError> {
459        let mut inner = sync_lock::lock(&self.inner, "Mailbox::push_system")?;
460        if let Some(flow) = inner.parked.take() {
461            if inner.parked_filter.matches(&value) {
462                inner.parked_filter = WaitFilter::Any;
463                inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
464                inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
465                return Ok(Delivery::Handoff(flow));
466            }
467            inner.parked = Some(flow);
468        }
469        inner.queue.force_push(value);
470        inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
471        Ok(Delivery::Queued)
472    }
473
474    /// Close the inbox for new `WAITING_SEND` parks, then drain waiters.
475    ///
476    /// Called from `finalize_flow` **before** unregistering the directory
477    /// so a sender that lost the Full/park race cannot park on a dead inbox.
478    pub(crate) fn close(&self) -> Result<Vec<Flow>, RuntimeError> {
479        let mut inner = sync_lock::lock(&self.inner, "Mailbox::close")?;
480        inner.closed = true;
481        Ok(inner.waiting_senders.drain(..).map(|w| *w.flow).collect())
482    }
483
484    /// Park a bytecode sender whose hop was refused (`WAITING_SEND`).
485    ///
486    /// Never drops `flow`: a closed or poisoned mailbox returns it in
487    /// [`ParkSender::Closed`] so the worker can finalize.
488    pub(crate) fn park_sender(&self, flow: Box<Flow>, message: Value) -> ParkSender {
489        match sync_lock::lock(&self.inner, "Mailbox::park_sender") {
490            Ok(inner) if inner.closed => ParkSender::Closed(flow),
491            Ok(mut inner) => {
492                inner.waiting_senders.push_back(WaitingSender { flow, message });
493                ParkSender::Parked
494            }
495            Err(e) => {
496                super::error::report_fault(e);
497                ParkSender::Closed(flow)
498            }
499        }
500    }
501
502    /// After a pop frees a slot, admit **one** waiting sender (no wake storm).
503    pub(crate) fn admit_waiting_sender(&self) -> Result<Option<Box<Flow>>, RuntimeError> {
504        let mut inner = sync_lock::lock(&self.inner, "Mailbox::admit_waiting_sender")?;
505        if inner.closed {
506            return Ok(None);
507        }
508        let Some(waiter) = inner.waiting_senders.pop_front() else {
509            return Ok(None);
510        };
511        match enqueue_locked(&mut inner, waiter.message.clone(), OverflowPolicy::Reject) {
512            Ok(_) => Ok(Some(waiter.flow)),
513            Err(_) => {
514                inner.waiting_senders.push_front(waiter);
515                Ok(None)
516            }
517        }
518    }
519
520    /// Pull one parked sender out (link-kill of a flow blocked on `WAITING_SEND`).
521    pub(crate) fn take_waiting_sender(
522        &self,
523        sender: FlowId,
524    ) -> Result<Option<Box<Flow>>, RuntimeError> {
525        let mut inner = sync_lock::lock(&self.inner, "Mailbox::take_waiting_sender")?;
526        if let Some(pos) = inner.waiting_senders.iter().position(|w| w.flow.id == sender) {
527            return Ok(inner.waiting_senders.remove(pos).map(|w| w.flow));
528        }
529        Ok(None)
530    }
531
532}
533
534/// Outcome of [`Mailbox::park_sender`].
535pub(crate) enum ParkSender {
536    Parked,
537    /// Inbox already closed (target finalizing) or lock poisoned.
538    Closed(Box<Flow>),
539}
540
541impl Default for Mailbox {
542    fn default() -> Self {
543        Self::new()
544    }
545}
546
547fn enqueue_locked(
548    inner: &mut MailboxInner,
549    value: Value,
550    policy: OverflowPolicy,
551) -> Result<Delivery, MailboxFull> {
552    match inner.queue.enqueue(value, policy) {
553        Ok(EnqueueEffect::Enqueued) => {
554            inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
555            Ok(Delivery::Queued)
556        }
557        Ok(EnqueueEffect::DroppedOldest) => {
558            inner.stats.dropped_oldest = inner.stats.dropped_oldest.saturating_add(1);
559            inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
560            Ok(Delivery::QueuedDropOldest)
561        }
562        Ok(EnqueueEffect::DroppedNewest) => {
563            inner.stats.dropped_newest = inner.stats.dropped_newest.saturating_add(1);
564            Ok(Delivery::DroppedNewest)
565        }
566        Err(reason) => {
567            inner.stats.rejected = inner.stats.rejected.saturating_add(1);
568            if reason == MailboxFullReason::ByteLimit {
569                inner.stats.rejected_byte_limit =
570                    inner.stats.rejected_byte_limit.saturating_add(1);
571            }
572            Err(MailboxFull { reason })
573        }
574    }
575}
576
577/// Stashes a message that arrived just as we were about to park, so the
578/// worker loop can resume the flow with it on the very next step
579/// without re-entering the mailbox. See [`Mailbox::park`].
580fn with_pending(mut flow: Box<Flow>, value: Value) -> Box<Flow> {
581    flow.pending_message = Some(value);
582    flow
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588    use crate::bytecode::Message;
589    use crate::scheduler::oneshot;
590    use crate::scheduler::process::{next_flow_id, RestartPolicy};
591    use crate::vm::{NativeTable, Vm};
592    use crate::bytecode::builder::ChunkBuilder;
593    use std::sync::Arc;
594
595    type TestResult = Result<(), Box<dyn std::error::Error>>;
596
597    fn dummy_flow() -> Result<Box<Flow>, Box<dyn std::error::Error>> {
598        let mut b = ChunkBuilder::new("mb");
599        b.begin_function("main", 0, 1);
600        b.emit_return(0);
601        let chunk = b.finish();
602        let vm = Vm::new(Arc::new(chunk), NativeTable::empty(), 0, &[])?;
603        let (tx, _rx) = oneshot::channel();
604        Ok(Box::new(Flow::new(
605            next_flow_id(),
606            vm,
607            Arc::new(Mailbox::new()),
608            RestartPolicy::Never,
609            tx,
610        )))
611    }
612
613    fn hop(sender: u64, request_id: u64, tag: u16, payload: u64) -> Value {
614        Value::Message(Message::new(sender, request_id, tag, payload))
615    }
616
617    fn msg(tag: u16, payload: u64) -> Value {
618        hop(1, 1, tag, payload)
619    }
620
621    fn tiny_reject(n: u32) -> Result<Mailbox, Box<dyn std::error::Error>> {
622        let cap = MailboxCapacity::new(n).ok_or("invalid mailbox capacity")?;
623        Ok(Mailbox::with_config(MailboxConfig::new(
624            cap,
625            OverflowPolicy::Reject,
626        )))
627    }
628
629    fn hop_msg(value: &Value) -> Result<Message, Box<dyn std::error::Error>> {
630        value.as_message().ok_or("expected Message hop".into())
631    }
632
633    #[test]
634    fn try_pop_match_skips_non_matching_fifo() -> TestResult {
635        let mb = Mailbox::new();
636        mb.push(msg(9, 1))??;
637        mb.push(msg(1, 42))??;
638        mb.push(msg(9, 2))??;
639        let got = mb.try_pop_match(1)?.ok_or("match")?;
640        assert_eq!(hop_msg(&got)?.payload, 42);
641        assert_eq!(
642            hop_msg(&mb.try_pop()?.ok_or("first leftover")?)?.tag,
643            9
644        );
645        assert_eq!(
646            hop_msg(&mb.try_pop()?.ok_or("second leftover")?)?.payload,
647            2
648        );
649        Ok(())
650    }
651
652    #[test]
653    fn push_while_park_match_queues_junk_keeps_waiter() -> TestResult {
654        let mb = Mailbox::new();
655        let flow = dummy_flow()?;
656        assert!(mb.park_match(flow, 1)?.is_ok());
657        assert!(matches!(mb.push(msg(9, 0))??, Delivery::Queued));
658        assert!(matches!(mb.push(msg(1, 7))??, Delivery::Handoff(_)));
659        assert_eq!(hop_msg(&mb.try_pop()?.ok_or("queued junk")?)?.tag, 9);
660        Ok(())
661    }
662
663    #[test]
664    fn ask_does_not_consume_reply_for_another_request() -> TestResult {
665        let mb = Mailbox::new();
666        mb.push(hop(10, 2, 2, 99))??;
667        mb.push(hop(10, 1, 2, 42))??;
668        let filter = WaitFilter::Correlation {
669            expect_request_id: 1,
670            expect_sender: Some(10),
671        };
672        let got = mb.try_pop_filter(filter)?.ok_or("id=1")?;
673        assert_eq!(hop_msg(&got)?.payload, 42);
674        let left = mb.try_pop()?.ok_or("leftover")?;
675        assert_eq!(hop_msg(&left)?.request_id, 2);
676        Ok(())
677    }
678
679    #[test]
680    fn ask_requires_reply_from_target() -> TestResult {
681        let mb = Mailbox::new();
682        let flow = dummy_flow()?;
683        let filter = WaitFilter::Correlation {
684            expect_request_id: 1,
685            expect_sender: Some(10),
686        };
687        assert!(mb.park_filter(flow, filter)?.is_ok());
688        assert!(matches!(mb.push(hop(99, 1, 2, 0))??, Delivery::Queued));
689        assert!(matches!(mb.push(hop(10, 1, 2, 42))??, Delivery::Handoff(_)));
690        assert_eq!(
691            hop_msg(&mb.try_pop()?.ok_or("non-matching queued")?)?.sender,
692            99
693        );
694        Ok(())
695    }
696
697    #[test]
698    fn reject_when_full_without_waiter() -> TestResult {
699        let mb = tiny_reject(1)?;
700        assert!(matches!(mb.push(msg(1, 1))??, Delivery::Queued));
701        // Bind the error: `Err(MailboxFull)` would introduce a *variable*
702        // named MailboxFull now that the type carries a reason, matching
703        // every error and asserting nothing.
704        match mb.push(msg(1, 2))? {
705            Err(full) => assert_eq!(full.reason(), MailboxFullReason::MessageLimit),
706            Ok(_) => return Err("expected the hop count bound to refuse".into()),
707        }
708        let s = mb.stats()?;
709        assert_eq!(s.enqueued, 1);
710        assert_eq!(s.rejected, 1);
711        assert_eq!(s.rejected_byte_limit, 0);
712        assert_eq!(s.queued_messages, 1);
713        Ok(())
714    }
715
716    /// Park `flow`, requiring that it actually parked, and hand back the
717    /// epoch. Collapses the two-level `Result` the tests do not care about.
718    fn park_now(mb: &Mailbox, flow: Box<Flow>) -> Result<WaitEpoch, Box<dyn std::error::Error>> {
719        match mb.park(flow)? {
720            Ok(epoch) => Ok(epoch),
721            Err(_) => Err("an empty mailbox should have parked the flow".into()),
722        }
723    }
724
725    fn handoff(mb: &Mailbox, value: Value) -> Result<Box<Flow>, Box<dyn std::error::Error>> {
726        match mb.push(value)?? {
727            Delivery::Handoff(flow) => Ok(flow),
728            other => Err(format!("expected a handoff, got {}", delivery_name(&other)).into()),
729        }
730    }
731
732    fn delivery_name(d: &Delivery) -> &'static str {
733        match d {
734            Delivery::Queued => "Queued",
735            Delivery::QueuedDropOldest => "QueuedDropOldest",
736            Delivery::DroppedNewest => "DroppedNewest",
737            Delivery::Handoff(_) => "Handoff",
738        }
739    }
740
741    #[test]
742    fn a_stale_deadline_cannot_steal_a_later_wait() -> TestResult {
743        let mb = Mailbox::new();
744        let first = park_now(&mb, dummy_flow()?)?;
745        // A hop beats the deadline: the handoff ends *this* wait.
746        let woken = handoff(&mb, msg(1, 1))?;
747        // The same flow parks again on a fresh `Receive`.
748        let second = park_now(&mb, woken)?;
749        assert_ne!(first, second, "each park must get its own epoch");
750
751        // The deadline armed for the first wait fires late. Before the
752        // epoch check it took this second wait, resumed the flow, and
753        // wrote Unit into the *first* wait's register.
754        assert!(mb.take_parked_at(first)?.is_none());
755        // The live wait is untouched, so its own deadline still works.
756        assert!(mb.take_parked_at(second)?.is_some());
757        Ok(())
758    }
759
760    #[test]
761    fn a_stale_deadline_does_not_downgrade_a_selective_waiter() -> TestResult {
762        let mb = Mailbox::new();
763        let first = park_now(&mb, dummy_flow()?)?;
764        let woken = handoff(&mb, msg(1, 1))?;
765        // Second wait is selective: only tag 7 may wake it.
766        let second = match mb.park_match(woken, 7)? {
767            Ok(epoch) => epoch,
768            Err(_) => return Err("empty mailbox should have parked the flow".into()),
769        };
770        assert_ne!(first, second);
771
772        assert!(mb.take_parked_at(first)?.is_none());
773        // The filter must survive the stale call: a tag-9 hop is queued,
774        // not handed off.
775        assert!(matches!(mb.push(msg(9, 0))??, Delivery::Queued));
776        // ...and tag 7 still wakes it.
777        assert!(matches!(mb.push(msg(7, 0))??, Delivery::Handoff(_)));
778        Ok(())
779    }
780
781    #[test]
782    fn a_deadline_for_a_wait_that_a_hop_ended_does_nothing() -> TestResult {
783        let mb = Mailbox::new();
784        let epoch = park_now(&mb, dummy_flow()?)?;
785        let _woken = handoff(&mb, msg(1, 1))?;
786        // Nobody is parked now; the deadline must be a no-op rather than
787        // reporting a flow it does not have.
788        assert!(mb.take_parked_at(epoch)?.is_none());
789        Ok(())
790    }
791
792    #[test]
793    fn byte_budget_refuses_before_the_hop_count_and_says_so() -> TestResult {
794        // 64 hop slots but a 1 KiB budget: blobs exhaust bytes first.
795        let cap = MailboxCapacity::new(64).ok_or("cap")?;
796        let budget = MailboxBytes::new(MailboxBytes::MIN).ok_or("bytes")?;
797        let mb = Mailbox::with_config(
798            MailboxConfig::new(cap, OverflowPolicy::Reject).with_bytes(budget),
799        );
800        assert!(matches!(mb.push(Value::bytes(vec![0u8; 900]))??, Delivery::Queued));
801        match mb.push(Value::bytes(vec![0u8; 900]))? {
802            Err(full) => assert_eq!(full.reason(), MailboxFullReason::ByteLimit),
803            Ok(_) => return Err("expected the byte budget to refuse".into()),
804        }
805        let s = mb.stats()?;
806        assert_eq!(s.queued_messages, 1);
807        assert!(s.queued_bytes >= 900);
808        assert_eq!(s.rejected, 1);
809        assert_eq!(s.rejected_byte_limit, 1);
810        Ok(())
811    }
812
813    #[test]
814    fn draining_a_hop_frees_its_byte_charge() -> TestResult {
815        let cap = MailboxCapacity::new(64).ok_or("cap")?;
816        let budget = MailboxBytes::new(MailboxBytes::MIN).ok_or("bytes")?;
817        let mb = Mailbox::with_config(
818            MailboxConfig::new(cap, OverflowPolicy::Reject).with_bytes(budget),
819        );
820        mb.push(Value::bytes(vec![0u8; 900]))??;
821        mb.try_pop()?.ok_or("queued blob")?;
822        assert_eq!(mb.stats()?.queued_bytes, 0);
823        // A receiver that keeps up must not be permanently throttled by a
824        // charge that was never refunded.
825        assert!(matches!(mb.push(Value::bytes(vec![0u8; 900]))??, Delivery::Queued));
826        Ok(())
827    }
828
829    #[test]
830    fn matching_handoff_does_not_count_as_full() -> TestResult {
831        let mb = tiny_reject(1)?;
832        mb.push(msg(9, 0))??;
833        let flow = dummy_flow()?;
834        assert!(mb.park_match(flow, 1)?.is_ok());
835        assert!(matches!(mb.push(msg(1, 7))??, Delivery::Handoff(_)));
836        assert_eq!(hop_msg(&mb.try_pop()?.ok_or("queued")?)?.tag, 9);
837        Ok(())
838    }
839
840    #[test]
841    fn waiting_send_admits_one_after_pop() -> TestResult {
842        let mb = tiny_reject(1)?;
843        assert!(matches!(mb.push(msg(1, 1))??, Delivery::Queued));
844        assert!(matches!(
845            mb.park_sender(dummy_flow()?, msg(1, 2)),
846            ParkSender::Parked
847        ));
848        assert!(mb.try_pop()?.is_some());
849        let woken = mb.admit_waiting_sender()?.ok_or("admitted")?;
850        drop(woken);
851        assert_eq!(hop_msg(&mb.try_pop()?.ok_or("second hop")?)?.payload, 2);
852        assert!(mb.admit_waiting_sender()?.is_none());
853        Ok(())
854    }
855
856    #[test]
857    fn close_rejects_late_park_sender() -> TestResult {
858        let mb = tiny_reject(1)?;
859        let leftover = mb.close()?;
860        assert!(leftover.is_empty());
861        match mb.park_sender(dummy_flow()?, msg(1, 1)) {
862            ParkSender::Closed(_) => {}
863            ParkSender::Parked => return Err("closed mailbox must not park a sender".into()),
864        }
865        Ok(())
866    }
867
868    #[test]
869    fn drop_oldest_still_wakes_on_match() -> TestResult {
870        let cap = MailboxCapacity::new(1).ok_or("cap")?;
871        let mb = Mailbox::with_config(MailboxConfig::new(cap, OverflowPolicy::DropOldest));
872        let flow = dummy_flow()?;
873        assert!(mb.park_match(flow, 1)?.is_ok());
874        mb.push(msg(9, 1))??;
875        assert!(matches!(mb.push(msg(1, 2))??, Delivery::Handoff(_)));
876        Ok(())
877    }
878}