Skip to content

fwmanager: table-declared boot checkpoints replace BootMonitor - #23

Open
chrysh wants to merge 8 commits into
mainfrom
add-boot-walk
Open

fwmanager: table-declared boot checkpoints replace BootMonitor#23
chrysh wants to merge 8 commits into
mainfrom
add-boot-walk

Conversation

@chrysh

@chrysh chrysh commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Traits and schema only — the walker implementing them follows in a separate PR, so this one stays reviewable on the contract alone.

What

  • BootMonitor is removed. A BootCheckpoint names its evidence as a board-defined signal id (signal: G, timeout, retry budget) — the schema names no signal kinds, and everything that is config is declared in the board device table (target/mock/devices.rs); config.rs only defines what that configuration has to look like.
  • EvidenceReader<G> resolves a signal id to BootStatus — board wiring, typically one per managed device, exhaustive match, direct dispatch.
  • BootStatus grows device-reported verdicts: FailedRetriable (consumes retry budget immediately instead of waiting out the window) and FailedFatal (ends the boot regardless of budget). Timeouts stay the observer's own judgment — hung devices report nothing; the reader tests pin this down with an MCTP-shaped mock where silence reads Booting forever.
  • BootWatch/WalkVerdict is the erased seam the orchestrator polls: Waiting/Complete/Retry/Dead per device, free of device and error types.
  • GpioBootMonitor keeps its behavior as a plain reader (inherent method, no trait).

Reading order

The first commit is the checkpoint-embedded-fn variant; the second defunctionalizes it into signal ids and records why (table stays pure data, borrows stay per-device, dispatch stays direct). The pair is kept deliberately as the design record — review the combined diff if you only want the endpoint. Commits three and four add the failure verdicts and the message-path reader tests.

Supersedes

Test

bazel test //services/fwmanager/... (13 api tests), bazel build //target/mock/...; device-table validate runs in const context, so a bad table is a build error.

Migration (for integrators)

BootMonitor is replaced by EvidenceReader<G> + BootStatus; the checkpoint schema changed shape:

// before: one monitor per device, checkpoint carries a schema-level signal kind
BootCheckpoint { name: "boot-complete", signal: BootSignal::GpioBootComplete(12),
                 window: Duration::from_secs(90) }
impl BootMonitor for MyMonitor { fn boot_status(&self) -> Result<BootStatus, E> }

// after: signal id is the board's own enum; window -> timeout; new max_retries
BootCheckpoint { name: "boot-complete", signal: MySignal::Gpio(12),
                 timeout: Duration::from_secs(90), max_retries: 1 }
impl EvidenceReader<MySignal> for MyBmcReader {
    fn read(&mut self, signal: &MySignal) -> Result<BootStatus, E>
}
  • max_retries: 0 = the single attempt is all the device gets.
  • BootStatus::Failed split into FailedRetriable / FailedFatal (device-reported verdicts; timeouts remain the observer's judgment).
  • Checkpoint names must be unique per device (build-time validate).
  • WalkVerdict::Retry carries deadline_millis of the re-armed window — schedule against it exactly like Waiting.

chrysh added 7 commits August 5, 2026 22:16
A BootCheckpoint is timing policy plus its own evidence check: a
capture-less fn handed the board's device context, so the channel
underneath never leaks past the check and an unobservable checkpoint
is unrepresentable. config.rs defines the schema (BootSignal is gone);
the board table declares the checkpoints against its own context and
error types. BootStatus stays as the shared vocabulary and absorbs
the latch-cleared-by-reset contract; GpioBootMonitor keeps its
behavior as a plain reader. BootWatch/WalkVerdict is the erased seam
the orchestrator polls — timeout and retry-budget judgment lands with
the walker that implements it.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
The embedded fn was the more general shape, but the generality went
unused while its costs did not: the table stopped being pure data
(unprintable, unvalidatable on mechanisms, never generatable), every
check shared one &mut board context, and dispatch went indirect. A
signal id is the same check defunctionalized: data in the table, an
exhaustive match in the board's EvidenceReader — typically one per
device, so each walk borrows only its own reader. Boot-evidence
mechanisms per board are a closed set; when one can't be named, that
is a new variant in that board's enum, not an API change.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
A device that knows it failed should end the wait early, and one that
knows a retry is pointless should say so, instead of the orchestrator
burning its window and budget to find out. BootStatus::Failed splits
into FailedRetriable (consumes budget immediately) and FailedFatal
(ends the boot regardless of budget). Timeouts stay the orchestrator's
own judgment — hung devices report nothing — and channel trouble stays
in the reader's Error, distinct from a device-reported verdict.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
A timeout is never on the wire: a hung endpoint reads Booting forever,
and only the orchestrator's clock turns silence into a verdict. The
message path carries the active verdicts (device failure codes) and
channel trouble, each on its own channel.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
Retry re-arms the window, but the caller had no way to know until
when — it would have had to reach into the checkpoint's timeout and
do the walker's arithmetic itself. Retry now carries deadline_millis
exactly like Waiting: one scheduling rule for both verdicts.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
Adapter crates cannot implement EvidenceReader themselves — a board's
signal vocabulary G is not theirs to know. Show the intended shape on
the trait: the board impl owns the match, the hardware binding is made
once at construction, the signal id proves the right reader was wired.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
Failure reports identify a checkpoint by name, so a duplicate within a
device would make them ambiguous — validate now rejects it at build
time (str comparison by hand: == on &str is not const). Also state
explicitly that max_retries=0 means the one attempt is all the device
gets.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
@chrysh

chrysh commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review, one commit per point:

  • EvidenceReader for GpioBootMonitor: took the documentation route (1039660) — adapter crates can't implement EvidenceReader<G> because a board's signal vocabulary G isn't theirs to know; that boundary is the design working as intended. The trait docs now show the wiring shape (board impl owns the match, hardware binding made once at construction).
  • Retry scheduling: WalkVerdict::Retry now carries deadline_millis of the re-armed window (716384e) — one scheduling rule for Waiting and Retry, no deadline arithmetic in the orchestrator.
  • Migration note: added to the PR description (no CHANGELOG file in the repo yet); includes the before/after snippet, windowtimeout + max_retries.
  • Name uniqueness: validate now rejects duplicate checkpoint names per device at build time, with a reject-path test (da23a4b).
  • max_retries = 0: pinned in the field docs — the one attempt is all the device gets (da23a4b).

The signal field now says on the spot why it is an id and who resolves
it, and validate points at the mock table, which demonstrates the
board-local const fence for checks the generic validate cannot do
(gpio line within the bank).

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
@chrysh

chrysh commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Round two addressed in 2134206:

  • signal field doc: now says on the spot that it's a board-defined id resolved by the board's EvidenceReader, an id rather than a function so the table stays pure data, with a pointer to the type-level docs for the full trade-off.
  • Board-local validation example: target/mock/devices.rs now demonstrates the pattern — a board-local const fn validate_signals (gpio line within the bank) running in the same const fence as the generic validate, and validate's docs point at it.
  • Empty checkpoint list: already rejected — validate asserts !checkpoints.is_empty() ("device must declare at least one boot checkpoint") and rejects_an_empty_checkpoint_list covers the reject path. When passive devices arrive (slot-layout work, upstream fwmanager: Add per-device slot layout to the device-table schema OpenPRoT/openprot#394's CommitPolicy::None), that rule relaxes to "empty list only with CommitPolicy::None" — deliberate future change, not a gap today.

@chrysh
chrysh marked this pull request as ready for review August 5, 2026 20:54
@chrysh
chrysh requested a review from leongross August 5, 2026 20:54
@chrysh

chrysh commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@rusty1968 @FerralCoder @embediver — review requested (GitHub only let me formally request @leongross here since this is the 9elements staging fork). This replaces the BootMonitor/BootWalk approach from OpenPRoT#395; once it settles here it goes upstream as one PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant