Merge pull request 'Issue 280: a rebuild of an unchanged source was read as a new bus' (#153) from issues/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus into main
This commit was merged in pull request #153.
This commit is contained in:
+14
@@ -143,6 +143,20 @@ machine. So:
|
||||
waits and the remedy;
|
||||
- **a rebuild that made the same artifacts from the same manifest is no move.**
|
||||
|
||||
> **Progressive insight — 2026-10-06.** This rule assumed that a rebuild changing nothing makes the same
|
||||
> artifacts. That holds for an archive or a bundle, which the builder packs deterministically. It does
|
||||
> not hold for an image: every build of an unchanged source makes a new image digest. So a catalogue
|
||||
> merge that never touched the bus rebuilt it, the new digest read as a new bus build, and every send
|
||||
> to the control node was refused until a planned bus upgrade
|
||||
> ([issue 280](../04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md)).
|
||||
> The rule said "a rebuild that made the same artifacts from the same manifest". It now also covers
|
||||
> **a rebuild made from the same source**: the module's tree at the commit, the trees of the contexts
|
||||
> it read, and its bases and toolchains by digest, hashed by the builder as the build's source
|
||||
> fingerprint. Such a rebuild is registered with the artifacts of the build it repeats, so no machine
|
||||
> is sent a new digest. Identical artifacts remain the second way to be no move, and the only way for
|
||||
> a build the registry decides. The decision stands: a rebuild that changes nothing is no move. Only
|
||||
> the test for "changes nothing" was wrong.
|
||||
|
||||
**The release plan walks what waits.** Whenever builds no gate has seen wait on machines and no plan that
|
||||
has started walks them, the mesh opens a release plan: every such machine heard from, **one at a time,
|
||||
the control node last**, each sent everything waiting there and judged by the gate before the next is
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-06
|
||||
located-in: [mesh-controller cmd/mesh-controller, mesh-controller internal/builder, mesh-controller internal/inventory]
|
||||
fixed-by: mesh-controller PR #99
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 280. A rebuild of an unchanged source was read as a new bus
|
||||
|
||||
## Symptom
|
||||
|
||||
On 2026-10-06 a catalogue merge added the merge check (a script at the repository's root) and changed
|
||||
the forge module. Nothing under the bus module's directory changed. The merge rebuilt the bus all the
|
||||
same, and the controller read the rebuild as a new bus build. From then on every send to the control
|
||||
node was refused until someone ran a planned `bus upgrade`. That was a deadlock. The fix for issue 278,
|
||||
which narrows what a merge rebuilds, was itself waiting to be sent to the control node. A person took
|
||||
one bus step by hand to break it.
|
||||
|
||||
## Diagnosis
|
||||
|
||||
**Two builds of one source counted as two builds.** [ADR 0236](../../02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)
|
||||
treats a rebuild as no move when it made the same artifacts from the same manifest. That works for an
|
||||
archive or a bundle: the builder packs those deterministically, so one source gives one digest. **An
|
||||
image is not byte-reproducible.** Every `docker build` of an unchanged source makes a new image
|
||||
digest. So for every module shipped as an image, the bus among them, a rebuild was always a move. The
|
||||
bus's guard did the rest: no send reaches the bus's machine while a new bus build waits there.
|
||||
|
||||
The rule asked the wrong question. What the mesh needs to know is whether the build was made from
|
||||
something different. The builder can answer that, because it reads only what it was given. The recipe
|
||||
and the compiler see the module's own directory and nothing else. Any other repository an artifact
|
||||
reads is cloned at a ref the manifest names. Every base is handed over by digest. A TypeScript or Go
|
||||
bundle is compiled in a toolchain image the mesh holds by digest.
|
||||
|
||||
**Why the merge rebuilt the bus.** The first guess was the shared-code rule from before issue 278,
|
||||
still live because the new announcer had not been deployed. That guess is wrong. The merge changed a
|
||||
file at the repository's root, the merge check script. **A file at the root is shared code under both
|
||||
rules**: issue 278 lets a directory holding a module claim its own files, and the root is never such a
|
||||
directory. The controller's `plans` what-if, which saves nothing, shows this. It was given the merge's
|
||||
files with the announcer's answer (the forge module's directory holds a module). It planned 103
|
||||
modules. The same files without the root script plan the forge module alone. **The new announcer would
|
||||
not have narrowed this merge.**
|
||||
|
||||
## Fix
|
||||
|
||||
- **The builder says what each build was made from**: its source fingerprint, a hash of
|
||||
- the git tree of the module's directory at the commit built, which covers its manifest, its recipes
|
||||
and its code;
|
||||
- the git tree of each other repository an artifact's context is cloned from;
|
||||
- every base it was handed, by digest only, so the registry address it was copied to does not count;
|
||||
- for a bundle, the compiler image's digest and the builder's own recipe for that language.
|
||||
|
||||
A build that pulls packages from the mesh's package registry at build time gets no fingerprint. That
|
||||
covers a package artifact, a TypeScript bundle with packages of its own, and an image whose recipe
|
||||
reads the registry credential. The same source can give a different program there, so such a build
|
||||
is told apart by its artifacts alone, as before. The controller records the fingerprint with each
|
||||
build.
|
||||
- **A build whose source is unchanged is no move.** The controller treats two builds of a module with
|
||||
one fingerprint as one build. It registers the rebuild at its new commit but with the artifacts of
|
||||
the build it repeats, which is the oldest build in the unbroken run with that fingerprint. A build
|
||||
that failed its gate breaks the run. As a result, no machine is ever sent a new digest for a source
|
||||
nobody changed. A module that stands on it is handed the same base, so it does not move either. The
|
||||
bus's planned step asks for nothing. A plan whose build matches what every machine running the
|
||||
module was sent sends nothing and judges nothing. Identical artifacts remain a second way to be no
|
||||
move.
|
||||
|
||||
## Left open
|
||||
|
||||
The wide rebuild itself. The builder reads only a module's own directory, so a file at a repository's
|
||||
root, outside every module's directory, is read by no module built from a subdirectory. The controller
|
||||
still treats such a file as shared code and rebuilds everything built from the repository. With this
|
||||
fix that costs build time and no longer moves anything. Narrowing it belongs to the controller's
|
||||
planning rule (issue 278's area) and is not done here.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| a fingerprint names what a build was made from | the builder's test: one tree and one base give one fingerprint, even with the base copied to another registry address; a changed tree or a moved base gives another; a build the registry decides, or whose tree was not named, has none |
|
||||
| an unchanged source keeps its artifacts | the controller's test: an image module rebuilt from an unchanged source with a new digest is registered with the digest the mesh held, at the new commit; modules standing on it are handed that digest; the two builds are one; a changed source registers and moves; a build with no fingerprint is told apart by its artifacts |
|
||||
| no bus step for an unchanged bus | the controller's test: the bus rebuilt from an unchanged source with a new digest holds no push to its machine, `bus upgrade` has nothing to do and takes no snapshot, and a real change to its source demands the planned step again |
|
||||
| a plan sends nothing for an unchanged source | the controller's test: a plan's build made from the source every machine runs is marked sent with "no move", with no send and no gate; a changed source is sent to its first machine and gated |
|
||||
| live | the next catalogue merge that rebuilds the bus without touching its directory demands no bus step; `bus` says every machine runs the build the mesh holds |
|
||||
Reference in New Issue
Block a user