docs: R-166 SHIPPED — the desired/in-flight/observed split (D-b)
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
Controller v0.189.0 implements operator decision D-b. Docs only here; no hub change and no hub version bump. - architecture/02-controller-module-map.md §0a — NEW, and it is the S-1 contract: desired (app.yaml) / in-flight (own marker file) / observed (not persisted), with the rule that ties them — never derive one from another. Absent desired state means UNKNOWN, never "running". One file, one writer. D-b's binding safety rule quoted verbatim. - 00-capability-map.md — the boot-recovery row now rests on a recorded signal, with the three live flows from 9201. The interrupted-operation half is marked IMPLEMENTED, not PROVEN-LIVE: nobody killed the controller mid-backup on metal. - OPEN-ITEMS/ROADMAP — R-166 SHIPPED with both blocking facts and their answers; R-157 mechanism B CLOSED and A restated as the whole item; R-170 NEW (the drive-backed boot gate still infers a Stop from a container count). - STATUS.md — the "an app can stay switched off and nothing says so" line rewritten to what is actually left: timing. - CLAUDE.md — end-of-session checklist gains: confirm your own last push's CI run went green, BY RUN ID. The failure email is a push signal; this is the pull check. - CONTEXT.md S-12 — the rulings, and the two lessons worth carrying: a test that constructs the thing it should prove the caller constructs is hollow (its red-proof will say so), and a field-by-field struct rebuild in a save path is a defect on sight.
This commit is contained in:
@@ -53,6 +53,80 @@ Risk tags: **clean** · **needs-rework** · **hazard** (entangles a delete-targe
|
||||
|
||||
---
|
||||
|
||||
## 0a. App state: desired / in-flight / observed — S-1 CONTRACT (2026-08-02, decision D-b, R-166)
|
||||
|
||||
> **This section is a live contract, not migration history** — the rest of this document is the
|
||||
> v0.33 keep/port/delete inventory. Read this before touching `stacks/`, `backup/` or `bootrecon/`.
|
||||
> Shipped in controller **v0.189.0**.
|
||||
|
||||
An app's state is **three different kinds of information**, and conflating them is what produced
|
||||
R-157 mechanism B and F-CRIT-1. They are stored differently on purpose.
|
||||
|
||||
| Kind | Question it answers | Where it lives | Persisted? |
|
||||
|---|---|---|---|
|
||||
| **Desired** | *What did the customer ask for?* | `app.yaml` → `desired_state` | Yes, beside the app's other settings |
|
||||
| **In-flight** | *Is an operation part-way through, and did it finish?* | its **own** marker file under `<data_dir>` | Yes, written before the operation and cleared after |
|
||||
| **Observed** | *Is it running, unhealthy, restarting, is its drive gone?* | nowhere | **No — rebuilt by looking** |
|
||||
|
||||
**The rule that ties them together: never derive one from another.** The defect this replaced did
|
||||
exactly that — it derived *desired* from *observed* (zero containers ⇒ "the customer stopped it"),
|
||||
and zero containers is equally what a power cut mid-compose, an interrupted deploy and an interrupted
|
||||
backup leave behind. Two real faults were therefore read as deliberate stops and stranded silently.
|
||||
|
||||
### Desired — `app.yaml`, `desired_state`
|
||||
|
||||
Tri-state: `""` (unknown) · `"running"` · `"stopped"`.
|
||||
|
||||
- **ONE OWNER: the customer's own action.** Writers are the `/api/stacks/{name}/{action}` switch,
|
||||
`DeployStack`, `UpdateOptionalConfig`'s redeploy branch, and the `.fab` import. **`StartStack` and
|
||||
`StopStack` are NOT writers** — a census found 14 callers of which only 2 are the customer; the
|
||||
rest are quiesce, the backup volume dump, offbox reconstitution, app export/restore, the storage
|
||||
gate, migration and the boot reconciler. Intent recorded in the primitive would make a nightly
|
||||
backup indistinguishable from the customer pressing Stop.
|
||||
- **Written BEFORE the act; a failed write REFUSES the act.**
|
||||
- **Absent means UNKNOWN — never "running".** Every `app.yaml` predating v0.189.0 lacks the field,
|
||||
so consumers must fall back to the pre-v0.189.0 behaviour rather than assume. A running-only
|
||||
backfill converges the unambiguous cases; **`stopped` is never inferred, from any signal.**
|
||||
|
||||
### In-flight — a marker file, one per owner
|
||||
|
||||
Two exist and they are deliberately **separate files**: `quiesce-state.json` (the whole-guest backup
|
||||
window, `internal/quiesce`) and `appstop-state.json` (app-data operations that stop an app —
|
||||
`backup.AppStopGuard`, covering the volume dump, offbox reconstitution and `.fab` export). **One
|
||||
file, one writer**: sharing would give one record two lifetimes, and one owner clearing the other's
|
||||
note is a stranded app by a different route.
|
||||
|
||||
- Written **before** the stop; cleared **only** after a restart that succeeded; a **failed** restart
|
||||
keeps the marker so the next startup retries.
|
||||
- **A `defer` is not the mechanism.** A SIGKILL runs no deferred function — established on live
|
||||
hardware by Campaign 8 fault 10, where what brought the stacks back was the marker read at startup.
|
||||
- Recovery runs at startup and **completes before** the boot reconciler is launched, so an app the
|
||||
marker explains is not also reported as an unexplained boot orphan.
|
||||
|
||||
### Observed — not persisted, by design
|
||||
|
||||
`aggregateState` walks **every container** of a stack and any unhealthy or mixed result wins, so a
|
||||
partly-dead app cannot read as healthy (F-CRIT-1's shape). This requirement is met here and must not
|
||||
be re-implemented downstream. Nothing about observed state is written to disk: a controller restart
|
||||
re-observes it within one refresh, whereas persisting it risks carrying a stale verdict across the
|
||||
very restart that fixed it (the same argument as `RestartingSince`).
|
||||
|
||||
### The binding safety rule (verbatim, from decision D-b)
|
||||
|
||||
> *Losing the state store must never cause an app to be deleted, restarted wrongly, or reported
|
||||
> healthy when it is not — the worst acceptable outcome is re-running a backup that already ran.*
|
||||
|
||||
Applied: a lost or corrupt marker means the app is not auto-restarted **by that mechanism**, which is
|
||||
the pre-v0.189.0 position, not a new hazard. A lost `app.yaml` already means the app is not deployed.
|
||||
**Nothing here may make an absent file more dangerous than a present one.**
|
||||
|
||||
### Not migrated
|
||||
|
||||
`shouldRecreateOnBoot` (`internal/web/intermediary.go`) — the drive-backed boot gate — still derives
|
||||
desired from observed via `hasContainers`. Tracked as **R-170**.
|
||||
|
||||
---
|
||||
|
||||
## 1. v0.33 module inventory (package → purpose, key deps)
|
||||
|
||||
| Package | Purpose | Key internal deps |
|
||||
|
||||
Reference in New Issue
Block a user