bb_runtime/node/config.rs
1//! Construction-time configuration for a `Node`.
2//!
3//! The local peer identity is required at construction; every
4//! other field carries a production-conservative default
5//! (cycle-op budget, in-flight async cap, outbound-queue cap,
6//! bus capacity).
7
8use crate::framework::{
9 DEFAULT_HIGH_WATER_PCT, DEFAULT_K_BEFORE_SILENT, DEFAULT_MIN_NOTICE_INTERVAL_NS,
10};
11use crate::ids::PeerId;
12
13use crate::envelope::EnvelopeCaps;
14
15/// Default per-cycle Op budget. `Some(1000)` yields voluntarily
16/// after 1000 op-invocations per poll; an emit of
17/// `EngineStep::CycleBudgetExceeded` tells the host the engine
18/// paused so other work can run. `None` disables the budget guard.
19pub const DEFAULT_CYCLE_OP_BUDGET: Option<usize> = Some(1000);
20
21/// Default cap on the number of in-flight `DispatchResult::Async`
22/// commands. `Some(10_000)` rejects further async dispatches with
23/// `OpError("pending-async limit exceeded")` once the cap is hit -
24/// protects against a runaway component returning `Async(_)` in a
25/// tight loop. `None` disables the cap.
26pub const DEFAULT_MAX_PENDING_ASYNC: Option<usize> = Some(10_000);
27
28/// Default cap on the outbound envelope queue depth. `Some(10_000)`
29/// drops the oldest envelope when the cap is hit (FIFO drop) and
30/// emits `EngineStep::OutboundDropped`. `None` disables the cap.
31pub const DEFAULT_MAX_OUTBOUND_QUEUE: Option<usize> = Some(10_000);
32
33/// Default bus capacity.
34pub const DEFAULT_BUS_CAPACITY: usize = 1024;
35
36/// Default per-target-boundary-hop
37/// budget for sizing async deadlines on wire.Send NodeProtos.
38/// 100 ms in nanoseconds. The compiler's `analyze_wire_dependencies`
39/// stamps a static `chain_depth` count on each Send; the engine
40/// multiplies by this budget at deadline-stamp time so a Send whose
41/// downstream chain reaches `N` target boundaries gets `N *
42/// per_hop_budget_ns` to respond.
43pub const DEFAULT_PER_HOP_BUDGET_NS: u64 = 100_000_000;
44
45/// Default cap on total in-flight ingress bytes the engine may hold
46/// across the ingress queue, slot table, and pending async
47/// completion buffers at any instant. 256 MiB. Wire and application
48/// boundaries `try_charge` against this cap before admitting a
49/// payload; overflow emits the appropriate `BudgetExceeded`
50/// `InfraEvent` and drops the offending bytes.
51pub const DEFAULT_INGRESS_BYTE_BUDGET: usize = 256 * 1024 * 1024;
52
53/// Default per-`AppEvent` payload cap consulted by
54/// `Node::deliver_event`. 1 MiB. Oversize payloads return
55/// `DeliveryError::OversizePayload` synchronously AND emit
56/// `AppIngressError { kind: PerItemCapExceeded }` on the bus.
57pub const DEFAULT_MAX_APP_EVENT_BYTES: usize = 1024 * 1024;
58
59/// Default per-`Invoke` input-count cap. 100 inputs. Caller-side
60/// guard against pathological `invoke()` calls; the cap rejects
61/// before any per-input allocation runs.
62pub const DEFAULT_MAX_INVOKE_INPUTS: usize = 100;
63
64/// Default per-`Invoke` cumulative payload cap. 10 MiB. Sum of
65/// every `(name, bytes)` entry's payload length.
66pub const DEFAULT_MAX_INVOKE_BYTES: usize = 10 * 1024 * 1024;
67
68/// Default per-`CompletionHandle` result-payload cap. 4 MiB. Larger
69/// completions emit `AppIngressError { kind: PerItemCapExceeded }`
70/// and the component sees an async-op timeout in place of the
71/// dropped completion.
72pub const DEFAULT_MAX_COMPLETION_RESULT_BYTES: usize = 4 * 1024 * 1024;
73
74/// Edge preset for [`NodeConfig::ingress_byte_budget`]. 8 MiB.
75pub const EDGE_INGRESS_BYTE_BUDGET: usize = 8 * 1024 * 1024;
76
77/// Edge preset for [`NodeConfig::max_app_event_bytes`]. 64 KiB.
78pub const EDGE_MAX_APP_EVENT_BYTES: usize = 64 * 1024;
79
80/// Edge preset for [`NodeConfig::max_invoke_inputs`]. 16 inputs.
81pub const EDGE_MAX_INVOKE_INPUTS: usize = 16;
82
83/// Edge preset for [`NodeConfig::max_invoke_bytes`]. 256 KiB.
84pub const EDGE_MAX_INVOKE_BYTES: usize = 256 * 1024;
85
86/// Edge preset for [`NodeConfig::max_completion_result_bytes`].
87/// 64 KiB.
88pub const EDGE_MAX_COMPLETION_RESULT_BYTES: usize = 64 * 1024;
89
90/// Construction-time configuration for a `Node`.
91#[derive(Clone, Debug)]
92pub struct NodeConfig {
93 /// Local peer identity. Required at construction - the Node
94 /// holds its own identity from the moment it exists.
95 pub peer_id: PeerId,
96
97 /// Soft per-poll-cycle budget - the engine voluntarily yields
98 /// after N op-invocations to honor caller-side backpressure.
99 /// `None` disables the budget guard.
100 pub cycle_op_budget: Option<usize>,
101
102 /// Cap on the number of in-flight `DispatchResult::Async`
103 /// commands. When the cap is hit, further async dispatches
104 /// fail synchronously with `OpError("pending-async limit
105 /// exceeded")`. `None` disables the cap.
106 pub max_pending_async: Option<usize>,
107
108 /// Cap on the outbound envelope queue depth. When the cap is
109 /// hit, the oldest envelope is dropped (FIFO) and a count
110 /// surfaces via `EngineStep::OutboundDropped`. `None` disables
111 /// the cap.
112 pub max_outbound_queue: Option<usize>,
113
114 /// Bus capacity. Overflow drops the oldest event + bumps a
115 /// counter.
116 pub bus_capacity: usize,
117
118 /// Per-target-boundary budget in nanoseconds. The Engine's
119 /// deadline-stamping path multiplies this by the static
120 /// `chain_depth` metadata on each outbound `wire.Send` to size
121 /// the call's deadline. Sized to represent the worst-case
122 /// round-trip cost of one network boundary; chains crossing
123 /// multiple boundaries pay the multiplier.
124 pub per_hop_budget_ns: u64,
125
126 /// inbound envelope decode caps the
127 /// [`crate::envelope::EnvelopeCodec::decode_capped`] consults
128 /// on every inbound buffer. Production defaults match the
129 /// design's "16 MiB / 256 / 4 MiB / 4 KiB" recommendation;
130 /// edge deployments use [`EnvelopeCaps::edge()`] for tighter
131 /// bounds (256 KiB / 16 / 64 KiB / 512). Custom caps via
132 /// [`Self::with_envelope_caps`].
133 pub envelope_caps: EnvelopeCaps,
134
135 /// Receiver-side back-pressure high-water mark, as a percentage
136 /// of the ingress queue capacity. Once ingress depth reaches
137 /// this fraction, the framework emits a `BackoffNotice` envelope
138 /// to each contributing sender per the backpressure protocol
139 /// design at
140 /// `docs/internal/superpowers/specs/2026-06-23-backpressure-runtime.md`
141 /// §6. Default [`DEFAULT_HIGH_WATER_PCT`] (75).
142 pub backpressure_high_water_pct: u8,
143
144 /// K = notices-without-recovery before the receiver transitions
145 /// the sender to silent-drop mode. Default
146 /// [`DEFAULT_K_BEFORE_SILENT`] (3) - matches the
147 /// `RttEma::is_warm` threshold at
148 /// `bb-runtime/src/framework/rtt_tracker.rs:126-128`.
149 pub backpressure_k_before_silent: u32,
150
151 /// Minimum interval enforced between successive `BackoffNotice`
152 /// emissions to the same sender. Acts as a hard lower bound on
153 /// the duplicate-suppression window so a flood of inbound
154 /// envelopes from one peer produces at most one notice per
155 /// interval. Default [`DEFAULT_MIN_NOTICE_INTERVAL_NS`]
156 /// (1 second).
157 pub backpressure_min_notice_interval_ns: u64,
158
159 /// Total in-flight ingress bytes the engine may hold across the
160 /// ingress queue + slot table + pending async completion buffers
161 /// at any instant. Wire and application ingress boundaries
162 /// `try_charge` against this cap before installing a payload; on
163 /// overflow the offending bytes are dropped and the appropriate
164 /// `BudgetExceeded` `InfraEvent` is emitted. Default
165 /// [`DEFAULT_INGRESS_BYTE_BUDGET`] (256 MiB); edge preset
166 /// [`EDGE_INGRESS_BYTE_BUDGET`] (8 MiB).
167 pub ingress_byte_budget: usize,
168
169 /// Per-`AppEvent` payload cap (host-driven push). Default
170 /// [`DEFAULT_MAX_APP_EVENT_BYTES`] (1 MiB); edge preset
171 /// [`EDGE_MAX_APP_EVENT_BYTES`] (64 KiB). On overflow,
172 /// `Node::deliver_event` returns `DeliveryError::OversizePayload`
173 /// AND emits `AppIngressError { kind: PerItemCapExceeded }`.
174 pub max_app_event_bytes: usize,
175
176 /// Per-`Invoke` input-count cap. Default
177 /// [`DEFAULT_MAX_INVOKE_INPUTS`] (100); edge preset
178 /// [`EDGE_MAX_INVOKE_INPUTS`] (16). Caps the number of
179 /// `(name, bytes)` pairs an `invoke()` call may carry before any
180 /// per-input allocation runs.
181 pub max_invoke_inputs: usize,
182
183 /// Per-`Invoke` cumulative payload cap. Default
184 /// [`DEFAULT_MAX_INVOKE_BYTES`] (10 MiB); edge preset
185 /// [`EDGE_MAX_INVOKE_BYTES`] (256 KiB). Sum of every input's
186 /// payload length crossing the boundary.
187 pub max_invoke_bytes: usize,
188
189 /// Per-`CompletionHandle` result-payload cap. Default
190 /// [`DEFAULT_MAX_COMPLETION_RESULT_BYTES`] (4 MiB); edge preset
191 /// [`EDGE_MAX_COMPLETION_RESULT_BYTES`] (64 KiB). The detail
192 /// string on `fail()` is independently capped at 4 KiB (truncated
193 /// rather than rejected). Component sees an async-op timeout in
194 /// place of a dropped completion.
195 pub max_completion_result_bytes: usize,
196}
197
198impl NodeConfig {
199 /// Construct with the given local peer identity and
200 /// production-conservative defaults for every other field.
201 pub fn new(peer_id: PeerId) -> Self {
202 Self {
203 peer_id,
204 cycle_op_budget: DEFAULT_CYCLE_OP_BUDGET,
205 max_pending_async: DEFAULT_MAX_PENDING_ASYNC,
206 max_outbound_queue: DEFAULT_MAX_OUTBOUND_QUEUE,
207 bus_capacity: DEFAULT_BUS_CAPACITY,
208 per_hop_budget_ns: DEFAULT_PER_HOP_BUDGET_NS,
209 envelope_caps: EnvelopeCaps::default(),
210 backpressure_high_water_pct: DEFAULT_HIGH_WATER_PCT,
211 backpressure_k_before_silent: DEFAULT_K_BEFORE_SILENT,
212 backpressure_min_notice_interval_ns: DEFAULT_MIN_NOTICE_INTERVAL_NS,
213 ingress_byte_budget: DEFAULT_INGRESS_BYTE_BUDGET,
214 max_app_event_bytes: DEFAULT_MAX_APP_EVENT_BYTES,
215 max_invoke_inputs: DEFAULT_MAX_INVOKE_INPUTS,
216 max_invoke_bytes: DEFAULT_MAX_INVOKE_BYTES,
217 max_completion_result_bytes: DEFAULT_MAX_COMPLETION_RESULT_BYTES,
218 }
219 }
220
221 /// Convenience constructor with the tighter edge-device presets
222 /// applied to every cap: envelope caps (256 KiB / 16 / 64 KiB /
223 /// 512), ingress budget (8 MiB), per-`AppEvent` (64 KiB),
224 /// per-`Invoke` (16 inputs / 256 KiB), and per-`Completion`
225 /// (64 KiB).
226 pub fn new_edge(peer_id: PeerId) -> Self {
227 Self {
228 envelope_caps: EnvelopeCaps::edge(),
229 ingress_byte_budget: EDGE_INGRESS_BYTE_BUDGET,
230 max_app_event_bytes: EDGE_MAX_APP_EVENT_BYTES,
231 max_invoke_inputs: EDGE_MAX_INVOKE_INPUTS,
232 max_invoke_bytes: EDGE_MAX_INVOKE_BYTES,
233 max_completion_result_bytes: EDGE_MAX_COMPLETION_RESULT_BYTES,
234 ..Self::new(peer_id)
235 }
236 }
237
238 /// Override the inbound envelope decode caps.
239 pub fn with_envelope_caps(mut self, caps: EnvelopeCaps) -> Self {
240 self.envelope_caps = caps;
241 self
242 }
243
244 /// Cap how many op-invocations a single `Node::poll()` may issue
245 /// before voluntarily yielding back to the host.
246 pub fn with_cycle_op_budget(mut self, budget: usize) -> Self {
247 self.cycle_op_budget = Some(budget);
248 self
249 }
250
251 /// Disable the per-cycle op budget - let `poll()` drain the
252 /// frontier to quiescence in a single cycle.
253 pub fn without_cycle_op_budget(mut self) -> Self {
254 self.cycle_op_budget = None;
255 self
256 }
257
258 /// Cap how many in-flight `DispatchResult::Async` commands the
259 /// engine will hold at once.
260 pub fn with_max_pending_async(mut self, cap: usize) -> Self {
261 self.max_pending_async = Some(cap);
262 self
263 }
264
265 /// Disable the pending-async cap - the engine accepts any
266 /// number of in-flight commands.
267 pub fn without_max_pending_async(mut self) -> Self {
268 self.max_pending_async = None;
269 self
270 }
271
272 /// Cap the outbound envelope queue depth.
273 pub fn with_max_outbound_queue(mut self, cap: usize) -> Self {
274 self.max_outbound_queue = Some(cap);
275 self
276 }
277
278 /// Disable the outbound queue cap.
279 pub fn without_max_outbound_queue(mut self) -> Self {
280 self.max_outbound_queue = None;
281 self
282 }
283
284 /// Override the bus capacity (default
285 /// [`DEFAULT_BUS_CAPACITY`]).
286 pub fn with_bus_capacity(mut self, capacity: usize) -> Self {
287 self.bus_capacity = capacity;
288 self
289 }
290
291 /// Override the per-target-boundary budget in nanoseconds
292 /// (default [`DEFAULT_PER_HOP_BUDGET_NS`]). Deployments with
293 /// known-slow links can dial this up; tightly-coupled
294 /// deployments can dial it down.
295 pub fn with_per_hop_budget_ns(mut self, budget_ns: u64) -> Self {
296 self.per_hop_budget_ns = budget_ns;
297 self
298 }
299
300 /// Override the receiver-side back-pressure high-water mark
301 /// percentage (default [`DEFAULT_HIGH_WATER_PCT`]). Clamped to
302 /// `1..=100` by the `BackpressureTracker` constructor.
303 pub fn with_backpressure_high_water_pct(mut self, pct: u8) -> Self {
304 self.backpressure_high_water_pct = pct;
305 self
306 }
307
308 /// Override K (notices-without-recovery before silent-drop;
309 /// default [`DEFAULT_K_BEFORE_SILENT`]). Clamped to at least 1
310 /// by the `BackpressureTracker` constructor.
311 pub fn with_backpressure_k_before_silent(mut self, k: u32) -> Self {
312 self.backpressure_k_before_silent = k;
313 self
314 }
315
316 /// Override the minimum interval between successive notices to
317 /// the same peer (default [`DEFAULT_MIN_NOTICE_INTERVAL_NS`]).
318 /// Clamped to at least 1 by the `BackpressureTracker`
319 /// constructor.
320 pub fn with_backpressure_min_notice_interval_ns(mut self, interval_ns: u64) -> Self {
321 self.backpressure_min_notice_interval_ns = interval_ns;
322 self
323 }
324
325 /// Override the cumulative ingress byte budget.
326 pub fn with_ingress_byte_budget(mut self, bytes: usize) -> Self {
327 self.ingress_byte_budget = bytes;
328 self
329 }
330
331 /// Override the per-`AppEvent` payload cap.
332 pub fn with_max_app_event_bytes(mut self, bytes: usize) -> Self {
333 self.max_app_event_bytes = bytes;
334 self
335 }
336
337 /// Override the per-`Invoke` input-count cap.
338 pub fn with_max_invoke_inputs(mut self, count: usize) -> Self {
339 self.max_invoke_inputs = count;
340 self
341 }
342
343 /// Override the per-`Invoke` cumulative payload cap.
344 pub fn with_max_invoke_bytes(mut self, bytes: usize) -> Self {
345 self.max_invoke_bytes = bytes;
346 self
347 }
348
349 /// Override the per-`CompletionHandle` result cap.
350 pub fn with_max_completion_result_bytes(mut self, bytes: usize) -> Self {
351 self.max_completion_result_bytes = bytes;
352 self
353 }
354}
355