1mod capacity;
32mod metrics;
33mod policy;
34mod queue;
35
36use std::sync::Mutex;
37
38use crate::bytecode::Value;
39
40use super::error::RuntimeError;
41use super::process::Flow;
42use super::sync_lock;
43
44pub use capacity::{MailboxBytes, MailboxCapacity};
45pub use metrics::MailboxStats;
46pub use policy::{MailboxConfig, OverflowPolicy};
47
48use queue::{EnqueueEffect, MailboxQueue};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub(crate) enum WaitFilter {
56 Any,
58 Tag(u16),
60 Correlation {
68 expect_request_id: u64,
69 expect_sender: Option<u64>,
70 },
71}
72
73impl WaitFilter {
74 #[inline]
75 pub(crate) fn matches(&self, value: &Value) -> bool {
76 match *self {
77 Self::Any => true,
78 Self::Tag(expected_tag) => match value.as_message() {
79 Some(m) => m.tag == expected_tag,
80 None => false,
81 },
82 Self::Correlation {
83 expect_request_id,
84 expect_sender,
85 } => match value.as_message() {
86 Some(m) => {
87 let id_ok = m.request_id == expect_request_id;
88 let sender_ok = match expect_sender {
89 Some(s) => m.sender == s,
90 None => true,
91 };
92 id_ok && sender_ok
93 }
94 None => false,
95 },
96 }
97 }
98}
99
100pub struct Mailbox {
149 inner: Mutex<MailboxInner>,
150 config: MailboxConfig,
151}
152
153struct MailboxInner {
154 queue: MailboxQueue,
155 parked: Option<Box<Flow>>,
156 parked_filter: WaitFilter,
158 wait_epoch: u64,
161 stats: MailboxStats,
162}
163
164pub enum Delivery {
172 Queued,
173 QueuedDropOldest,
174 DroppedNewest,
175 Handoff(Box<Flow>),
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum MailboxFullReason {
187 MessageLimit,
189 ByteLimit,
193}
194
195impl std::fmt::Display for MailboxFullReason {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 MailboxFullReason::MessageLimit => write!(f, "hop count limit"),
199 MailboxFullReason::ByteLimit => write!(f, "byte budget"),
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct MailboxFull {
207 reason: MailboxFullReason,
208}
209
210impl MailboxFull {
211 #[inline]
212 pub const fn reason(self) -> MailboxFullReason {
213 self.reason
214 }
215}
216
217impl std::fmt::Display for MailboxFull {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 write!(f, "mailbox full ({})", self.reason)
220 }
221}
222
223impl std::error::Error for MailboxFull {}
224
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
251pub struct WaitEpoch(u64);
252
253impl WaitEpoch {
254 #[inline]
257 pub const fn get(self) -> u64 {
258 self.0
259 }
260}
261
262impl std::fmt::Display for WaitEpoch {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 write!(f, "wait#{}", self.0)
265 }
266}
267
268impl Mailbox {
269 pub fn new() -> Self {
270 Self::with_config(MailboxConfig::DEFAULT)
271 }
272
273 pub fn with_config(config: MailboxConfig) -> Self {
274 Mailbox {
275 inner: Mutex::new(MailboxInner {
276 queue: MailboxQueue::new(config.capacity().get(), config.bytes().get()),
277 parked: None,
278 parked_filter: WaitFilter::Any,
279 wait_epoch: 0,
280 stats: MailboxStats::default(),
281 }),
282 config,
283 }
284 }
285
286 #[inline]
287 pub fn config(&self) -> MailboxConfig {
288 self.config
289 }
290
291 pub fn stats(&self) -> Result<MailboxStats, RuntimeError> {
295 let inner = sync_lock::lock(&self.inner, "Mailbox::stats")?;
296 let mut stats = inner.stats;
297 stats.queued_messages = inner.queue.len();
298 stats.queued_bytes = inner.queue.bytes();
299 Ok(stats)
300 }
301
302 pub fn push(&self, value: Value) -> Result<Result<Delivery, MailboxFull>, RuntimeError> {
314 let mut inner = sync_lock::lock(&self.inner, "Mailbox::push")?;
315 if let Some(flow) = inner.parked.take() {
316 if inner.parked_filter.matches(&value) {
317 inner.parked_filter = WaitFilter::Any;
318 inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
319 inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
320 return Ok(Ok(Delivery::Handoff(flow)));
321 }
322 inner.parked = Some(flow);
323 return Ok(enqueue_locked(
324 &mut inner,
325 value,
326 self.config.overflow(),
327 ));
328 }
329 Ok(enqueue_locked(
330 &mut inner,
331 value,
332 self.config.overflow(),
333 ))
334 }
335
336 pub fn try_pop(&self) -> Result<Option<Value>, RuntimeError> {
338 self.try_pop_filter(WaitFilter::Any)
339 }
340
341 pub fn try_pop_match(&self, tag: u16) -> Result<Option<Value>, RuntimeError> {
343 self.try_pop_filter(WaitFilter::Tag(tag))
344 }
345
346 pub(crate) fn try_pop_filter(
348 &self,
349 filter: WaitFilter,
350 ) -> Result<Option<Value>, RuntimeError> {
351 let mut inner = sync_lock::lock(&self.inner, "Mailbox::try_pop_filter")?;
352 let got = inner.queue.take(filter);
353 if got.is_some() {
354 inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
355 }
356 Ok(got)
357 }
358
359 pub fn park(&self, flow: Box<Flow>) -> Result<Result<WaitEpoch, Box<Flow>>, RuntimeError> {
372 self.park_filter(flow, WaitFilter::Any)
373 }
374
375 pub fn park_match(
379 &self,
380 flow: Box<Flow>,
381 tag: u16,
382 ) -> Result<Result<WaitEpoch, Box<Flow>>, RuntimeError> {
383 self.park_filter(flow, WaitFilter::Tag(tag))
384 }
385
386 pub(crate) fn park_filter(
389 &self,
390 flow: Box<Flow>,
391 filter: WaitFilter,
392 ) -> Result<Result<WaitEpoch, Box<Flow>>, RuntimeError> {
393 let mut inner = sync_lock::lock(&self.inner, "Mailbox::park_filter")?;
394 if let Some(value) = inner.queue.take(filter) {
395 inner.stats.dequeued = inner.stats.dequeued.saturating_add(1);
396 drop(inner);
397 return Ok(Err(with_pending(flow, value)));
398 }
399 inner.wait_epoch = inner.wait_epoch.wrapping_add(1);
403 let epoch = WaitEpoch(inner.wait_epoch);
404 inner.parked_filter = filter;
405 inner.parked = Some(flow);
406 Ok(Ok(epoch))
407 }
408
409 pub fn take_parked_at(&self, epoch: WaitEpoch) -> Result<Option<Box<Flow>>, RuntimeError> {
422 let mut inner = sync_lock::lock(&self.inner, "Mailbox::take_parked_at")?;
423 if inner.wait_epoch != epoch.0 {
424 return Ok(None);
425 }
426 match inner.parked.take() {
427 Some(flow) => {
428 inner.parked_filter = WaitFilter::Any;
429 Ok(Some(flow))
430 }
431 None => Ok(None),
432 }
433 }
434}
435
436impl Default for Mailbox {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442fn enqueue_locked(
443 inner: &mut MailboxInner,
444 value: Value,
445 policy: OverflowPolicy,
446) -> Result<Delivery, MailboxFull> {
447 match inner.queue.enqueue(value, policy) {
448 Ok(EnqueueEffect::Enqueued) => {
449 inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
450 Ok(Delivery::Queued)
451 }
452 Ok(EnqueueEffect::DroppedOldest) => {
453 inner.stats.dropped_oldest = inner.stats.dropped_oldest.saturating_add(1);
454 inner.stats.enqueued = inner.stats.enqueued.saturating_add(1);
455 Ok(Delivery::QueuedDropOldest)
456 }
457 Ok(EnqueueEffect::DroppedNewest) => {
458 inner.stats.dropped_newest = inner.stats.dropped_newest.saturating_add(1);
459 Ok(Delivery::DroppedNewest)
460 }
461 Err(reason) => {
462 inner.stats.rejected = inner.stats.rejected.saturating_add(1);
463 if reason == MailboxFullReason::ByteLimit {
464 inner.stats.rejected_byte_limit =
465 inner.stats.rejected_byte_limit.saturating_add(1);
466 }
467 Err(MailboxFull { reason })
468 }
469 }
470}
471
472fn with_pending(mut flow: Box<Flow>, value: Value) -> Box<Flow> {
476 flow.pending_message = Some(value);
477 flow
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use crate::bytecode::Message;
484 use crate::scheduler::oneshot;
485 use crate::scheduler::process::{next_flow_id, RestartPolicy};
486 use crate::vm::{NativeTable, Vm};
487 use crate::ChunkBuilder;
488 use std::sync::Arc;
489
490 type TestResult = Result<(), Box<dyn std::error::Error>>;
491
492 fn dummy_flow() -> Result<Box<Flow>, Box<dyn std::error::Error>> {
493 let mut b = ChunkBuilder::new("mb");
494 b.begin_function("main", 0, 1);
495 b.emit_return(0);
496 let chunk = b.finish();
497 let vm = Vm::new(Arc::new(chunk), NativeTable::empty(), 0, &[])?;
498 let (tx, _rx) = oneshot::channel();
499 Ok(Box::new(Flow::new(
500 next_flow_id(),
501 vm,
502 Arc::new(Mailbox::new()),
503 RestartPolicy::Never,
504 tx,
505 )))
506 }
507
508 fn hop(sender: u64, request_id: u64, tag: u16, payload: u64) -> Value {
509 Value::Message(Message::new(sender, request_id, tag, payload))
510 }
511
512 fn msg(tag: u16, payload: u64) -> Value {
513 hop(1, 1, tag, payload)
514 }
515
516 fn tiny_reject(n: u32) -> Result<Mailbox, Box<dyn std::error::Error>> {
517 let cap = MailboxCapacity::new(n).ok_or("invalid mailbox capacity")?;
518 Ok(Mailbox::with_config(MailboxConfig::new(
519 cap,
520 OverflowPolicy::Reject,
521 )))
522 }
523
524 fn hop_msg(value: &Value) -> Result<Message, Box<dyn std::error::Error>> {
525 value.as_message().ok_or("expected Message hop".into())
526 }
527
528 #[test]
529 fn try_pop_match_skips_non_matching_fifo() -> TestResult {
530 let mb = Mailbox::new();
531 mb.push(msg(9, 1))??;
532 mb.push(msg(1, 42))??;
533 mb.push(msg(9, 2))??;
534 let got = mb.try_pop_match(1)?.ok_or("match")?;
535 assert_eq!(hop_msg(&got)?.payload, 42);
536 assert_eq!(
537 hop_msg(&mb.try_pop()?.ok_or("first leftover")?)?.tag,
538 9
539 );
540 assert_eq!(
541 hop_msg(&mb.try_pop()?.ok_or("second leftover")?)?.payload,
542 2
543 );
544 Ok(())
545 }
546
547 #[test]
548 fn push_while_park_match_queues_junk_keeps_waiter() -> TestResult {
549 let mb = Mailbox::new();
550 let flow = dummy_flow()?;
551 assert!(mb.park_match(flow, 1)?.is_ok());
552 assert!(matches!(mb.push(msg(9, 0))??, Delivery::Queued));
553 assert!(matches!(mb.push(msg(1, 7))??, Delivery::Handoff(_)));
554 assert_eq!(hop_msg(&mb.try_pop()?.ok_or("queued junk")?)?.tag, 9);
555 Ok(())
556 }
557
558 #[test]
559 fn ask_does_not_consume_reply_for_another_request() -> TestResult {
560 let mb = Mailbox::new();
561 mb.push(hop(10, 2, 2, 99))??;
562 mb.push(hop(10, 1, 2, 42))??;
563 let filter = WaitFilter::Correlation {
564 expect_request_id: 1,
565 expect_sender: Some(10),
566 };
567 let got = mb.try_pop_filter(filter)?.ok_or("id=1")?;
568 assert_eq!(hop_msg(&got)?.payload, 42);
569 let left = mb.try_pop()?.ok_or("leftover")?;
570 assert_eq!(hop_msg(&left)?.request_id, 2);
571 Ok(())
572 }
573
574 #[test]
575 fn ask_requires_reply_from_target() -> TestResult {
576 let mb = Mailbox::new();
577 let flow = dummy_flow()?;
578 let filter = WaitFilter::Correlation {
579 expect_request_id: 1,
580 expect_sender: Some(10),
581 };
582 assert!(mb.park_filter(flow, filter)?.is_ok());
583 assert!(matches!(mb.push(hop(99, 1, 2, 0))??, Delivery::Queued));
584 assert!(matches!(mb.push(hop(10, 1, 2, 42))??, Delivery::Handoff(_)));
585 assert_eq!(
586 hop_msg(&mb.try_pop()?.ok_or("non-matching queued")?)?.sender,
587 99
588 );
589 Ok(())
590 }
591
592 #[test]
593 fn reject_when_full_without_waiter() -> TestResult {
594 let mb = tiny_reject(1)?;
595 assert!(matches!(mb.push(msg(1, 1))??, Delivery::Queued));
596 match mb.push(msg(1, 2))? {
600 Err(full) => assert_eq!(full.reason(), MailboxFullReason::MessageLimit),
601 Ok(_) => return Err("expected the hop count bound to refuse".into()),
602 }
603 let s = mb.stats()?;
604 assert_eq!(s.enqueued, 1);
605 assert_eq!(s.rejected, 1);
606 assert_eq!(s.rejected_byte_limit, 0);
607 assert_eq!(s.queued_messages, 1);
608 Ok(())
609 }
610
611 fn park_now(mb: &Mailbox, flow: Box<Flow>) -> Result<WaitEpoch, Box<dyn std::error::Error>> {
614 match mb.park(flow)? {
615 Ok(epoch) => Ok(epoch),
616 Err(_) => Err("an empty mailbox should have parked the flow".into()),
617 }
618 }
619
620 fn handoff(mb: &Mailbox, value: Value) -> Result<Box<Flow>, Box<dyn std::error::Error>> {
621 match mb.push(value)?? {
622 Delivery::Handoff(flow) => Ok(flow),
623 other => Err(format!("expected a handoff, got {}", delivery_name(&other)).into()),
624 }
625 }
626
627 fn delivery_name(d: &Delivery) -> &'static str {
628 match d {
629 Delivery::Queued => "Queued",
630 Delivery::QueuedDropOldest => "QueuedDropOldest",
631 Delivery::DroppedNewest => "DroppedNewest",
632 Delivery::Handoff(_) => "Handoff",
633 }
634 }
635
636 #[test]
637 fn a_stale_deadline_cannot_steal_a_later_wait() -> TestResult {
638 let mb = Mailbox::new();
639 let first = park_now(&mb, dummy_flow()?)?;
640 let woken = handoff(&mb, msg(1, 1))?;
642 let second = park_now(&mb, woken)?;
644 assert_ne!(first, second, "each park must get its own epoch");
645
646 assert!(mb.take_parked_at(first)?.is_none());
650 assert!(mb.take_parked_at(second)?.is_some());
652 Ok(())
653 }
654
655 #[test]
656 fn a_stale_deadline_does_not_downgrade_a_selective_waiter() -> TestResult {
657 let mb = Mailbox::new();
658 let first = park_now(&mb, dummy_flow()?)?;
659 let woken = handoff(&mb, msg(1, 1))?;
660 let second = match mb.park_match(woken, 7)? {
662 Ok(epoch) => epoch,
663 Err(_) => return Err("empty mailbox should have parked the flow".into()),
664 };
665 assert_ne!(first, second);
666
667 assert!(mb.take_parked_at(first)?.is_none());
668 assert!(matches!(mb.push(msg(9, 0))??, Delivery::Queued));
671 assert!(matches!(mb.push(msg(7, 0))??, Delivery::Handoff(_)));
673 Ok(())
674 }
675
676 #[test]
677 fn a_deadline_for_a_wait_that_a_hop_ended_does_nothing() -> TestResult {
678 let mb = Mailbox::new();
679 let epoch = park_now(&mb, dummy_flow()?)?;
680 let _woken = handoff(&mb, msg(1, 1))?;
681 assert!(mb.take_parked_at(epoch)?.is_none());
684 Ok(())
685 }
686
687 #[test]
688 fn byte_budget_refuses_before_the_hop_count_and_says_so() -> TestResult {
689 let cap = MailboxCapacity::new(64).ok_or("cap")?;
691 let budget = MailboxBytes::new(MailboxBytes::MIN).ok_or("bytes")?;
692 let mb = Mailbox::with_config(
693 MailboxConfig::new(cap, OverflowPolicy::Reject).with_bytes(budget),
694 );
695 assert!(matches!(mb.push(Value::bytes(vec![0u8; 900]))??, Delivery::Queued));
696 match mb.push(Value::bytes(vec![0u8; 900]))? {
697 Err(full) => assert_eq!(full.reason(), MailboxFullReason::ByteLimit),
698 Ok(_) => return Err("expected the byte budget to refuse".into()),
699 }
700 let s = mb.stats()?;
701 assert_eq!(s.queued_messages, 1);
702 assert!(s.queued_bytes >= 900);
703 assert_eq!(s.rejected, 1);
704 assert_eq!(s.rejected_byte_limit, 1);
705 Ok(())
706 }
707
708 #[test]
709 fn draining_a_hop_frees_its_byte_charge() -> TestResult {
710 let cap = MailboxCapacity::new(64).ok_or("cap")?;
711 let budget = MailboxBytes::new(MailboxBytes::MIN).ok_or("bytes")?;
712 let mb = Mailbox::with_config(
713 MailboxConfig::new(cap, OverflowPolicy::Reject).with_bytes(budget),
714 );
715 mb.push(Value::bytes(vec![0u8; 900]))??;
716 mb.try_pop()?.ok_or("queued blob")?;
717 assert_eq!(mb.stats()?.queued_bytes, 0);
718 assert!(matches!(mb.push(Value::bytes(vec![0u8; 900]))??, Delivery::Queued));
721 Ok(())
722 }
723
724 #[test]
725 fn matching_handoff_does_not_count_as_full() -> TestResult {
726 let mb = tiny_reject(1)?;
727 mb.push(msg(9, 0))??;
728 let flow = dummy_flow()?;
729 assert!(mb.park_match(flow, 1)?.is_ok());
730 assert!(matches!(mb.push(msg(1, 7))??, Delivery::Handoff(_)));
731 assert_eq!(hop_msg(&mb.try_pop()?.ok_or("queued")?)?.tag, 9);
732 Ok(())
733 }
734
735 #[test]
736 fn drop_oldest_still_wakes_on_match() -> TestResult {
737 let cap = MailboxCapacity::new(1).ok_or("cap")?;
738 let mb = Mailbox::with_config(MailboxConfig::new(cap, OverflowPolicy::DropOldest));
739 let flow = dummy_flow()?;
740 assert!(mb.park_match(flow, 1)?.is_ok());
741 mb.push(msg(9, 1))??;
742 assert!(matches!(mb.push(msg(1, 2))??, Delivery::Handoff(_)));
743 Ok(())
744 }
745}