From b0a74b23fd778373a1764b6197422c0f83c291a1 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 17:45:48 +0200 Subject: [PATCH] ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation; design 30; issues 184, 186 --- ...e-produces-a-tiered-plan-the-mesh-keeps.md | 92 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../30-the-mesh-updates-itself-on-a-push.md | 17 +++- .../00-report.md | 10 +- .../00-report.md | 7 ++ 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md diff --git a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md new file mode 100644 index 0000000..7da271b --- /dev/null +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -0,0 +1,92 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-01 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0157-a-build-narrates-on-the-bus.md +--- + +# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue + +## Context + +A merge on the forge reaches the controller as an event, and the controller asks the build +machine for what that merge changed. Until today that meant the modules whose recorded source is +that repository; since this afternoon it also means everything standing on what moved +([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)). +Both are done inside the handler that received the event: it asks one build, waits for it, asks the +next, and returns when the last is done. Three things followed from that shape on 2026-10-01: + +- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime + image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)). +- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as + history, and the dependents are asked by hand. +- Nothing is deployed between builds. A merge that changes the build machine and something the build + machine builds asks for both in order, but the second is built by whichever build machine is running + — the old one, unless somebody pushed in between. The order the dependents are sorted in exists for + the artifacts; it says nothing about what must be *running*. + +And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a +build was made against, the repositories a build read, and the fact that every source-built module is +built by the build machine, each read by a different function in the merge handler. + +## Decision + +**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the +kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the +module's build reads the other's repository), `built-by` (the module is built by the holder of the +build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query +of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside +the controller needs it — and nothing else computes an edge. + +**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge +changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers: +tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it +answers, the tiers, and each module's state — is written to the store before any build is asked. The +handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every +outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store. + +**3. A tier is done when it is built, and when what the next tier needs from it is running.** A +module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next +tier is asked only once every module in this tier is built and every rolled-out module of this tier +that a later tier is `built-by` or `packages` has been applied by the machines running it — the +machines' reports say so. A module whose policy says *record* is built and not waited for. So a merge +touching the build machine and the controller builds the build machine, waits until it is the build +machine that is running, and only then asks for the controller's build. + +**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it +is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built. +A plan that has waited past a bound is named red there, which is the first fact of +[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list. + +## Consequences + +- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue + 184's remaining cause — a handler that waits for its own work — is removed rather than worked + around; the bus's heartbeats stop being dropped under a merge. +- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are + in the queue (mesh-controller 194). +- A release across repositories is a plan whose edges cross repositories; the order a person kept in + a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan, + not by a separate release record. +- The explicit `build --on ` stays as the way to ask for the same plan by hand. +- A module's `build.on` remains the one place a manifest states a dependency the store cannot see. + +## How this is checked + +| Rule | Checked by | +|---|---| +| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call | +| A merge's set is sorted into tiers, each depending only on earlier ones | a unit test on the tiering: the runtime, a module on it, a plugin on that, and an unrelated module left out | +| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome | +| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after | +| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone | +| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan | +| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked | + +## References + +- [ADR 0157](0157-a-build-narrates-on-the-bus.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) +- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md) +- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index da05750..52fb397 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -175,6 +175,7 @@ python3 00-META/checks/index.py fail if stale - **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) - **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) - **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md) +- **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md b/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md index 949fd41..cc86e06 100644 --- a/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md +++ b/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md @@ -2,8 +2,10 @@ layer: to-be status: proposed code: [] -updated: 2026-09-27 +updated: 2026-10-01 decisions: + - 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md + - 02-DECISIONS/0157-a-build-narrates-on-the-bus.md - 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md - 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md --- @@ -114,6 +116,19 @@ automate the freeze. (this is how the uplink managers and the re-registrations above were done). Only image-bearing modules need the build machine, which narrows what the deadlock above can block. +## What a merge does now (2026-10-01) + +Revision, [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md). The +trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made +the build narrate; this makes the merge a plan). A module's dependencies are one relation in the +catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes +what changed and everything reachable from it along those edges, sorts the set into tiers, writes the +plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose +rolled-out modules a later tier is built by waits until the machines report them applied; a +controller replaced mid-plan resumes from the store. `status` lists open plans and names one that +has waited too long. The transition discipline for breaking changes in the list above is still +unwritten, and still the next thing. + ## Why now, and why not yet **Why it matters:** self-update is the difference between a mesh a person maintains by typing diff --git a/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md b/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md index a01c796..c6bd562 100644 --- a/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md +++ b/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-10-01 -located-in: [mesh-controller internal/link/serve.go (act handles one message at a time; sourceMoved waits for every build the merge asks)] +located-in: [mesh-controller cmd/mesh-controller/upgrades.go (the merge handler builds inside the receive loop)] fixed-by: amended-design: [] --- @@ -50,3 +50,9 @@ busy should say so where `status` is read. and a build outcome for another module arrive together, and the outcome is recorded before the build finishes; live, the controller's log during the next catalogue merge shows registrations interleaved with the merge's own. + +## Decided, 2026-10-01 + +[ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): a merge +produces a tiered plan the store keeps; the handler asks the first tier and returns; outcomes advance +the plan; a controller replaced mid-plan resumes it. The loop is never held by a build again. diff --git a/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md b/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md index 501a1fb..91e01f1 100644 --- a/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md +++ b/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md @@ -65,3 +65,10 @@ this report asks for. *How this would be checked:* a builder restarted between an ask and its build still builds it; a merge of a dependent repository before its prerequisite is held and named; `builds` lists asked, running and built. + +## Decided, 2026-10-01 + +The third fault is answered by [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): +a release across repositories is a plan whose dependency edges cross repositories, sorted into +tiers and deployed tier by tier, read in `status`. The order a person kept is the order the tiers +give.