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}