Merge pull request 'Issue 352: a gate read another send's report, and a put-back chose a controller older than its store' (#252) from issues/352-a-gate-read-another-sends-report-and-a-put-back-chose-a-controller-older-than-its-store into main
This commit was merged in pull request #252.
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-09
|
||||
located-in: [mesh-controller cmd/mesh-controller (gate.go, release.go, release_plan.go, acting.go, push.go), mesh-controller internal/inventory, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 352. A gate read another send's report, and a put-back chose a controller older than its store
|
||||
|
||||
## Symptom
|
||||
|
||||
On 2026-10-09, local times on the control node (its controller's and node-engine's journals, and `plans`):
|
||||
|
||||
- 16:19 a release sent the control node three core builds — the controller (2913c54c), the node-engine
|
||||
(d8ff1540) and the tool runner (83c19941). 16:21:43 the control node applied them; 16:21:53 its
|
||||
node-engine said each was healthy. The release's gate did not pass in the nine minutes that followed.
|
||||
- 16:30:56 a merge of the controller (e6b00e2e) made a plan; 16:31:37 that plan sent the control node the
|
||||
newer controller — a newer send to the same machine, while the release's gate was judging there;
|
||||
16:31:44 the serving controller was replaced by the newer one.
|
||||
- 16:32:20 the new process judged the release's gate **failed**: "not healthy within 10m0s of its apply:
|
||||
the control node has not reported on what it was sent" — and put all three builds back, on every
|
||||
machine, in one send. The control node had reported them, ten minutes earlier.
|
||||
- 16:32:24 the put-back controller (ef26d4cb) started and said it was behind its own row: it carried fewer
|
||||
migrations than the store had applied, and served what it could.
|
||||
- 16:35:59 that older controller judged the newer plan's gate **passed** on the control node ("healthy 3
|
||||
times over 4m17s") — from the health of the build it was itself, not of the build the plan had sent,
|
||||
which the put-back had removed from the machine. The plan walked on, believing the controller delivered.
|
||||
|
||||
## Why it is an issue
|
||||
|
||||
Four rules did not hold.
|
||||
|
||||
- **A gate judges its own send** (ADR 0236). The gate held the machine's report against the declaration
|
||||
sent *last*, which another plan's send had just replaced; the report on the gate's own send read as
|
||||
none. The report was never lost; it was compared with the wrong send.
|
||||
- **A gate judges the build it sent.** The controller's gate read the lease holder's readiness and the
|
||||
machine's report, and neither said *which* controller build was running. A put-back build passed the
|
||||
gate of the build it had replaced.
|
||||
- **A put-back puts back something that can run the mesh** (ADR 0236: "put back by something other than
|
||||
itself"). A controller older than the store's schema starts behind its own records. Nothing refused it.
|
||||
- **One walk of a module on a machine at a time** (issue 281, ADR 0236). A release's walk was invisible to
|
||||
the merge plan's send: a release keeps no module records, and the check for a walk in progress read
|
||||
only those.
|
||||
|
||||
The diagnosis and the fix are in [`01-diagnosis.md`](01-diagnosis.md).
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# 352. Diagnosis
|
||||
|
||||
## 2026-10-09
|
||||
|
||||
**The record.** A search of the records for "has not reported on what it was sent" and "older than the
|
||||
store" found nothing. Issue 335 (a passed gate failed by later reports) and issue 281 (a machine's condition
|
||||
read as a module's) are the nearest; neither is this.
|
||||
|
||||
### What is measured
|
||||
|
||||
- **D1, the report compared with the wrong send.** The gate reads each machine's last report beside the
|
||||
declaration the machine was *last sent* (`LastReports`: `Current` is "the report names the declaration
|
||||
in the node's `sent` column"). The release's send wrote `sent`; the control node reported on it, applied;
|
||||
the merge plan's send at 16:31:37 overwrote `sent`; from then the same report read as not current, and
|
||||
the gate's wait ended at its bound with "has not reported on what it was sent". The controller restart
|
||||
at 16:31:44 is in the timeline but is not the cause: the plan and the gate are kept in the store, and a
|
||||
fresh process read the same `sent` and the same report. Nothing was lost across it.
|
||||
- **D2, the build judged is not the build running.** `judgeHealth` for the controller asks: the lease is
|
||||
held since after the send, and the holder says ready. The holder's `Build` is the link-time version
|
||||
stamp, which the mesh's own builds do not set ("development build"), so it names no build. After the
|
||||
put-back, the registered controller build, the lease holder and the machine's last send all were the
|
||||
put-back build, and the newer plan's gate — comparing none of them with the build it sent — passed on
|
||||
the put-back build's health.
|
||||
- **D3, a put-back to a controller behind the store.** A failed gate restores the module's registered build
|
||||
to the one the first machine ran before (`PreviousBuild`) and sends it. Nothing asked whether that
|
||||
build can read the store as the failed build left it. The controller says so itself when it starts
|
||||
("this control plane is behind the mesh-controller row"), and serves anyway (ADR 0185).
|
||||
- **The merge plan's send crossed the release's gate.** `walkedBy` finds a walk in progress by a plan's
|
||||
module records (`p.Modules[module].First`). A release (the walk of the builds waiting for a gate) keeps its walk in its gate's carried moves,
|
||||
not in module records, so the release's open gate on the control node held nothing back. And a plan's
|
||||
own modules were never checked against another walk at all: the check ran over the moves a machine
|
||||
had waiting, and skipped the plan's own.
|
||||
|
||||
### What is guessed
|
||||
|
||||
- Why the release's gate had not passed in the nine minutes before the merge is not established. The
|
||||
node-engine said all three healthy at 16:21:53; the gate needs three passes forty seconds apart after a
|
||||
two-minute settle, and the controller's own readiness (its self-check run once, `status` within ten
|
||||
seconds). I think the controller's readiness came late — it was the build being judged — but I did not
|
||||
read its lease's health at the time.
|
||||
|
||||
## The fix
|
||||
|
||||
In mesh-controller, pull request 185.
|
||||
|
||||
- **A gate keeps what it sent, and reads the report against that.** Each gate keeps, per machine, the
|
||||
declaration its send carried — its digest and its sequence (`PlanGate.Sent`, read from the node's
|
||||
record right after the send). A report is on that send when it names that declaration, or one the
|
||||
machine was sequenced after it. The gate's judging and the merge plan's first-machine wait both read
|
||||
this; a gate kept before sends were kept reads the last send, as before. Kept with the plan in the
|
||||
store, so a controller restarted mid-judging reads the same.
|
||||
- **A gate judges only the build the machine was last sent.** Each gate judges what the machine was
|
||||
last sent of the module (`sent_builds`) against the commit it judges. Another build there means the
|
||||
judging is **superseded**: a new verdict, decided at once, with no verdict on the build, nothing marked,
|
||||
nothing put back; a release so superseded ends, and what still waits is released again; a merge plan
|
||||
so superseded ends, said, and the newer plan carries on.
|
||||
- **The controller is never put back to a build older than the store's schema.** A controller is told
|
||||
which build it is by its declaration (`MESH_CONTROLLER_VERSION`, the bundle's version, `${version}`
|
||||
composed by the catalogue — now also inside a process's env), and records when it serves the highest
|
||||
migration it carries, by that version (`controller_schema`, migration 0087). A failed controller gate
|
||||
reads the record of the build it would put back: one that reaches less than the store has applied, or
|
||||
never recorded what it reaches, is not put back; the current build is kept, and the condition
|
||||
(`rollback-failed`, urgent) says why and that a person decides. The lease holder's `Build` is that
|
||||
version too.
|
||||
- **A walk of a module on a machine holds every other send of another build of it there.** `walkedBy`
|
||||
reads a release's open gate as its walk, and a plan's own modules are held by it as any other move is:
|
||||
a merge plan whose first machine is under a release's gate for the same module waits, said as
|
||||
"waiting for … which release-… is walking". The same build to the same machine is not held: it is
|
||||
already there.
|
||||
|
||||
Not changed: whether a release's walk and a merge plan's walk of the core should be one walk (the walk
|
||||
carrying over) is left to a design; serialising them is what this fixes.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Check |
|
||||
|---|---|
|
||||
| A newer send to the judged machine does not unreport the gate's | `TestReplay352` (the backlog fixture: the release sends, the machine reports, another send lands unreported, the gate still passes) — fails on the commit before the fix with "has not reported on what it was sent" |
|
||||
| A send that moves the module supersedes the judging, puts nothing back | `TestReplay352`, `TestAPlansJudgingIsSupersededByASendThatMovesItsModule` |
|
||||
| A report is held against the gate's own send, by digest or sequence | `TestAReportIsHeldAgainstTheGatesOwnSend`, inventory `TestASendAndAReportAreKnownByTheirDeclaration` |
|
||||
| Only the build last sent is judged | `TestAGateJudgesOnlyTheBuildTheMachineWasLastSent` |
|
||||
| A walk holds other sends of the module there, a release's too | `TestAWalkWaitsForAJudgingOfTheSameModuleOnThatMachine`, `TestAPlansFirstSendWaitsForAReleaseJudgingTheModuleThere` |
|
||||
| No put-back to a controller behind the store, or one unproved | `TestTheControllerIsNotPutBackToABuildOlderThanTheStoresSchema`, inventory `TestASchemaReachIsKeptByBuild`, catalogue `TestAProcessEnvCanNameTheBuildsOwnVersion`, `TestTheHolderNamesTheBuildTheDeclarationToldIt` |
|
||||
| Every condition of the change is tested | 31 mutations, each failing a test |
|
||||
|
||||
A replay in mesh-lab's register is still owed before this core issue resolves (ADR 0237).
|
||||
Reference in New Issue
Block a user