degenbot_workers/plan.rs
1//! `plan` — the `FleetPlan` tiered boot authority (FLEETFLOOR FF-T2 / MEBF4V).
2//!
3//! LW-T4 made the budget the sole sizing authority with ONE
4//! floor: below the pinned-role floor the boot refused. FF-T2 generalizes
5//! that one floor into ordered HOST TIERS, one pure function of the
6//! budget:
7//!
8//! - pinned: the pinned-role derivation succeeds (floor(Q) >= H+A+R+M+2)
9//! — today's topology, byte-stable (the derivation is the ONE
10//! `FleetBudget::derive`, unchanged);
11//! - serial: 2-5 core hosts (the pinned derivation refuses but the host
12//! has the 2-core minimum: one core for I/O work, one core for solve
13//! work) — the arm itself lands with FF-T4; until then the boot
14//! refuses with the tier's own typed refusal, never a silent narrow
15//! (the reth `has_enough_parallelism` lesson: lane capability is
16//! explicit);
17//! - refused: below 2 cores — `BudgetError::BelowHostFloor`, typed.
18//!
19//! Forced bindings (`runtime.fleet_profile = pinned | serial`, env
20//! `DEGENBOT_FLEET_PROFILE`) run on any host with 2 or more cores; a forced
21//! pinned binding below the floor is MARKED `oversubscribed` (latency
22//! contract void, correctness contract intact) — loud, never silent.
23//!
24//! # Composition, never duplication
25//!
26//! The plan picks the BINDING and the per-binding budget projection;
27//! `SlotLayout::of` stays the ONE geometry derivation under each
28//! binding (a serial-plan projection must yield a legal non-empty layout:
29//! 1 solver seat, 1 resolve, >= 1 poolupd, sim per budget). The plan is
30//! boot-frozen like the landed frozen-layout property: no resize
31//! re-derivation is ever triggered. The plan INHERITS budget.rs's
32//! documented derive-vs-doc-table discrepancy (the module note): the rule
33//! column is the authority; a fix, if ever wanted, is its own card.
34//!
35//! # Plan identity
36//!
37//! `PLAN_ID` names and versions the algebra (`fleetplan/1`): the ONE boot
38//! log line names it with the binding and the detected budget, and a
39//! tier edit bumps the version so logs stay unambiguous.
40
41use degenbot_config::FleetProfile;
42
43use crate::budget::{BudgetError, BudgetMode, BudgetOverrides, FleetBudget};
44use crate::dispatcher::BootError;
45
46/// The plan algebra's name + version (named and versioned by contract: the
47/// boot log line names it; a tier edit bumps the version).
48pub const PLAN_ID: &str = "fleetplan/1";
49
50/// The minimum usable host (the epic's goal state): one core for I/O
51/// work, one core for solve work. Below this the plan refuses TYPED.
52/// Canonically owned by budget.rs — re-exported for the plan's
53/// tier gate.
54pub use crate::budget::HOST_FLOOR_CORES;
55
56/// The fleet host binding (the adapter that maps lanes to threads; the
57/// FLEETFLOOR design contract). The census/log label is
58/// [`Binding::label`].
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Binding {
61 /// The pinned binding: dedicated seats per role, today's topology
62 /// (6 cores or more under default overrides).
63 Pinned,
64 /// The serial binding: one ambient I/O lane plus one cycle lane, one
65 /// solve seat (the arm lands with FF-T4).
66 Serial,
67}
68
69impl Binding {
70 /// The census `binding` label (the field's closed vocabulary:
71 /// pinned / shared / logical). The fleet roles stamp the PINNED
72 /// binding today; the serial binding maps them onto shared threads as
73 /// LOGICAL lanes when it lands (FF-T4) — the label changes with the
74 /// binding, the row does not.
75 #[must_use]
76 pub const fn label(self) -> &'static str {
77 match self {
78 Self::Pinned => "pinned",
79 Self::Serial => "logical",
80 }
81 }
82
83 /// The binding NAME (error-message vocabulary: pinned / serial — the
84 /// plan profile the operator set, distinct from the census label).
85 #[must_use]
86 pub const fn name(self) -> &'static str {
87 match self {
88 Self::Pinned => "pinned",
89 Self::Serial => "serial",
90 }
91 }
92}
93
94impl std::fmt::Display for Binding {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 // The NAME (pinned / serial) — the log/error vocabulary; the
97 // census label is the thread-mapping vocabulary ([`Binding::label`]).
98 f.write_str(self.name())
99 }
100}
101
102/// The resolved boot plan: one pure function of the budget (the tier
103/// decision + the per-binding projection inputs). Boot-frozen by design;
104/// [`Clone`] so the host can carry it without borrowing.
105#[derive(Debug, Clone, PartialEq)]
106pub struct FleetPlan {
107 /// The plan identity ([`PLAN_ID`]) — the boot log line names it.
108 pub id: &'static str,
109 /// The binding the host tier resolves to.
110 pub binding: Binding,
111 /// A forced pinned binding below the pinned-role floor: the latency
112 /// contract is void (the declared shares exceed the quota), the
113 /// correctness contract is intact. Loud, never silent.
114 pub oversubscribed: bool,
115 /// The detected budget the plan decided on (cores, fractional).
116 pub budget_cpus: f64,
117 /// The typed budget refusal the resolved plan overrode or fell from
118 /// (`None` when nothing was refused): under `auto`, the pinned-budget
119 /// refusal that placed this host in the serial tier (the boot re-raises
120 /// it while the serial arm is pending — FF-T4 — so the refusal stays
121 /// the typed, named budget error the pinned derivation produced); under
122 /// a FORCED pinned profile below the pinned-role floor, the overridden
123 /// `QuotaTooSmallForPinnedRoles` the marked plan runs past (the runtime
124 /// status names the floor it fell from — FF-T5 addendum, 452GZC);
125 /// `None` for forced serial and every unrefused boot.
126 pub tier_refusal: Option<BudgetError>,
127}
128
129impl FleetPlan {
130 /// The per-binding budget projection: the [`FleetBudget`] the binding
131 /// boots under. Every binding selects a [`BudgetMode`] and the ONE
132 /// owner ([`FleetBudget::project`]) produces the table — this
133 /// module holds no budget arithmetic. The PINNED eligible path is the
134 /// sum-checked derivation (byte-stable); the marked and serial modes
135 /// are total (the sum invariant is void there by contract —
136 /// oversubscribed / shared threads).
137 ///
138 /// # Errors
139 /// [`BootError`] — the pinned eligible projection's typed refusal. The
140 /// marked/serial projections are total; their layout legality is
141 /// `SlotLayout::of`'s dead-station check, asserted by the
142 /// plan-tier invariant tests.
143 pub fn projected_budget(&self, overrides: &BudgetOverrides) -> Result<FleetBudget, BootError> {
144 let mode = match self.binding {
145 Binding::Pinned if !self.oversubscribed => BudgetMode::Pinned,
146 Binding::Pinned => BudgetMode::PinnedMarked,
147 Binding::Serial => BudgetMode::Serial,
148 };
149 FleetBudget::project(self.budget_cpus, overrides, mode).map_err(BootError::from)
150 }
151
152 /// The typed refusal the BOOT raises while the serial arm is pending
153 /// (FF-T4): under `auto` the tier's own pinned-budget refusal (the
154 /// CI-stable message); under a FORCED serial profile the named
155 /// pending-arm invariant (the operator asked for a binding that does
156 /// not exist yet — refusing is the honest answer, never a silent
157 /// narrow).
158 #[must_use]
159 pub fn pending_serial_refusal(&self) -> BootError {
160 match self.tier_refusal {
161 Some(refusal) => BootError::Budget(refusal),
162 None => BootError::Invariant(
163 "the serial binding (the 2-5-core tier) lands with FF-T4 \
164 — forced serial refuses rather than run the pinned topology \
165 silently narrower",
166 ),
167 }
168 }
169}
170
171/// ONE pure function of the budget (the tiered boot authority). Total,
172/// deterministic, no host reads: the caller supplies the detected
173/// quota, the profile, and the terminal overrides. The budget stays the
174/// sole sizing authority (min(cgroup quota, affinity), fractional,
175/// rounded down for tier eligibility).
176///
177/// # Errors
178/// [`BootError::Budget`] — below the 2-core host floor, an override the
179/// resolved binding cannot honor (out-of-bounds, never a silent clamp),
180/// or (forced/oversubscribed configs) the derivation's own refusal.
181pub fn plan(
182 quota_cpus: f64,
183 profile: FleetProfile,
184 overrides: &BudgetOverrides,
185) -> Result<FleetPlan, BootError> {
186 // Tier eligibility floors the fractional quota (budget.rs floors at
187 // 1.0 on detection; the tier check is against HOST_FLOOR_CORES).
188 #[expect(
189 clippy::cast_possible_truncation,
190 reason = "quota floors are small positive values (core counts)"
191 )]
192 #[expect(
193 clippy::cast_sign_loss,
194 reason = "the quota is floored at 1.0 before the cast"
195 )]
196 let quota_floor = quota_cpus.max(1.0).floor() as u64;
197 if quota_floor < HOST_FLOOR_CORES {
198 return Err(BootError::Budget(BudgetError::BelowHostFloor {
199 quota: quota_cpus,
200 }));
201 }
202 match profile {
203 FleetProfile::Auto => match FleetBudget::derive(quota_cpus, overrides) {
204 Ok(_) => {
205 validate_io_workers(Binding::Pinned, overrides)?;
206 Ok(FleetPlan {
207 id: PLAN_ID,
208 binding: Binding::Pinned,
209 oversubscribed: false,
210 budget_cpus: quota_cpus,
211 tier_refusal: None,
212 })
213 }
214 // The 2-5-core tier: the plan says Serial, carrying the pinned
215 // refusal that placed the host there. The arm lands with
216 // FF-T4; until then the boot re-raises the tier refusal.
217 Err(refusal @ BudgetError::QuotaTooSmallForPinnedRoles { .. }) => {
218 validate_io_workers(Binding::Serial, overrides)?;
219 Ok(FleetPlan {
220 id: PLAN_ID,
221 binding: Binding::Serial,
222 oversubscribed: false,
223 budget_cpus: quota_cpus,
224 tier_refusal: Some(refusal),
225 })
226 }
227 // An oversubscribed / under-minimum pinned config is a
228 // configuration bug on ANY tier: refuse with its own typed
229 // error, never fall through to a narrower plan.
230 Err(other) => Err(BootError::Budget(other)),
231 },
232 FleetProfile::Pinned => {
233 validate_io_workers(Binding::Pinned, overrides)?;
234 // Forced pinned runs on any host with 2 or more cores; below
235 // the pinned-role floor the latency contract is void (marked).
236 // The typed refusal the operator overrode RIDES the plan — like
237 // the auto serial tier carries its placing refusal — so the
238 // runtime status names the pinned floor it fell from (FF-T5
239 // addendum, 452GZC).
240 let (oversubscribed, tier_refusal) = match FleetBudget::derive(quota_cpus, overrides) {
241 Ok(_) => (false, None),
242 Err(refusal) => (true, Some(refusal)),
243 };
244 Ok(FleetPlan {
245 id: PLAN_ID,
246 binding: Binding::Pinned,
247 oversubscribed,
248 budget_cpus: quota_cpus,
249 tier_refusal,
250 })
251 }
252 FleetProfile::Serial => {
253 validate_io_workers(Binding::Serial, overrides)?;
254 Ok(FleetPlan {
255 id: PLAN_ID,
256 binding: Binding::Serial,
257 oversubscribed: false,
258 budget_cpus: quota_cpus,
259 tier_refusal: None,
260 })
261 }
262 }
263}
264
265/// The per-binding `runtime.io_workers` bounds (FF-T2: an out-of-bounds
266/// override raises a typed refusal with a hint, never a silent clamp).
267/// The pinned binding keeps the ambient floor (A >= 1); the serial
268/// binding owns exactly ONE ambient I/O lane (A == 1).
269fn validate_io_workers(binding: Binding, overrides: &BudgetOverrides) -> Result<(), BootError> {
270 let Some(requested) = overrides.ambient_io_workers else {
271 return Ok(());
272 };
273 let legal = match binding {
274 Binding::Pinned => requested >= 1,
275 Binding::Serial => requested == 1,
276 };
277 if legal {
278 return Ok(());
279 }
280 Err(BootError::Budget(BudgetError::IoWorkersOutOfBounds {
281 requested,
282 binding: binding.name(),
283 }))
284}
285
286#[cfg(test)]
287#[expect(clippy::expect_used)]
288mod tests {
289 use super::*;
290 use crate::dispatcher::SlotLayout;
291 use degenbot_config::FleetProfile;
292
293 fn overrides() -> BudgetOverrides {
294 BudgetOverrides::default()
295 }
296
297 /// THE FF-T2 algebra table (the AC, verbatim): one pure function of
298 /// the budget over the ordered host tiers.
299 #[test]
300 fn the_tier_table_is_the_contract() {
301 // plan(1.5) -> Err: below the 2-core host floor (one core for
302 // I/O work, one core for solve work), typed.
303 let err = plan(1.5, FleetProfile::Auto, &overrides()).expect_err("sub-2-core host");
304 assert!(
305 matches!(err, BootError::Budget(BudgetError::BelowHostFloor { quota }) if (quota - 1.5).abs() < f64::EPSILON),
306 "below-2-cores refuses with the typed host-floor error, got {err:?}"
307 );
308 // plan(2.0) -> Serial: the pinned derivation refuses (floor 2 <
309 // the 6-core pinned-role floor), the host keeps the 2-core minimum.
310 let p = plan(2.0, FleetProfile::Auto, &overrides()).expect("2-core host plans");
311 assert_eq!(p.binding, Binding::Serial);
312 assert!(!p.oversubscribed);
313 assert_eq!(p.id, PLAN_ID);
314 // plan(5.99) -> Serial: tier eligibility FLOORS the fraction.
315 let p = plan(5.99, FleetProfile::Auto, &overrides()).expect("5.99-core host plans");
316 assert_eq!(p.binding, Binding::Serial);
317 // plan(6.0) -> Pinned: the pinned derivation succeeds exactly
318 // there (H1+A1+R1+M1+S2 = 6).
319 let p = plan(6.0, FleetProfile::Auto, &overrides()).expect("6-core host plans");
320 assert_eq!(p.binding, Binding::Pinned);
321 assert!(!p.oversubscribed);
322 // Forced pinned on a 2-core quota -> Pinned + oversubscribed
323 // (latency contract void, correctness intact, LOUD).
324 let p = plan(2.0, FleetProfile::Pinned, &overrides()).expect("forced pinned plans");
325 assert_eq!(p.binding, Binding::Pinned);
326 assert!(
327 p.oversubscribed,
328 "a forced pinned binding below the floor is marked"
329 );
330 // Forced serial on a 24-core quota -> Serial (forced bindings
331 // run on any host with 2 or more cores).
332 let p = plan(24.0, FleetProfile::Serial, &overrides()).expect("forced serial plans");
333 assert_eq!(p.binding, Binding::Serial);
334 assert!(!p.oversubscribed);
335 assert!(p.tier_refusal.is_none());
336 }
337
338 /// The auto serial tier CARRIES the pinned refusal that placed the
339 /// host there: the boot re-raises it while the arm is pending
340 /// (FF-T4), keeping the CI-stable typed message.
341 #[test]
342 fn the_auto_serial_tier_carries_the_pinned_refusal() {
343 let p = plan(4.0, FleetProfile::Auto, &overrides()).expect("4-core host plans");
344 assert_eq!(p.binding, Binding::Serial);
345 let refusal = p.tier_refusal.as_ref().expect("the tier refusal rides");
346 assert!(
347 matches!(
348 refusal,
349 BudgetError::QuotaTooSmallForPinnedRoles { quota, required }
350 if (*quota - 4.0).abs() < f64::EPSILON && *required == 6
351 ),
352 "the carried refusal is the pinned derivation's own, got {refusal:?}"
353 );
354 // The boot-facing refusal is the SAME budget error (the
355 // pending-arm gate), and a forced serial profile gets the
356 // named pending invariant instead.
357 assert!(matches!(
358 p.pending_serial_refusal(),
359 BootError::Budget(BudgetError::QuotaTooSmallForPinnedRoles { .. })
360 ));
361 let forced = plan(24.0, FleetProfile::Serial, &overrides()).expect("forced serial plans");
362 assert!(matches!(
363 forced.pending_serial_refusal(),
364 BootError::Invariant(_)
365 ));
366 }
367
368 /// FF-T5 addendum (452GZC): a forced pinned binding below the
369 /// pinned-role floor is MARKED and KEEPS the typed refusal it overrode —
370 /// the runtime status names the pinned floor it fell from, exactly like
371 /// the auto serial tier carries its placing refusal. At/above the floor
372 /// nothing was refused.
373 #[test]
374 fn the_forced_pinned_oversubscription_carries_the_pinned_floor_refusal() {
375 let p = plan(4.0, FleetProfile::Pinned, &overrides()).expect("forced pinned plans");
376 assert_eq!(p.binding, Binding::Pinned);
377 assert!(p.oversubscribed);
378 let refusal = p
379 .tier_refusal
380 .as_ref()
381 .expect("the overridden refusal rides");
382 assert!(
383 matches!(
384 refusal,
385 BudgetError::QuotaTooSmallForPinnedRoles { quota, required }
386 if (*quota - 4.0).abs() < f64::EPSILON && *required == 6
387 ),
388 "the carried refusal is the pinned derivation's own, got {refusal:?}"
389 );
390 for quota in [6.0_f64, 8.0, 24.0] {
391 let p = plan(quota, FleetProfile::Pinned, &overrides()).expect("eligible host plans");
392 assert!(!p.oversubscribed);
393 assert!(p.tier_refusal.is_none());
394 }
395 }
396
397 /// `runtime.io_workers` is validated against the plan bounds: an
398 /// out-of-bounds override raises a typed refusal with a hint, never
399 /// a silent clamp. The pinned binding keeps the ambient floor
400 /// (A >= 1); the serial binding owns exactly ONE ambient I/O lane.
401 #[test]
402 fn io_workers_overrides_are_validated_against_plan_bounds() {
403 // Pinned + A=0: below the ambient floor.
404 let ov = BudgetOverrides {
405 ambient_io_workers: Some(0),
406 ..overrides()
407 };
408 let err = plan(8.0, FleetProfile::Pinned, &ov).expect_err("A=0 refuses");
409 assert!(
410 matches!(
411 err,
412 BootError::Budget(BudgetError::IoWorkersOutOfBounds {
413 requested: 0,
414 binding: "pinned"
415 })
416 ),
417 "the out-of-bounds override names the request and the binding, got {err:?}"
418 );
419 // Serial + A=2: off the exactly-one ambient I/O lane (the auto
420 // serial tier validates the same bounds).
421 let ov = BudgetOverrides {
422 ambient_io_workers: Some(2),
423 ..overrides()
424 };
425 let err = plan(4.0, FleetProfile::Auto, &ov).expect_err("serial A=2 refuses");
426 assert!(
427 matches!(
428 err,
429 BootError::Budget(BudgetError::IoWorkersOutOfBounds {
430 requested: 2,
431 binding: "serial"
432 })
433 ),
434 "the serial tier validates its one-lane bound, got {err:?}"
435 );
436 // A legal override rides untouched (never a clamp).
437 let ov = BudgetOverrides {
438 ambient_io_workers: Some(2),
439 ..overrides()
440 };
441 let p = plan(8.0, FleetProfile::Auto, &ov).expect("8-core A=2 plans");
442 assert_eq!(p.binding, Binding::Pinned);
443 assert_eq!(
444 p.projected_budget(&ov)
445 .expect("projection derives")
446 .ambient_cpus,
447 2
448 );
449 }
450
451 /// The plan-tier invariants (the `SlotLayout` dead-station family
452 /// extended into tiers): a serial-plan projection must yield a LEGAL,
453 /// non-empty layout — 1 solver seat, 1 resolve, >= 1 poolupd, sim per
454 /// budget; the forced-pinned marked projection must stay legal too.
455 #[test]
456 fn plan_projections_yield_legal_non_empty_layouts() {
457 for quota in [2.0_f64, 2.5, 4.0, 5.99, 24.0] {
458 let p = plan(quota, FleetProfile::Serial, &overrides()).expect("serial plans");
459 let b = p
460 .projected_budget(&overrides())
461 .expect("serial projection is total");
462 assert_eq!(b.solver_pin_count, 1, "serial-0: exactly one solve seat");
463 assert_eq!(b.resolve_cpus, 1);
464 assert!(b.pool_state_updater_slots >= 1);
465 assert!(b.sim_slot_cap >= 1, "sim per budget");
466 // The layout is legal (every hosted range non-empty, the merge
467 // sidecar last); the seat counts were asserted on the budget
468 // fields above — the ONE geometry derivation accepted them.
469 SlotLayout::of(&b).expect("the serial projection hosts a legal layout");
470 }
471 // The forced-pinned marked projection: legal stations, the pin
472 // count floors at 1, the mark carries the deficit story.
473 for quota in [2.0_f64, 4.0, 5.99] {
474 let p = plan(quota, FleetProfile::Pinned, &overrides()).expect("forced pinned plans");
475 assert!(p.oversubscribed);
476 let b = p
477 .projected_budget(&overrides())
478 .expect("the marked projection is total");
479 assert!(
480 b.solver_pin_count >= 1,
481 "station legality: the pin count floors at 1"
482 );
483 assert!(
484 (b.fractional_remainder - 0.0).abs() < 1e-9,
485 "oversubscribed: no spendable remainder"
486 );
487 SlotLayout::of(&b).expect("the marked projection hosts a legal layout");
488 }
489 }
490
491 /// The pinned eligible path is the ONE derivation, byte-stable: the
492 /// projection IS `FleetBudget::derive` (no second arithmetic).
493 #[test]
494 fn the_pinned_eligible_projection_is_the_one_derivation() {
495 for quota in [6.0_f64, 8.0, 24.0, 6.5] {
496 let p = plan(quota, FleetProfile::Auto, &overrides()).expect("eligible host plans");
497 assert_eq!(p.binding, Binding::Pinned);
498 assert_eq!(
499 p.projected_budget(&overrides())
500 .expect("projection derives"),
501 FleetBudget::derive(quota, &overrides()).expect("the derivation")
502 );
503 }
504 }
505}