Skip to main content

byteflow/scheduler/
mailbox.rs

1use std::collections::VecDeque;
2use std::sync::Mutex;
3
4use crate::bytecode::Value;
5
6use super::error::RuntimeError;
7use super::process::Flow;
8use super::sync_lock;
9
10/// Selective wait criterion installed while a flow is parked in its mailbox.
11///
12/// Used by classic `Receive`, `ReceiveMatch`, and `Ask`. Matching always
13/// **skips** (never drops) non-matching hops already in the queue.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub(crate) enum WaitFilter {
16    /// `Receive`: consume the oldest value regardless of contents.
17    Any,
18    /// `ReceiveMatch`: oldest `Message` whose application `tag` matches.
19    Tag(u16),
20    /// `Ask`: reply belonging to one specific request.
21    ///
22    /// `expect_request_id` is the RPC correlation key.
23    /// `expect_sender`, when present, additionally constrains reply origin.
24    /// Ask uses `Some(resolved_target_FlowId)` so a hop with the same
25    /// `request_id` from an unrelated flow cannot complete the RPC.
26    /// Compare against FlowId, never CapId (S2 after FlowCap).
27    Correlation {
28        expect_request_id: u64,
29        expect_sender: Option<u64>,
30    },
31}
32
33impl WaitFilter {
34    #[inline]
35    pub(crate) fn matches(&self, value: &Value) -> bool {
36        match *self {
37            Self::Any => true,
38            Self::Tag(expected_tag) => value
39                .as_message()
40                .map(|m| m.tag == expected_tag)
41                .unwrap_or(false),
42            Self::Correlation {
43                expect_request_id,
44                expect_sender,
45            } => match value.as_message() {
46                Some(m) => {
47                    m.request_id == expect_request_id
48                        && expect_sender.map(|s| m.sender == s).unwrap_or(true)
49                }
50                None => false,
51            },
52        }
53    }
54}
55
56/// A flow's inbox, plus (when the owning flow is blocked on
57/// `Receive` / `ReceiveMatch` / `Ask` with nothing to read) the parked flow itself.
58///
59/// # Why the flow lives *inside* its own mailbox while waiting
60///
61/// Internally, a flow is reachable through its [`super::process::FlowId`], which
62/// resolves (via [`super::directory::Directory`]) to this `Mailbox`. Bytecode
63/// does **not** address by Pid anymore (FlowCap): `Send` / `Ask` resolve a
64/// Cap to a FlowId first, then look up here. Host [`crate::Runtime::send`]
65/// still uses FlowId directly (trusted).
66///
67/// Storing the blocked `Box<Flow>` directly in `MailboxInner::parked`, behind
68/// the same mutex that guards the message queue, turns "deliver a message and
69/// wake the receiver if it was waiting" into a single critical section — which
70/// is what actually prevents the classic lost-wakeup race:
71///
72/// ```text
73/// racing without a shared lock:
74///   receiver: queue.pop() -> None
75///   sender:   queue.push(msg); wake(receiver)   // receiver isn't parked yet!
76///   receiver: park()                            // ...and now sleeps forever
77///
78/// with both steps under one mutex (what this type does):
79///   receiver: lock; queue.pop() -> None; store self in `parked`; unlock
80///   sender:   lock; parked.take() -> Some(receiver); unlock; wake(receiver)
81/// ```
82/// Because "check the queue" and "become parked" happen atomically with
83/// respect to "push and check for a parked receiver", there is no window
84/// where a message can be pushed without either landing in the queue for a
85/// later `Receive` or immediately waking an already-parked one.
86///
87/// # Selective wait (`ReceiveMatch` / `Ask`)
88///
89/// When parked with a non-[`WaitFilter::Any`] filter, only a hop that
90/// satisfies the filter wakes the flow. Other hops are appended to the
91/// queue and the waiter stays parked (FIFO skip, never drop).
92pub struct Mailbox {
93    inner: Mutex<MailboxInner>,
94}
95
96struct MailboxInner {
97    queue: VecDeque<Value>,
98    parked: Option<Box<Flow>>,
99    /// Active while `parked` is `Some`. Ignored when nobody is waiting.
100    parked_filter: WaitFilter,
101}
102
103impl Default for MailboxInner {
104    fn default() -> Self {
105        Self {
106            queue: VecDeque::new(),
107            parked: None,
108            parked_filter: WaitFilter::Any,
109        }
110    }
111}
112
113/// Outcome of pushing a message: either it was queued for later, or it
114/// immediately handed off to a flow that was parked waiting for it — in
115/// which case the caller (see `worker::deliver`) is responsible for feeding
116/// the value back into that flow's VM and re-enqueuing it as `Ready`.
117pub enum Delivery {
118    Queued,
119    Handoff(Box<Flow>),
120}
121
122impl Mailbox {
123    pub fn new() -> Self {
124        Mailbox {
125            inner: Mutex::new(MailboxInner::default()),
126        }
127    }
128
129    /// Push `value`. If a flow is currently parked on this mailbox
130    /// and the hop satisfies its wait filter, it is atomically removed and
131    /// returned via [`Delivery::Handoff`]. Otherwise the hop is queued
132    /// (and a selective waiter stays parked).
133    ///
134    /// Mutex poison → [`RuntimeError`] (fail-closed; do not continue on
135    /// inconsistent shared state).
136    pub fn push(&self, value: Value) -> Result<Delivery, RuntimeError> {
137        let mut inner = sync_lock::lock(&self.inner, "Mailbox::push")?;
138        if let Some(flow) = inner.parked.take() {
139            if inner.parked_filter.matches(&value) {
140                inner.parked_filter = WaitFilter::Any;
141                return Ok(Delivery::Handoff(flow));
142            }
143            // Selective wait: keep parked, queue the non-matching hop.
144            inner.parked = Some(flow);
145            inner.queue.push_back(value);
146            return Ok(Delivery::Queued);
147        }
148        inner.queue.push_back(value);
149        Ok(Delivery::Queued)
150    }
151
152    /// Non-blocking pop of the front hop (classic `Receive`).
153    pub fn try_pop(&self) -> Result<Option<Value>, RuntimeError> {
154        self.try_pop_filter(WaitFilter::Any)
155    }
156
157    /// Non-blocking selective pop by application `tag` (`ReceiveMatch`).
158    pub fn try_pop_match(&self, tag: u16) -> Result<Option<Value>, RuntimeError> {
159        self.try_pop_filter(WaitFilter::Tag(tag))
160    }
161
162    /// Non-blocking pop under an arbitrary [`WaitFilter`] (FIFO skip).
163    pub(crate) fn try_pop_filter(
164        &self,
165        filter: WaitFilter,
166    ) -> Result<Option<Value>, RuntimeError> {
167        let mut inner = sync_lock::lock(&self.inner, "Mailbox::try_pop_filter")?;
168        Ok(take_with_filter(&mut inner.queue, filter))
169    }
170
171    /// Atomically re-check the queue and, if still empty, store `flow`
172    /// as parked (classic `Receive` — any hop wakes).
173    ///
174    /// Outer `Result` is infrastructure (mutex poison). Inner `Result` is
175    /// the lost-wakeup race: `Err(flow)` means a message arrived between
176    /// the worker's `try_pop` and this call — resume immediately with the
177    /// stashed pending message rather than parking forever.
178    pub fn park(&self, flow: Box<Flow>) -> Result<Result<(), Box<Flow>>, RuntimeError> {
179        self.park_filter(flow, WaitFilter::Any)
180    }
181
182    /// Like [`park`](Self::park), but only a hop with `Message.tag == tag`
183    /// ends the wait. Non-matching hops already in the queue are left
184    /// untouched (FIFO skip).
185    pub fn park_match(
186        &self,
187        flow: Box<Flow>,
188        tag: u16,
189    ) -> Result<Result<(), Box<Flow>>, RuntimeError> {
190        self.park_filter(flow, WaitFilter::Tag(tag))
191    }
192
193    /// Park under an arbitrary filter. Re-checks the queue under the same
194    /// mutex before installing the waiter (anti lost-wakeup).
195    pub(crate) fn park_filter(
196        &self,
197        flow: Box<Flow>,
198        filter: WaitFilter,
199    ) -> Result<Result<(), Box<Flow>>, RuntimeError> {
200        let mut inner = sync_lock::lock(&self.inner, "Mailbox::park_filter")?;
201        if let Some(value) = take_with_filter(&mut inner.queue, filter) {
202            drop(inner);
203            return Ok(Err(with_pending(flow, value)));
204        }
205        inner.parked_filter = filter;
206        inner.parked = Some(flow);
207        Ok(Ok(()))
208    }
209
210    /// Attempt to take a timed-out parked flow back out, used by the
211    /// timer wheel when a `ReceiveTimeout` deadline fires. Returns `None`
212    /// if the flow was already woken by a `Send` in the meantime.
213    pub fn take_parked(&self) -> Result<Option<Box<Flow>>, RuntimeError> {
214        let mut inner = sync_lock::lock(&self.inner, "Mailbox::take_parked")?;
215        inner.parked_filter = WaitFilter::Any;
216        Ok(inner.parked.take())
217    }
218}
219
220impl Default for Mailbox {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226/// Remove one matching hop from `queue`, preserving relative order of
227/// everything else (FIFO skip — never drop non-matching entries).
228fn take_with_filter(queue: &mut VecDeque<Value>, filter: WaitFilter) -> Option<Value> {
229    match filter {
230        WaitFilter::Any => queue.pop_front(),
231        other => {
232            let idx = queue.iter().position(|v| other.matches(v))?;
233            queue.remove(idx)
234        }
235    }
236}
237
238/// Stashes a message that arrived just as we were about to park, so the
239/// worker loop can resume the flow with it on the very next step
240/// without re-entering the mailbox. See [`Mailbox::park`].
241fn with_pending(mut flow: Box<Flow>, value: Value) -> Box<Flow> {
242    flow.pending_message = Some(value);
243    flow
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::bytecode::Message;
250    use crate::scheduler::oneshot;
251    use crate::scheduler::process::{next_flow_id, RestartPolicy};
252    use crate::vm::{NativeTable, Vm};
253    use crate::ChunkBuilder;
254    use std::sync::Arc;
255
256    fn dummy_flow() -> Box<Flow> {
257        let mut b = ChunkBuilder::new("mb");
258        b.begin_function("main", 0, 1);
259        b.emit_return(0);
260        let chunk = b.finish();
261        let vm = Vm::new(Arc::new(chunk), NativeTable::empty(), 0, &[]).expect("vm");
262        let (tx, _rx) = oneshot::channel();
263        Box::new(Flow::new(
264            next_flow_id(),
265            vm,
266            Arc::new(Mailbox::new()),
267            RestartPolicy::Never,
268            tx,
269        ))
270    }
271
272    fn hop(sender: u64, request_id: u64, tag: u16, payload: u64) -> Value {
273        Value::Message(Message::new(sender, request_id, tag, payload))
274    }
275
276    fn msg(tag: u16, payload: u64) -> Value {
277        hop(1, 1, tag, payload)
278    }
279
280    #[test]
281    fn try_pop_match_skips_non_matching_fifo() {
282        let mb = Mailbox::new();
283        mb.push(msg(9, 1)).unwrap();
284        mb.push(msg(1, 42)).unwrap();
285        mb.push(msg(9, 2)).unwrap();
286        let got = mb.try_pop_match(1).unwrap().expect("match");
287        assert_eq!(got.as_message().unwrap().payload, 42);
288        assert_eq!(mb.try_pop().unwrap().unwrap().as_message().unwrap().tag, 9);
289        assert_eq!(mb.try_pop().unwrap().unwrap().as_message().unwrap().payload, 2);
290    }
291
292    #[test]
293    fn push_while_park_match_queues_junk_keeps_waiter() {
294        let mb = Mailbox::new();
295        let flow = dummy_flow();
296        assert!(mb.park_match(flow, 1).unwrap().is_ok());
297        assert!(matches!(mb.push(msg(9, 0)).unwrap(), Delivery::Queued));
298        assert!(matches!(mb.push(msg(1, 7)).unwrap(), Delivery::Handoff(_)));
299        assert_eq!(mb.try_pop().unwrap().unwrap().as_message().unwrap().tag, 9);
300    }
301
302    #[test]
303    fn ask_does_not_consume_reply_for_another_request() {
304        let mb = Mailbox::new();
305        mb.push(hop(10, 2, 2, 99)).unwrap();
306        mb.push(hop(10, 1, 2, 42)).unwrap();
307        let filter = WaitFilter::Correlation {
308            expect_request_id: 1,
309            expect_sender: Some(10),
310        };
311        let got = mb.try_pop_filter(filter).unwrap().expect("id=1");
312        assert_eq!(got.as_message().unwrap().payload, 42);
313        // request_id=2 remains
314        let left = mb.try_pop().unwrap().unwrap();
315        assert_eq!(left.as_message().unwrap().request_id, 2);
316    }
317
318    #[test]
319    fn ask_requires_reply_from_target() {
320        let mb = Mailbox::new();
321        let flow = dummy_flow();
322        let filter = WaitFilter::Correlation {
323            expect_request_id: 1,
324            expect_sender: Some(10), // expect server=10
325        };
326        assert!(mb.park_filter(flow, filter).unwrap().is_ok());
327        // spoof: same request_id, wrong sender
328        assert!(matches!(
329            mb.push(hop(99, 1, 2, 0)).unwrap(),
330            Delivery::Queued
331        ));
332        // real reply from target
333        assert!(matches!(
334            mb.push(hop(10, 1, 2, 42)).unwrap(),
335            Delivery::Handoff(_)
336        ));
337        assert_eq!(mb.try_pop().unwrap().unwrap().as_message().unwrap().sender, 99);
338    }
339}