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..cb5c1b6 --- /dev/null +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -0,0 +1,111 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-01 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.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. The edges are derived from facts +recorded at two moments and written by nobody: registration records the manifest (`declared`, and +`built-by` for anything with a source), a build's take-in records what the image was built on and +which repositories it read (`stands-on`, `packages`). A module's first build places it by its +declared edges alone; from its second it is placed by what was true. + +**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means +B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build* +dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the +next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means +B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports. +One cycle is real and resolved by the kinds themselves: the runtime image is built by the build +machine, and the build machine stands on the runtime image; the image comes first, built by the +build machine that is running, which is the only one there could be — a `built-by` edge never orders +a module after a build machine that stands on it. A provision is not a dependency of this relation: +a consumer binds to its provider through what the push renders, and a change to the provider's image +changes nothing in the consumer's; a consumer whose build does read a provider's source declares it. +"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and +not the plan's. + +**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` 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 along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said | +| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases | +| 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-says-what-it-does-on-the-bus-as-it-happens.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..1ea8dd8 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-says-what-it-does-on-the-bus-as-it-happens.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.