Skip to main content

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