Skip to main content

byteflow/scheduler/mailbox/
capacity.rs

1/// Logical hop bound for one flow's inbox.
2///
3/// # Why not `usize`
4///
5/// A raw `capacity: usize` lets a 64-bit host pass an absurd value and
6/// pretend it is a memory contract. An embeddable M:N runtime must pick
7/// deliberate bounds: enough hops for request/reply bursts, not enough to
8/// grow RSS until OOM when a slow consumer sits under a fast producer.
9///
10/// `MIN = 1` (a mailbox that cannot hold a hop is not a mailbox).
11/// `MAX = 1 << 20` (~1M hops) is a hard ceiling, not a recommendation —
12/// the default is [`MailboxCapacity::DEFAULT`] (256).
13#[repr(transparent)]
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct MailboxCapacity(u32);
16
17impl MailboxCapacity {
18    pub const MIN: u32 = 1;
19    pub const MAX: u32 = 1 << 20;
20    /// Default logical bound used by [`super::MailboxConfig::DEFAULT`].
21    pub const DEFAULT: MailboxCapacity = MailboxCapacity(256);
22
23    /// `Some` iff `value` is in `MIN..=MAX`.
24    pub const fn new(value: u32) -> Option<Self> {
25        if value < Self::MIN || value > Self::MAX {
26            None
27        } else {
28            Some(Self(value))
29        }
30    }
31
32    #[inline]
33    pub const fn get(self) -> usize {
34        self.0 as usize
35    }
36
37    #[inline]
38    pub const fn get_u32(self) -> u32 {
39        self.0
40    }
41}
42
43impl Default for MailboxCapacity {
44    fn default() -> Self {
45        Self::DEFAULT
46    }
47}
48
49/// Byte budget for one flow's inbox, enforced alongside
50/// [`MailboxCapacity`].
51///
52/// # Why a hop count is not a memory bound
53///
54/// [`MailboxCapacity`] bounds *how many* hops an inbox holds. Since ABI v4
55/// a hop may carry [`crate::Value::Str`] / [`crate::Value::Bytes`], and the
56/// `.bf` decoder accepts blobs up to 1 MiB, so "256 hops" is anywhere from
57/// ~12 KiB of scalars to ~256 MiB of blobs. A count alone therefore does
58/// not bound RSS — which is the whole point of bounding the mailbox.
59///
60/// Cost per hop comes from [`crate::Value::memory_size`]; read its doc for
61/// why `Arc`-shared payloads are deliberately over-charged.
62///
63/// # Choosing the default
64///
65/// `DEFAULT` is 4 MiB, which must stay **strictly greater** than the
66/// largest single hop the `.bf` decoder will produce (`MAX_BLOB` in
67/// [`crate::decode`]'s module, 1 MiB) plus its envelope. A budget equal to
68/// the maximum payload size would make a legal, decodable constant
69/// permanently undeliverable under the default config — a bound should
70/// refuse abuse, not valid inputs. `byte_budget_default_fits_one_max_hop`
71/// guards that relationship.
72///
73/// `MIN = 1 KiB` (an inbox that cannot hold a handful of scalar hops is
74/// not an inbox). `MAX = 1 GiB` is a hard ceiling, not a recommendation.
75#[repr(transparent)]
76#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub struct MailboxBytes(usize);
78
79impl MailboxBytes {
80    pub const MIN: usize = 1 << 10;
81    pub const MAX: usize = 1 << 30;
82    /// Default byte budget used by [`super::MailboxConfig::DEFAULT`]:
83    /// 4 MiB. See the type doc for why it is not 1 MiB.
84    pub const DEFAULT: MailboxBytes = MailboxBytes(1 << 22);
85
86    /// `Some` iff `value` is in `MIN..=MAX`.
87    pub const fn new(value: usize) -> Option<Self> {
88        if value < Self::MIN || value > Self::MAX {
89            None
90        } else {
91            Some(Self(value))
92        }
93    }
94
95    #[inline]
96    pub const fn get(self) -> usize {
97        self.0
98    }
99}
100
101impl Default for MailboxBytes {
102    fn default() -> Self {
103        Self::DEFAULT
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn rejects_zero_and_over_max() -> Result<(), &'static str> {
113        assert!(MailboxCapacity::new(0).is_none());
114        assert!(MailboxCapacity::new(MailboxCapacity::MAX + 1).is_none());
115        let min = MailboxCapacity::new(1).ok_or("min capacity")?;
116        assert_eq!(min.get(), 1);
117        let max = MailboxCapacity::new(MailboxCapacity::MAX).ok_or("max capacity")?;
118        assert_eq!(max.get_u32(), MailboxCapacity::MAX);
119        Ok(())
120    }
121
122    #[test]
123    fn default_is_compile_time_valid() {
124        assert_eq!(MailboxCapacity::DEFAULT.get(), 256);
125        assert!(MailboxCapacity::new(256).is_some());
126    }
127
128    #[test]
129    fn byte_budget_rejects_out_of_range() -> Result<(), &'static str> {
130        assert!(MailboxBytes::new(0).is_none());
131        assert!(MailboxBytes::new(MailboxBytes::MIN - 1).is_none());
132        assert!(MailboxBytes::new(MailboxBytes::MAX + 1).is_none());
133        let min = MailboxBytes::new(MailboxBytes::MIN).ok_or("min bytes")?;
134        assert_eq!(min.get(), MailboxBytes::MIN);
135        Ok(())
136    }
137
138    #[test]
139    fn byte_budget_default_holds_a_full_scalar_inbox() {
140        // DEFAULT must not reject a mailbox filled to DEFAULT capacity with
141        // scalar hops, or the byte bound would shadow the hop bound.
142        let worst = MailboxCapacity::DEFAULT.get() * std::mem::size_of::<crate::Value>();
143        assert!(MailboxBytes::DEFAULT.get() > worst);
144    }
145
146    #[test]
147    fn byte_budget_default_fits_one_max_hop() {
148        // `MAX_BLOB` in `bytecode::format` (private) caps a decoded
149        // `Str`/`Bytes` constant at 1 MiB. The default budget must fit one
150        // of those *plus* its envelope, or the decoder would happily accept
151        // a constant that no mailbox could ever receive.
152        const MAX_BLOB: usize = 1_048_576;
153        let biggest = crate::Value::bytes(vec![0u8; MAX_BLOB]);
154        assert!(biggest.memory_size() <= MailboxBytes::DEFAULT.get());
155    }
156}