Read a rebuild of an unchanged source as no move, whatever image digest it made (hq issue 280)
mesh/merge-gate error: the check could not run: a throwaway postgres:17-alpine could not be raised: docker run --label mesh.build=build-1791317509716888018…
mesh/delivery delivered
mesh/merge-gate error: the check could not run: a throwaway postgres:17-alpine could not be raised: docker run --label mesh.build=build-1791317509716888018…
mesh/delivery delivered
An image is not byte-reproducible, so ADR 0236's 'same artifacts is no move' never held for one: a catalogue merge that did not touch the bus rebuilt it, and every send to the control node waited for a planned bus upgrade. The builder now records a source fingerprint per build (module tree, context trees, bases and toolchains by digest). A rebuild with the fingerprint of the build it repeats is registered with that build's artifacts, handed to modules standing on it, holds no push, demands no bus step, and a plan sends and gates nothing for it. Identical artifacts remain a second way to be no move.
This commit is contained in:
@@ -292,6 +292,7 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
||||
})
|
||||
}
|
||||
result.Against = built.Against
|
||||
result.SourceFingerprint = built.Source
|
||||
for _, r := range built.Read {
|
||||
result.Read = append(result.Read, link.ReadRepository{Repository: r.Repository, Ref: r.Ref})
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ func buildOnce(ctx context.Context, args []string) error {
|
||||
Ref: *ref,
|
||||
Manifest: built.Manifest,
|
||||
Against: built.Against,
|
||||
Source: built.Source,
|
||||
}
|
||||
for _, r := range built.Read {
|
||||
out.Read = append(out.Read, readRepository{Repository: r.Repository, Ref: r.Ref})
|
||||
@@ -135,6 +136,7 @@ type onceResult struct {
|
||||
Made []madeArtifact `json:"made"`
|
||||
Against []string `json:"against,omitempty"`
|
||||
Read []readRepository `json:"read,omitempty"`
|
||||
Source string `json:"source-fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
// readRepository is a repository this build read source from besides the module's own.
|
||||
|
||||
@@ -148,6 +148,8 @@ func buildFrom(result link.BuildResult) inventory.Build {
|
||||
// be, for exactly the modules it needs most. Keeping them is what makes a replay able to
|
||||
// rebuild the graph rather than a list of names.
|
||||
Path: result.Path,
|
||||
// What it was made from (novox/hq issue 280): two builds with one are one build.
|
||||
SourceFingerprint: result.SourceFingerprint,
|
||||
}
|
||||
// When it was asked, which is what orders it against another build of the same module
|
||||
// (novox/hq 04-ISSUES/219) — not when it was heard.
|
||||
@@ -580,6 +582,23 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
|
||||
"back: it is recorded and not registered again — a newer build is", result.On, manifest.Module,
|
||||
short(result.Commit))
|
||||
}
|
||||
// **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from
|
||||
// what the build the mesh stands on was made from registers that build's artifacts at the new
|
||||
// commit, so no machine is sent a new digest for a source nobody changed — an image is not
|
||||
// byte-reproducible, and the bus rebuilt for another module's merge demanded a planned upgrade.
|
||||
if kept.SourceFingerprint != "" {
|
||||
stands, raw, err := inv.StandingBuild(ctx, manifest.Module, kept.ID)
|
||||
if err != nil {
|
||||
return manifest, kept, err
|
||||
}
|
||||
if stands != "" && stands != kept.ID && len(raw) > 0 {
|
||||
if same, err := catalogue.ParseManifest(raw); err == nil && same.Module == manifest.Module {
|
||||
fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n",
|
||||
manifest.Module, short(result.Commit), stands)
|
||||
manifest = same
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, manifest, recorded); err != nil {
|
||||
if errors.Is(err, inventory.ErrSuperseded) {
|
||||
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w",
|
||||
|
||||
@@ -54,13 +54,15 @@ type busPending struct {
|
||||
machines []string
|
||||
from map[string]string
|
||||
to string
|
||||
// same are the commits whose build made the same artifacts and manifest as the build the mesh holds.
|
||||
// same are the commits whose build was made from the same source as the build the mesh holds, or
|
||||
// made the same artifacts and manifest (novox/hq issue 280).
|
||||
same map[string]bool
|
||||
}
|
||||
|
||||
// moves is whether sending the machine would replace its bus: a build it was not last sent, unless the
|
||||
// two builds made the same artifacts from the same manifest — a rebuild of the same source for another
|
||||
// module's merge changes nothing the machine runs.
|
||||
// two builds were made from the same source, or made the same artifacts from the same manifest — a
|
||||
// rebuild of the same source for another module's merge changes nothing the machine runs, whatever
|
||||
// image digest it made (novox/hq issue 280).
|
||||
func (b busPending) moves(machine string) bool {
|
||||
from, known := b.from[machine]
|
||||
if b.module == "" || b.to == "" || (known && sameCommit(from, b.to)) {
|
||||
@@ -100,6 +102,15 @@ func pendingBus(ctx context.Context, inv *inventory.Inventory) (busPending, erro
|
||||
for commit, refs := range made {
|
||||
b.same[commit] = refs != "" && refs == made[b.to]
|
||||
}
|
||||
sources, err := inv.BuildSourceFingerprints(ctx, b.module)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
for commit, src := range sources {
|
||||
if src != "" && src == sources[b.to] {
|
||||
b.same[commit] = true
|
||||
}
|
||||
}
|
||||
for _, n := range b.machines {
|
||||
sent, known, err := inv.SentBuilds(ctx, n)
|
||||
if err != nil {
|
||||
|
||||
@@ -72,8 +72,10 @@ var errWalkedElsewhere = errors.New("a plan already walking a build there sends
|
||||
type moveFacts struct {
|
||||
current map[string]inventory.CurrentBuild
|
||||
fps map[string]map[string]string
|
||||
passed map[string]map[string]bool
|
||||
plans []inventory.Plan
|
||||
// srcs is, per module, per commit, its build's source fingerprint (novox/hq issue 280).
|
||||
srcs map[string]map[string]string
|
||||
passed map[string]map[string]bool
|
||||
plans []inventory.Plan
|
||||
}
|
||||
|
||||
func readMoveFacts(ctx context.Context, inv *inventory.Inventory) (moveFacts, error) {
|
||||
@@ -85,6 +87,9 @@ func readMoveFacts(ctx context.Context, inv *inventory.Inventory) (moveFacts, er
|
||||
if f.fps, err = inv.Fingerprints(ctx); err != nil {
|
||||
return f, err
|
||||
}
|
||||
if f.srcs, err = inv.SourceFingerprints(ctx); err != nil {
|
||||
return f, err
|
||||
}
|
||||
if f.passed, err = inv.PassedCommits(ctx); err != nil {
|
||||
return f, err
|
||||
}
|
||||
@@ -92,12 +97,17 @@ func readMoveFacts(ctx context.Context, inv *inventory.Inventory) (moveFacts, er
|
||||
return f, err
|
||||
}
|
||||
|
||||
// identical is whether two builds of a module put the same thing on a machine: the same commit, or
|
||||
// builds that made the same artifacts from the same manifest.
|
||||
// identical is whether two builds of a module put the same thing on a machine: the same commit,
|
||||
// builds made from the same source (novox/hq issue 280) — the module's tree, the contexts it read, its
|
||||
// bases and toolchains, whatever digests an image rebuild made of them — or builds that made the same
|
||||
// artifacts from the same manifest.
|
||||
func (f moveFacts) identical(module, a, b string) bool {
|
||||
if a == b || sameCommit(a, b) {
|
||||
return true
|
||||
}
|
||||
if sa := f.srcs[module][a]; sa != "" && sa == f.srcs[module][b] {
|
||||
return true
|
||||
}
|
||||
fa := f.fps[module][a]
|
||||
return fa != "" && fa == f.fps[module][b]
|
||||
}
|
||||
@@ -112,6 +122,40 @@ func (f moveFacts) gated(module, commit string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// unchangedOnEvery is whether a plan's build of a module was made from the source of the build every
|
||||
// machine running it was last sent (novox/hq issue 280): no move on any of them. False when no machine
|
||||
// runs it, when what one was sent is not known, or when the build stands for itself.
|
||||
func unchangedOnEvery(ctx context.Context, inv *inventory.Inventory, module, build string, running []string) (bool, error) {
|
||||
if build == "" || len(running) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
same, err := inv.SameSourceCommits(ctx, module, build)
|
||||
if err != nil || len(same) == 0 {
|
||||
return false, err
|
||||
}
|
||||
for _, n := range running {
|
||||
sent, known, err := inv.SentBuilds(ctx, n)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
was, carried := sent[module]
|
||||
if !known || !carried || !inRun(same, was) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// inRun is whether a commit is one of a run's, however either is abbreviated.
|
||||
func inRun(run map[string]bool, commit string) bool {
|
||||
for c := range run {
|
||||
if sameCommit(c, commit) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// moves is what a machine's next send would move that no gate has seen: modules whose policy rolls out
|
||||
// (a recorded one is a person's push), that the machine was sent before, moving to a build not identical
|
||||
// to the one it runs and that has passed no gate. A machine whose last send's builds are not known names
|
||||
|
||||
@@ -620,6 +620,20 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// **A build whose source is unchanged is never a move** (novox/hq issue 280): made from the source
|
||||
// of the build every machine running it was sent, it is registered with that build's artifacts —
|
||||
// nothing to send, and nothing for a gate to judge.
|
||||
if state.FirstAt == nil {
|
||||
if same, err := unchangedOnEvery(ctx, inv, m, state.Build, running); err != nil {
|
||||
return false, err
|
||||
} else if same {
|
||||
now := time.Now().UTC()
|
||||
state.SentAt = &now
|
||||
state.Why = "no move: made from the source every machine running it runs"
|
||||
fmt.Printf("%s: %s built from the source every machine running it runs: no move, nothing sent\n", p.ID, m)
|
||||
continue
|
||||
}
|
||||
}
|
||||
policy, err := inv.UpgradeOf(ctx, m)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// A build whose source is unchanged is never a move (novox/hq issue 280): an image is not
|
||||
// byte-reproducible, so two builds of one source make two digests, and the rule that a rebuild with
|
||||
// identical artifacts is no move (ADR 0236) never held for one.
|
||||
|
||||
// anImageBuild is a build outcome of an image module: its manifest names the image by the digest made.
|
||||
func anImageBuild(t *testing.T, module, id, commit, digest, source string, asked time.Time) link.BuildResult {
|
||||
t.Helper()
|
||||
image := "registry.invalid:5000/" + module + "/server@sha256:" + digest
|
||||
manifest, _ := json.Marshal(map[string]any{"module": module, "version": "1",
|
||||
"resources": []any{map[string]any{"id": "svc", "type": "container", "name": module, "image": image}}})
|
||||
return link.BuildResult{ID: link.NewBuildID(asked), Repository: "novox/mesh-catalog", Path: "modules/" + module,
|
||||
On: "anchor", Module: module, Commit: commit, Manifest: manifest, SourceFingerprint: source,
|
||||
Made: []link.MadeArtifact{{Name: "server", Kind: "image", Reference: image}},
|
||||
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}
|
||||
}
|
||||
|
||||
// shelfNames is whether the module the mesh holds names this digest.
|
||||
func shelfNames(t *testing.T, inv *inventory.Inventory, module, digest string) bool {
|
||||
t.Helper()
|
||||
shelf, err := inv.Catalogue(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(shelf[module])
|
||||
return strings.Contains(string(raw), digest)
|
||||
}
|
||||
|
||||
// A rebuild of an unchanged source is registered with the artifacts of the build it was first made
|
||||
// as: no machine is sent a new digest, a module standing on it is handed the same base, and the two
|
||||
// builds are one to every rule that asks whether a send moves something. A real source change moves.
|
||||
func TestARebuildOfAnUnchangedSourceKeepsItsArtifacts(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
start := time.Now().Add(-time.Hour)
|
||||
for i, b := range []link.BuildResult{
|
||||
anImageBuild(t, "app", "", "c1aaaaaa", strings.Repeat("a", 64), "src1:same", start),
|
||||
// Another module's merge rebuilt it: a new commit, a new image digest, the same source.
|
||||
anImageBuild(t, "app", "", "c2bbbbbb", strings.Repeat("b", 64), "src1:same", start.Add(time.Minute)),
|
||||
} {
|
||||
if _, _, err := takeIn(ctx, inv, b); err != nil {
|
||||
t.Fatalf("build %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if !shelfNames(t, inv, "app", strings.Repeat("a", 64)) || shelfNames(t, inv, "app", strings.Repeat("b", 64)) {
|
||||
t.Fatal("a rebuild of an unchanged source registered the digest it made, not the one the mesh holds")
|
||||
}
|
||||
if src, err := inv.SourceOf(ctx, "app"); err != nil || src.BuiltFrom != "c2bbbbbb" {
|
||||
t.Fatalf("the module is not at the commit it was rebuilt from: %+v %v", src, err)
|
||||
}
|
||||
held, err := inv.Held(ctx)
|
||||
if err != nil || !strings.HasSuffix(held["app/server"], strings.Repeat("a", 64)) {
|
||||
t.Fatalf("a module standing on it is handed %q, not the build the mesh holds (%v)", held["app/server"], err)
|
||||
}
|
||||
f, err := readMoveFacts(ctx, inv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !f.identical("app", "c1aaaaaa", "c2bbbbbb") {
|
||||
t.Fatal("two builds of one source are not one build")
|
||||
}
|
||||
|
||||
// A real change to the source moves: its own digest registered, and not identical.
|
||||
if _, _, err := takeIn(ctx, inv, anImageBuild(t, "app", "", "c3cccccc", strings.Repeat("c", 64), "src1:changed",
|
||||
start.Add(2*time.Minute))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !shelfNames(t, inv, "app", strings.Repeat("c", 64)) {
|
||||
t.Fatal("a changed source did not register what it made")
|
||||
}
|
||||
if f, _ = readMoveFacts(ctx, inv); f.identical("app", "c2bbbbbb", "c3cccccc") {
|
||||
t.Fatal("a changed source is read as the same build")
|
||||
}
|
||||
// And a builder that says no fingerprint is told apart by its artifacts alone, as before.
|
||||
if _, _, err := takeIn(ctx, inv, anImageBuild(t, "app", "", "c4dddddd", strings.Repeat("d", 64), "",
|
||||
start.Add(3*time.Minute))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !shelfNames(t, inv, "app", strings.Repeat("d", 64)) {
|
||||
t.Fatal("a build with no fingerprint did not register what it made")
|
||||
}
|
||||
}
|
||||
|
||||
// The bus rebuilt for another module's merge, its source untouched and its image digest new: no move —
|
||||
// no push to its machine is held, no bus step is demanded, and `bus upgrade` has nothing to do. A real
|
||||
// change to the bus's source is the planned step again.
|
||||
func TestABusRebuiltFromAnUnchangedSourceDemandsNoBusStep(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
start := time.Now().Add(-time.Hour)
|
||||
bus := func(id, commit, digest, source string, at time.Time) link.BuildResult {
|
||||
b := anImageBuild(t, "nats", id, commit, digest, source, at)
|
||||
manifest, _ := json.Marshal(map[string]any{"module": "nats", "version": "2",
|
||||
"provides": []any{map[string]any{"name": "mesh-bus"}},
|
||||
"resources": []any{map[string]any{"id": "svc", "type": "container", "name": "nats",
|
||||
"image": b.Made[0].Reference}}})
|
||||
b.Manifest = manifest
|
||||
return b
|
||||
}
|
||||
if _, _, err := takeIn(ctx, inv, bus("", "n1111111", strings.Repeat("a", 64), "src1:bus", start)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.Assign(ctx, "anchor", "nats"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"nats": "n1111111"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The wide rebuild: a new commit, a new image digest, the same source.
|
||||
if _, _, err := takeIn(ctx, inv, bus("", "n2222222", strings.Repeat("b", 64), "src1:bus", start.Add(time.Minute))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if held, err := busHeld(ctx, inv, []string{"anchor"}); err != nil || len(held) != 0 {
|
||||
t.Fatalf("a bus rebuilt from an unchanged source held its machine: %v %v", held, err)
|
||||
}
|
||||
wasSnapshot := takeBusSnapshot
|
||||
t.Cleanup(func() { takeBusSnapshot = wasSnapshot })
|
||||
takeBusSnapshot = func(context.Context, string, string) (string, error) {
|
||||
return "", errors.New("no snapshot is taken for a bus step nobody needs")
|
||||
}
|
||||
var sent [][]string
|
||||
wasSend := sendRollout
|
||||
sendRollout = func(_ context.Context, _ *stores, names []string) ([]string, error) {
|
||||
sent = append(sent, names)
|
||||
return names, nil
|
||||
}
|
||||
t.Cleanup(func() { sendRollout = wasSend })
|
||||
if err := busCommand(ctx, []string{"upgrade", "--why", "nothing changed", "--reversible"}); err != nil || len(sent) != 0 {
|
||||
t.Fatalf("a bus step ran for a bus whose source is unchanged: %v, sent %v", err, sent)
|
||||
}
|
||||
if !shelfNames(t, inv, "nats", strings.Repeat("a", 64)) {
|
||||
t.Fatal("the bus the mesh holds is not the image its machine runs")
|
||||
}
|
||||
|
||||
// A real change to the bus's source: the planned step.
|
||||
if _, _, err := takeIn(ctx, inv, bus("", "n3333333", strings.Repeat("c", 64), "src1:bus-2.12", start.Add(2*time.Minute))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held, err := busHeld(ctx, inv, []string{"anchor"})
|
||||
if err != nil || !strings.Contains(held["anchor"], "planned step") {
|
||||
t.Fatalf("a changed bus did not demand its planned step: %v %v", held, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A plan's build made from the source every machine running the module runs is not sent and not
|
||||
// judged: nothing moves. A build of a changed source is sent to its first machine, gated, as before.
|
||||
func TestAPlanSendsNothingForAnUnchangedSource(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
source string
|
||||
sent [][]string
|
||||
}{
|
||||
{"unchanged", "src1:app", nil},
|
||||
{"changed", "src1:app-changed", [][]string{{"anchor"}}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
g := aGateMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := g.open.inventory
|
||||
// What anchor and laptop run (c1) was made from src1:app; the plan's build of c2 from the source
|
||||
// the case says.
|
||||
for _, b := range []inventory.Build{
|
||||
{ID: "build-1s", Module: "app", Commit: "c1", SourceFingerprint: "src1:app",
|
||||
Asked: time.Now().Add(-30 * time.Second), At: time.Now().Add(-30 * time.Second)},
|
||||
{ID: "build-2s", Module: "app", Commit: "c2", SourceFingerprint: tc.source,
|
||||
Asked: time.Now(), At: time.Now()},
|
||||
} {
|
||||
b.Manifest, _ = json.Marshal(catalogue.Manifest{Module: "app", Version: "1"})
|
||||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
p := g.plan(t)
|
||||
p.Modules["app"].Build = "build-2s"
|
||||
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
advancePlans(ctx, g.open)
|
||||
if !reflect.DeepEqual(g.sent, tc.sent) {
|
||||
t.Fatalf("sent %v, want %v", g.sent, tc.sent)
|
||||
}
|
||||
p = g.plan(t)
|
||||
s := p.Modules["app"]
|
||||
if tc.sent == nil && (s.SentAt == nil || s.Gate != nil || !strings.Contains(s.Why, "no move")) {
|
||||
t.Fatalf("an unchanged source was not read as no move: %+v", s)
|
||||
}
|
||||
if tc.sent != nil && (s.Gate == nil || len(s.First) == 0) {
|
||||
t.Fatalf("a changed source was not sent to its first machine, gated: %+v", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user