diff --git a/docs/src/design/orchestrator/orchestrator-overview.md b/docs/src/design/orchestrator/orchestrator-overview.md index 54f14166f..a005e2b05 100644 --- a/docs/src/design/orchestrator/orchestrator-overview.md +++ b/docs/src/design/orchestrator/orchestrator-overview.md @@ -1,4 +1,4 @@ -# Orchestrator State Machine +# Orchestrator The orchestrator is the eRoT's boot-sequence controller. It walks the platform trust chain — verifying each component's firmware and releasing it from reset in @@ -28,6 +28,10 @@ avoid the two drifting apart. - [**Platform Architecture**](./orchestrator-platform.md): The platform half around the core — surrounding services, capability contracts, the board device table, and the fail-safe rules at the responsibility boundary. +- [**Runtime**](./orchestrator-runtime.md): How the runtime loop + gathers hardware interrupts, IPC channel messages, and watchdog deadlines into + the core's event stream, and carries the resulting effects and decisions back + out. ## Design Principles @@ -48,6 +52,22 @@ hiding them as implicit state changes. See the The platform supplies the trust chain (component ids, kinds, and required/optional policy) and the recovery-retry cap at startup. +## Board composition + +*Board composition* (or *system composition*) is the per-target choice — described +declaratively in a [`system.json5`](../../architecture.md) file, assembled by +Pigweed at build time — of how the platform's functions are split across +processes and which resources each process owns: hardware register blocks and the +kernel [interrupt objects and IPC channels](../pw-kernel-ipc.md) built on top of +them. It is separate from the +board-supplied *policy* above: policy is *what* to verify (the trust chain); +composition is *how* the surrounding services are wired. The orchestrator core +and its platform-agnostic crates are the same across every composition — a driver +may own a GPIO bank and forward boot-progress over a channel in one image, while +the orchestrator holds the pins directly in another. That choice changes which +inbound sources the [runtime](./orchestrator-runtime.md) sees and who owns each +device, but never the core's states or the loop that serves them. + ## Relationship to CSA Architecture The state machine is a direct implementation of the boot sequence described in diff --git a/docs/src/design/orchestrator/orchestrator-runtime.md b/docs/src/design/orchestrator/orchestrator-runtime.md new file mode 100644 index 000000000..fbff52c24 --- /dev/null +++ b/docs/src/design/orchestrator/orchestrator-runtime.md @@ -0,0 +1,111 @@ +# Runtime + +The **runtime** is the orchestrator's event loop: the single task that sits +between two collaborators — the pure [state machine](./orchestrator-machine.md) +(the *core*) that decides, and the `PlatformDriver` that executes — turning +outside happenings (interrupts, timeouts, IPC messages) into the `Event`s the +core consumes, and handing the `Effect`s it emits to the driver. It holds no +policy of its own — every decision stays in the core; the runtime only moves +information across the process boundary. + +It gathers three possible inbound sources — **hardware interrupts** +(boot-progress lines the orchestrator owns directly), **IPC channels**, and +**watchdog deadlines** (timeouts) — into the one event stream the core +supervises, and carries the core's decision back out. Which sources are present +is a [board-composition](./orchestrator-overview.md#board-composition) choice: +boot-progress is a hardware interrupt only when the orchestrator owns the pins; +otherwise a monitor forwards it as an IPC message. + +The unifying idea is the kernel **wait group**: interrupts and IPC are not +separate mechanisms but interchangeable *members* of one group, and a watchdog +deadline is that same wait's *timeout* — so all three collapse into the return +of one `object_wait(handle, signal_mask, deadline)`. A *signal* here is just a +named bit on a waitable object, not a source in its own right: a latched IRQ bit +on an interrupt object, or `READABLE` / `USER` on a channel (a service raises +`Signals::USER` on a client channel to notify without a reply). The loop is +written once against "a member that signaled, or the deadline that lapsed," +never against a specific source; what differs between sources is only the +*decoder* that turns each into an `Event`. + +Two neighboring pages carry the supporting detail: +[Platform Architecture](./orchestrator-platform.md) names the services and the +responsibility boundary, and [pw_kernel IPC](../pw-kernel-ipc.md) gives the +concrete channel syscalls. The worked, compiling reference is the QEMU +integration test at `target/ast10x0/tests/orchestrator/runtime/main.rs`. + +## The single wait point + +The runtime is a single-threaded loop parked in one place: a kernel +`object_wait` over a **wait group**. Every inbound source is registered once as a +*member* of that group (`wait_group_add`), and the wait returns whichever member +signaled. Each member resolves to at most one `Event`: + +- **Boot-progress signals** — a component reaching a checkpoint raises a + boot-progress signal. Depending on the board's composition it arrives as an + interrupt object the orchestrator holds directly or as a message a monitor + forwards over a channel; either way the loop maps it to + [`Event::ComponentReady`] (an `Active` component's iRoT-verified readiness) + or [`Event::Booted`] (a `Passive` component's liveness). +- **Watchdog deadlines** — the timer is not a separate task. `BootWatchdogs` + (`services/orchestrator/server`) folds all armed boot windows and the commit + window into a *single deadline* passed straight to `object_wait`. When the + wait returns `DeadlineExceeded`, `poll_expired()` yields the mapped + [`Event::Timeout`] / [`Event::CommitTimeout`]. +- **IPC channel messages** — an update agent, management path, peer service, or + a boot-progress-forwarding monitor holds a channel *initiator*; the runtime + holds the *handler*. A readable channel is another object the loop waits on; + its message decodes to an `Event` — an [`Event::UpdateRequest`] to answer, or + a forwarded [`Event::Booted`] / [`Event::ComponentReady`] notification. + +The three share one `object_wait`, so a slow image hash on one path cannot +delay a boot window on another — this is the "never block" rule of the +[Platform Architecture](./orchestrator-platform.md#responsibility-scope) made +concrete: the loop only ever blocks at the wait, and only until the *nearest* +of any signal, any channel, or the nearest deadline. + +**Members are uniform; only the decode differs.** A wait-group member is an +object watched for a signal bit — a latched IRQ bit on an *interrupt object*, or +`READABLE` / `USER` on a *channel* — and the loop treats them identically: wait, +see which member signaled, run that member's decode, dispatch the resulting +`Event`. Nothing above the decode step knows which kind a member is. That +uniformity pushes two questions *below* the runtime layer, where they belong: + +- **Who owns the underlying hardware** — does the orchestrator own the GPIO bank + and hold the boot-progress interrupt object itself, or does a monitor/GPIO + server own the pins and forward boot-progress over a channel? — is a + [board-composition](./orchestrator-overview.md#board-composition) choice made + in `system.json5`. Either shape is just one member of the group; the loop is + byte-for-byte the same. +- **What a member means** is the per-member decode. + +```mermaid +flowchart LR + subgraph SRC["Inbound sources (SIG/CH are wait-group members; TMR is the wait's timeout; boot-progress rides CH if a monitor owns the pins)"] + SIG["Boot-progress signal
(directly-owned interrupt object, latched)"] + TMR["Watchdog deadline
(BootWatchdogs → one Instant)"] + CH["IPC channel
(handler endpoint, READABLE)"] + end + + WAIT["object_wait(signals, deadline)
the single park point"] + + subgraph MAP["Inbound adapters (source → Event)"] + SMAP["signal → ComponentReady / Booted"] + TMAP["poll_expired → Timeout / CommitTimeout"] + CMAP["channel_read → decode → UpdateRequest / ..."] + end + + CORE["Orchestrator::dispatch
(pure reducer)"] + + OUT["Effects → PlatformDriver
+ response → channel_respond"] + + SIG --> WAIT --> SMAP --> CORE + TMR --> WAIT --> TMAP --> CORE + CH --> WAIT --> CMAP --> CORE + CORE --> OUT +``` + +[`Event::ComponentReady`]: ./orchestrator-machine.md +[`Event::Booted`]: ./orchestrator-machine.md +[`Event::Timeout`]: ./orchestrator-machine.md +[`Event::CommitTimeout`]: ./orchestrator-machine.md +[`Event::UpdateRequest`]: ./orchestrator-machine.md