ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation with three kinds #262
@@ -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 <base>` 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)
|
||||||
@@ -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)
|
- **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)
|
- **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)
|
- **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
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: proposed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-09-27
|
updated: 2026-10-01
|
||||||
decisions:
|
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/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
|
- 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
|
(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.
|
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 now, and why not yet
|
||||||
|
|
||||||
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
|
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: located
|
||||||
opened: 2026-10-01
|
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:
|
fixed-by:
|
||||||
amended-design: []
|
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
|
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
|
build finishes; live, the controller's log during the next catalogue merge shows registrations
|
||||||
interleaved with the merge's own.
|
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.
|
||||||
|
|||||||
@@ -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
|
*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,
|
merge of a dependent repository before its prerequisite is held and named; `builds` lists asked,
|
||||||
running and built.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user