Skip to main content

byteflow/scheduler/mailbox/
policy.rs

1/// What happens when a hop would exceed the mailbox's **logical** capacity.
2///
3/// # Why there is no `Block`
4///
5/// Parking the **OS worker** on a full mailbox would stall every other flow
6/// on that thread — the opposite of an M:N scheduler. Backpressure that
7/// waits belongs to scheduler-level `WAITING_SEND` (one parked sender
8/// woken per freed slot), not to a queue method that holds a worker.
9///
10/// This revision therefore only offers **non-blocking** overflow:
11///
12/// | Policy | Effect |
13/// |--------|--------|
14/// | [`Reject`](Self::Reject) | Hop is not accepted; caller sees [`super::MailboxFull`] / [`crate::SendError::MailboxFull`] |
15/// | [`DropNewest`](Self::DropNewest) | Incoming hop discarded; queue unchanged |
16/// | [`DropOldest`](Self::DropOldest) | Oldest queued hop dropped, incoming enqueued (still FIFO among survivors) |
17///
18/// Default is [`Reject`](Self::Reject): fail-closed. Silent drop is an
19/// explicit embedder choice.
20#[repr(u8)]
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum OverflowPolicy {
23    #[default]
24    Reject = 0,
25    DropNewest = 1,
26    DropOldest = 2,
27}
28
29/// Memory + overflow contract for every mailbox spawned by a [`crate::Runtime`].
30///
31/// Fields are private so an out-of-range capacity or byte budget cannot
32/// sneak in. Construct with [`MailboxConfig::new`] or
33/// [`MailboxConfig::DEFAULT`], then narrow the byte budget with
34/// [`MailboxConfig::with_bytes`].
35///
36/// Both bounds apply; the first one reached refuses the hop. See
37/// [`super::MailboxBytes`] for why the hop count alone is not a memory
38/// bound.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct MailboxConfig {
41    capacity: super::MailboxCapacity,
42    bytes: super::MailboxBytes,
43    overflow: OverflowPolicy,
44}
45
46impl MailboxConfig {
47    /// Compile-time-valid default: 256 hops, 4 MiB, reject on overflow.
48    pub const DEFAULT: MailboxConfig = MailboxConfig {
49        capacity: super::MailboxCapacity::DEFAULT,
50        bytes: super::MailboxBytes::DEFAULT,
51        overflow: OverflowPolicy::Reject,
52    };
53
54    /// Capacity + policy, with the default byte budget.
55    ///
56    /// The byte budget is a separate builder step rather than a third
57    /// parameter so embedders that only care about hop count are not
58    /// forced to reason about bytes to keep compiling.
59    pub const fn new(capacity: super::MailboxCapacity, overflow: OverflowPolicy) -> Self {
60        Self {
61            capacity,
62            bytes: super::MailboxBytes::DEFAULT,
63            overflow,
64        }
65    }
66
67    /// Replace the byte budget.
68    pub const fn with_bytes(self, bytes: super::MailboxBytes) -> Self {
69        Self { bytes, ..self }
70    }
71
72    #[inline]
73    pub const fn capacity(&self) -> super::MailboxCapacity {
74        self.capacity
75    }
76
77    #[inline]
78    pub const fn bytes(&self) -> super::MailboxBytes {
79        self.bytes
80    }
81
82    #[inline]
83    pub const fn overflow(&self) -> OverflowPolicy {
84        self.overflow
85    }
86}
87
88impl Default for MailboxConfig {
89    fn default() -> Self {
90        Self::DEFAULT
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::scheduler::mailbox::{MailboxBytes, MailboxCapacity};
98
99    #[test]
100    fn default_is_reject_256() {
101        let c = MailboxConfig::DEFAULT;
102        assert_eq!(c.capacity().get(), 256);
103        assert_eq!(c.bytes().get(), MailboxBytes::DEFAULT.get());
104        assert_eq!(c.overflow(), OverflowPolicy::Reject);
105    }
106
107    #[test]
108    fn new_preserves_parts() -> Result<(), &'static str> {
109        let cap = MailboxCapacity::new(4).ok_or("cap")?;
110        let c = MailboxConfig::new(cap, OverflowPolicy::DropOldest);
111        assert_eq!(c.capacity().get(), 4);
112        assert_eq!(c.overflow(), OverflowPolicy::DropOldest);
113        assert_eq!(c.bytes().get(), MailboxBytes::DEFAULT.get());
114        Ok(())
115    }
116
117    #[test]
118    fn with_bytes_narrows_only_the_budget() -> Result<(), &'static str> {
119        let cap = MailboxCapacity::new(4).ok_or("cap")?;
120        let budget = MailboxBytes::new(8192).ok_or("bytes")?;
121        let c = MailboxConfig::new(cap, OverflowPolicy::DropOldest).with_bytes(budget);
122        assert_eq!(c.bytes().get(), 8192);
123        assert_eq!(c.capacity().get(), 4);
124        assert_eq!(c.overflow(), OverflowPolicy::DropOldest);
125        Ok(())
126    }
127}