|
|
|
@@ -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)
|