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_payload_int(msg: &Message) -> i64 {
614        msg.payload.as_int().unwrap_or(0)
615    }
616
617    fn hop(sender: u64, request_id: u64, tag: u16, payload: impl Into<Value>) -> Value {
618        Value::Message(Message::new(sender, request_id, tag, payload))
619    }
620
621    fn msg(tag: u16, payload: impl Into<Value>) -> Value {
622        hop(1, 1, tag, payload)
623    }
624
625    fn tiny_reject(n: u32) -> Result<Mailbox, Box<dyn std::error::Error>> {
626        let cap = MailboxCapacity::new(n).ok_or("invalid mailbox capacity")?;
627        Ok(Mailbox::with_config(MailboxConfig::new(
628            cap,
629            OverflowPolicy::Reject,
630        )))
631    }
632
633    fn hop_msg(value: &Value) -> Result<&Message, Box<dyn std::error::Error>> {
634        value.as_message().ok_or_else(|| "expected Message hop".into())
635    }
636
637    #[test]
638    fn try_pop_match_skips_non_matching_fifo() -> TestResult {
639        let mb = Mailbox::new();
640        mb.push(msg(9, 1))??;
641        mb.push(msg(1, 42))??;
642        mb.push(msg(9, 2))??;
643        let got = mb.try_pop_match(1)?.ok_or("match")?;
644        assert_eq!(hop_payload_int(hop_msg(&got)?), 42);
645        assert_eq!(
646            hop_msg(&mb.try_pop()?.ok_or("first leftover")?)?.tag,
647            9
648        );
649        assert_eq!(
650            hop_payload_int(hop_msg(&mb.try_pop()?.ok_or("second leftover")?)?),
651            2
652        );
653        Ok(())
654    }
655
656    #[test]
657    fn push_while_park_match_queues_junk_keeps_waiter() -> TestResult {
658        let mb = Mailbox::new();
659        let flow = dummy_flow()?;
660        assert!(mb.park_match(flow, 1)?.is_ok());
661        assert!(matches!(mb.push(msg(9, 0))??, Delivery::Queued));
662        assert!(matches!(mb.push(msg(1, 7))??, Delivery::Handoff(_)));
663        assert_eq!(hop_msg(&mb.try_pop()?.ok_or("queued junk")?)?.tag, 9);
664        Ok(())
665    }
666
667    #[test]
668    fn ask_does_not_consume_reply_for_another_request() -> TestResult {
669        let mb = Mailbox::new();
670        mb.push(hop(10, 2, 2, 99))??;
671        mb.push(hop(10, 1, 2, 42))??;
672        let filter = WaitFilter::Correlation {
673            expect_request_id: 1,
674            expect_sender: Some(10),
675        };
676        let got = mb.try_pop_filter(filter)?.ok_or("id=1")?;
677        assert_eq!(hop_payload_int(hop_msg(&got)?), 42);
678        let left = mb.try_pop()?.ok_or("leftover")?;
679        assert_eq!(hop_msg(&left)?.request_id, 2);
680        Ok(())
681    }
682
683    #[test]
684    fn ask_requires_reply_from_target() -> TestResult {
685        let mb = Mailbox::new();
686        let flow = dummy_flow()?;
687        let filter = WaitFilter::Correlation {
688            expect_request_id: 1,
689            expect_sender: Some(10),
690        };
691        assert!(mb.park_filter(flow, filter)?.is_ok());
692        assert!(matches!(mb.push(hop(99, 1, 2, 0))??, Delivery::Queued));
693        assert!(matches!(mb.push(hop(10, 1, 2, 42))??, Delivery::Handoff(_)));
694        assert_eq!(
695            hop_msg(&mb.try_pop()?.ok_or("non-matching queued")?)?.sender,
696            99
697        );
698        Ok(())
699    }
700
701    #[test]
702    fn reject_when_full_without_waiter() -> TestResult {
703        let mb = tiny_reject(1)?;
704        assert!(matches!(mb.push(msg(1, 1))??, Delivery::Queued));
705        // Bind the error: `Err(MailboxFull)` would introduce a *variable*
706        // named MailboxFull now that the type carries a reason, matching
707        // every error and asserting nothing.
708        match mb.push(msg(1, 2))? {
709            Err(full) => assert_eq!(full.reason(), MailboxFullReason::MessageLimit),
710            Ok(_) => return Err("expected the hop count bound to refuse".into()),
711        }
712        let s = mb.stats()?;
713        assert_eq!(s.enqueued, 1);
714        assert_eq!(s.rejected, 1);
715        assert_eq!(s.rejected_byte_limit, 0);
716        assert_eq!(s.queued_messages, 1);
717        Ok(())
718    }
719
720    /// Park `flow`, requiring that it actually parked, and hand back the
721    /// epoch. Collapses the two-level `Result` the tests do not care about.
722    fn park_now(mb: &Mailbox, flow: Box<Flow>) -> Result<WaitEpoch, Box<dyn std::error::Error>> {
723        match mb.park(flow)? {
724            Ok(epoch) => Ok(epoch),
725            Err(_) => Err("an empty mailbox should have parked the flow".into()),
726        }
727    }
728
729    fn handoff(mb: &Mailbox, value: Value) -> Result<Box<Flow>, Box<dyn std::error::Error>> {
730        match mb.push(value)?? {
731            Delivery::Handoff(flow) => Ok(flow),
732            other => Err(format!("expected a handoff, got {}", delivery_name(&other)).into()),
733        }
734    }
735
736    fn delivery_name(d: &Delivery) -> &'static str {
737        match d {
738            Delivery::Queued => "Queued",
739            Delivery::QueuedDropOldest => "QueuedDropOldest",
740            Delivery::DroppedNewest => "DroppedNewest",
741            Delivery::Handoff(_) => "Handoff",
742        }
743    }
744
745    #[test]
746    fn a_stale_deadline_cannot_steal_a_later_wait() -> TestResult {
747        let mb = Mailbox::new();
748        let first = park_now(&mb, dummy_flow()?)?;
749        // A hop beats the deadline: the handoff ends *this* wait.
750        let woken = handoff(&mb, msg(1, 1))?;
751        // The same flow parks again on a fresh `Receive`.
752        let second = park_now(&mb, woken)?;
753        assert_ne!(first, second, "each park must get its own epoch");
754
755        // The deadline armed for the first wait fires late. Before the
756        // epoch check it took this second wait, resumed the flow, and
757        // wrote Unit into the *first* wait's register.
758        assert!(mb.take_parked_at(first)?.is_none());
759        // The live wait is untouched, so its own deadline still works.
760        assert!(mb.take_parked_at(second)?.is_some());
761        Ok(())
762    }
763
764    #[test]
765    fn a_stale_deadline_does_not_downgrade_a_selective_waiter() -> TestResult {
766        let mb = Mailbox::new();
767        let first = park_now(&mb, dummy_flow()?)?;
768        let woken = handoff(&mb, msg(1, 1))?;
769        // Second wait is selective: only tag 7 may wake it.
770        let second = match mb.park_match(woken, 7)? {
771            Ok(epoch) => epoch,
772            Err(_) => return Err("empty mailbox should have parked the flow".into()),
773        };
774        assert_ne!(first, second);
775
776        assert!(mb.take_parked_at(first)?.is_none());
777        // The filter must survive the stale call: a tag-9 hop is queued,
778        // not handed off.
779        assert!(matches!(mb.push(msg(9, 0))??, Delivery::Queued));
780        // ...and tag 7 still wakes it.
781        assert!(matches!(mb.push(msg(7, 0))??, Delivery::Handoff(_)));
782        Ok(())
783    }
784
785    #[test]
786    fn a_deadline_for_a_wait_that_a_hop_ended_does_nothing() -> TestResult {
787        let mb = Mailbox::new();
788        let epoch = park_now(&mb, dummy_flow()?)?;
789        let _woken = handoff(&mb, msg(1, 1))?;
790        // Nobody is parked now; the deadline must be a no-op rather than
791        // reporting a flow it does not have.
792        assert!(mb.take_parked_at(epoch)?.is_none());
793        Ok(())
794    }
795
796    #[test]
797    fn byte_budget_refuses_before_the_hop_count_and_says_so() -> TestResult {
798        // 64 hop slots but a 1 KiB budget: blobs exhaust bytes first.
799        let cap = MailboxCapacity::new(64).ok_or("cap")?;
800        let budget = MailboxBytes::new(MailboxBytes::MIN).ok_or("bytes")?;
801        let mb = Mailbox::with_config(
802            MailboxConfig::new(cap, OverflowPolicy::Reject).with_bytes(budget),
803        );
804        assert!(matches!(mb.push(Value::bytes(vec![0u8; 900]))??, Delivery::Queued));
805        match mb.push(Value::bytes(vec![0u8; 900]))? {
806            Err(full) => assert_eq!(full.reason(), MailboxFullReason::ByteLimit),
807            Ok(_) => return Err("expected the byte budget to refuse".into()),
808        }
809        let s = mb.stats()?;
810        assert_eq!(s.queued_messages, 1);
811        assert!(s.queued_bytes >= 900);
812        assert_eq!(s.rejected, 1);
813        assert_eq!(s.rejected_byte_limit, 1);
814        Ok(())
815    }
816
817    #[test]
818    fn draining_a_hop_frees_its_byte_charge() -> TestResult {
819        let cap = MailboxCapacity::new(64).ok_or("cap")?;
820        let budget = MailboxBytes::new(MailboxBytes::MIN).ok_or("bytes")?;
821        let mb = Mailbox::with_config(
822            MailboxConfig::new(cap, OverflowPolicy::Reject).with_bytes(budget),
823        );
824        mb.push(Value::bytes(vec![0u8; 900]))??;
825        mb.try_pop()?.ok_or("queued blob")?;
826        assert_eq!(mb.stats()?.queued_bytes, 0);
827        // A receiver that keeps up must not be permanently throttled by a
828        // charge that was never refunded.
829        assert!(matches!(mb.push(Value::bytes(vec![0u8; 900]))??, Delivery::Queued));
830        Ok(())
831    }
832
833    #[test]
834    fn matching_handoff_does_not_count_as_full() -> TestResult {
835        let mb = tiny_reject(1)?;
836        mb.push(msg(9, 0))??;
837        let flow = dummy_flow()?;
838        assert!(mb.park_match(flow, 1)?.is_ok());
839        assert!(matches!(mb.push(msg(1, 7))??, Delivery::Handoff(_)));
840        assert_eq!(hop_msg(&mb.try_pop()?.ok_or("queued")?)?.tag, 9);
841        Ok(())
842    }
843
844    #[test]
845    fn waiting_send_admits_one_after_pop() -> TestResult {
846        let mb = tiny_reject(1)?;
847        assert!(matches!(mb.push(msg(1, 1))??, Delivery::Queued));
848        assert!(matches!(
849            mb.park_sender(dummy_flow()?, msg(1, 2)),
850            ParkSender::Parked
851        ));
852        assert!(mb.try_pop()?.is_some());
853        let woken = mb.admit_waiting_sender()?.ok_or("admitted")?;
854        drop(woken);
855        assert_eq!(hop_payload_int(hop_msg(&mb.try_pop()?.ok_or("second hop")?)?), 2);
856        assert!(mb.admit_waiting_sender()?.is_none());
857        Ok(())
858    }
859
860    #[test]
861    fn close_rejects_late_park_sender() -> TestResult {
862        let mb = tiny_reject(1)?;
863        let leftover = mb.close()?;
864        assert!(leftover.is_empty());
865        match mb.park_sender(dummy_flow()?, msg(1, 1)) {
866            ParkSender::Closed(_) => {}
867            ParkSender::Parked => return Err("closed mailbox must not park a sender".into()),
868        }
869        Ok(())
870    }
871
872    #[test]
873    fn drop_oldest_still_wakes_on_match() -> TestResult {
874        let cap = MailboxCapacity::new(1).ok_or("cap")?;
875        let mb = Mailbox::with_config(MailboxConfig::new(cap, OverflowPolicy::DropOldest));
876        let flow = dummy_flow()?;
877        assert!(mb.park_match(flow, 1)?.is_ok());
878        mb.push(msg(9, 1))??;
879        assert!(matches!(mb.push(msg(1, 2))??, Delivery::Handoff(_)));
880        Ok(())
881    }
882}