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