Merge pull request 'ADR 0211: a machine's power is a node seat, its moments take contributions, and its states are events' (#364) from decision/0211-power-is-a-node-seat into main

This commit is contained in:
2026-10-04 13:58:25 +00:00
3 changed files with 133 additions and 0 deletions
@@ -0,0 +1,122 @@
---
topic: what runs on it
status: accepted
date: 2026-10-04
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
---
# 211. A machine's power is a node seat, its moments take contributions, and its states are events
## Context
The laptop's module needs code to run around sleep:
- the GPU driver's own suspend and resume actions;
- a touchpad reset after waking.
It wrote drop-ins of its own into the service manager's sleep services, so it wrote into files that
belong to another tool's holder. [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
forbids exactly that. A second module wanting code after waking would do the same, and nothing would
order the two or say that either needs sleep to be handled at all.
The mesh also cannot tell a sleeping machine from a lost one. A laptop with its lid closed stops its
heartbeat exactly as a crashed machine does, and is reported "out of touch" either way.
[Research 028](../01-RESEARCH/028-the-meshs-output-channel/00-overview.md) would turn every closed lid
into an alert.
## Considered Options
1. **Each module writes its own sleep drop-ins,** as the laptop's did. Rejected by ADR 0210: no
owner, no order, no dependency.
2. **The service manager's holder takes power hooks.** Rejected: sleep and power are logind's and the
firmware's concern, not service management's. On a laptop they also include lid, power source and
battery, which the service manager knows nothing about.
3. **A power seat on every machine.** Its holder:
- owns the machine's power handling;
- places code that modules contribute for named moments;
- publishes the machine's power states as events.
Chosen. It was the operator's proposal.
## Decision
**1. `node-power` is a node seat in the mesh's own set.** One module per node holds it. Every
machine has one, servers included: every machine boots and shuts down. The first holder is a module
named `power`.
**2. Its holder owns the machine's power handling:**
- logind's power-key and lid settings;
- the hooks around sleep, boot and shutdown;
- the reading of power source and battery where the machine has them.
A model's specific values, such as what the lid does on that laptop, are the model's module's
contribution or a setting of `power` per node (ADR 0174), never a second writer of logind's
configuration.
**3. Modules contribute code for named moments.** The moments:
- after boot;
- before sleep;
- after waking;
- before shutdown;
- on mains power;
- on battery.
A contribution is POSIX shell code, written with ADR 0204's mechanism as ADR 0208 §4 did for the
session's start:
- a `shell` contribution whose `for` names the moment, in the `first`, `normal` or `last` slot;
- placed by the holder with `${shell:<moment>:<slot>}` in the scripts its own units run;
- run as root, in module order, each piece bounded in time, so that one module's hang cannot hold a
machine awake.
Per ADR 0210, a contribution for a moment depends on `node-power`.
**4. Its states are the holder's events, on the bus:**
- `booted`, `sleeping`, `woke`, `shutting-down`;
- `on-mains`, `on-battery`, `battery-low`, where the machine has a battery.
They carry the machine's role and a time, and nothing secret, so any node and the controller may
consume them.
- **`sleeping` is published before the machine sleeps.** The holder takes logind's delay lock,
publishes, and releases the lock once the bus has acknowledged, within logind's delay bound.
- **On waking,** the holder queues events until the bus is reachable, then publishes them in order.
**5. A machine that said `sleeping` is asleep, not out of touch,** until it says `woke` or misses its
expected return. The controller shows the state, and the output channel (research 028) does not
treat a sleeping machine as a fault.
## Consequences
- The laptop's module moves its sleep drop-ins into contributions:
- the GPU driver's suspend and resume actions before sleep and after waking;
- its touchpad reset after waking.
Its own files in the service manager's directories go.
- Assigning `power` to every machine is phase 1 of [to-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md).
- The controller gains the moments as contribution targets placed by `node-power`, and a node's
power state in what it shows about the node.
- **What got harder:**
- code that must run at a precise point inside the sleep transaction cannot be a contribution; the
GPU driver's own units are an example. Such code still declares its own units, and only the
request to run them is contributed;
- an event published around sleep depends on the network still being up. The delay lock buys the
time, and if the bus does not answer within it, the machine sleeps anyway and says so on waking.
## How it is checked
| Rule | Checked by |
|---|---|
| A moment's contribution derives a dependency on `node-power`, and lands in that moment's placeholder in module order | the controller's contribution tests |
| A contribution naming an unknown moment is refused | the catalogue check |
| One piece of hook code that hangs is ended after its bound, and the next still runs | the power module's tests over real child processes |
| `sleeping` is published before sleep and `woke` after, and a missed acknowledgement does not hold the machine awake | the power module's tests with a fake logind and bus, and a live suspend of the laptop |
| A machine that said `sleeping` is not reported out of touch | the controller's status test |
## References
- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md),
[ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md),
[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md),
[ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md),
[ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md)
- [Research 028](../01-RESEARCH/028-the-meshs-output-channel/00-overview.md)
+1
View File
@@ -309,6 +309,7 @@ python3 00-META/checks/index.py fail if stale
- **0206** — [A node reports the Anthropic grant it holds; the licence manager adopts a licence by refreshing it, and what each node should hold is the manager's state](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md)
- **0208** — [The graphical session is one module per piece, on the mesh's seats](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)
- **0209** — [A login on a node moves that node to the account it logged in to; an API key is added from any node, sealed](0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md)
- **0211** — [A machine's power is a node seat, its moments take contributions, and its states are events](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)
### How it is built
@@ -15,6 +15,7 @@ decisions:
- 02-DECISIONS/0205-software-the-distribution-does-not-package-ships-as-a-pinned-archive-of-the-module.md
- 02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md
- 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
- 02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md
- 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
---
@@ -77,6 +78,15 @@ than reported.
`kernel` is last because a mistake in it costs a boot. `docker` stays a module without the runtime seat
until ADRs 0165 and 0166 are accepted.
**Added 2026-10-04.** Two more modules for every machine:
- `power` holds `node-power` ([ADR 0211](../../02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md)). Other modules contribute code for
its moments (after boot, before sleep, after waking, before shutdown, on mains, on battery), and it
publishes the machine's power states on the bus.
- `dbus` holds the message bus. Modules shipping D-Bus policies or services contribute them to it.
It shares curated events, never raw traffic. An upgrade never restarts the bus live: its package
waits for a reboot. A live restart in the middle of a full upgrade took down a workstation's
logins on the day this was written.
## Phase 2 — both workstations
In order: