From a245c2f1e979d2d10ae9e9cc92933477980389e4 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:23:30 +0100 Subject: [PATCH 1/4] Contract: the seat mesh-wants' want names the node that keeps a kept build in kept-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §526, §379, §271 --- contracts/go/clients/meshwantsclient/wants_test.go | 1 + contracts/go/seats/meshwants/contract.go | 4 +++- contracts/go/seats/meshwants/contract_test.go | 6 ++++++ contracts/seats/mesh-wants/fixtures/states.json | 13 +++++++++++++ contracts/seats/mesh-wants/seat.json | 4 ++++ contracts/ts/seats/meshwants/index.d.ts | 2 ++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/contracts/go/clients/meshwantsclient/wants_test.go b/contracts/go/clients/meshwantsclient/wants_test.go index 490fa692..a621583d 100644 --- a/contracts/go/clients/meshwantsclient/wants_test.go +++ b/contracts/go/clients/meshwantsclient/wants_test.go @@ -42,6 +42,7 @@ func TestWantsAreWhatTheEarlierCodecWrote(t *testing.T) { "put": {Want{Fp: a, Self: true, Put: true, Hold: true, Yes: d, Release: "r1"}, `{"v":1,"fp":"` + a + `","put":true,"hold":true,"yes":"` + d + `","release":"r1","self":true}`}, "removed": {Want{Removed: true, Words: "gone"}, `{"v":1,"removed":true,"words":"gone"}`}, "released": {Want{Fp: a, Composed: c, Version: "0.1.15", Parts: map[string]string{"mesh-sdk:go": d}}, `{"v":1,"fp":"` + a + `","composed":"` + c + `","version":"0.1.15","parts":{"mesh-sdk:go":"` + d + `"}}`}, + "kept": {Want{Fp: a, Composed: c, Parts: map[string]string{"x:image": d}, KeptOn: "n1"}, `{"v":1,"fp":"` + a + `","composed":"` + c + `","parts":{"x:image":"` + d + `"},"kept-on":"n1"}`}, } { t.Run(name, func(t *testing.T) { raw, err := EncodeWant(row.w) diff --git a/contracts/go/seats/meshwants/contract.go b/contracts/go/seats/meshwants/contract.go index 7e958b08..1030ce77 100644 --- a/contracts/go/seats/meshwants/contract.go +++ b/contracts/go/seats/meshwants/contract.go @@ -895,7 +895,9 @@ type WantsByWantNodeModuleValue struct { Composed string `json:"composed,omitempty"` Fp string `json:"fp,omitempty"` Hold bool `json:"hold,omitempty"` - Newest string `json:"newest,omitempty"` + // for a kept build, the build node that keeps it, from the builder's build.ended; the node-engine fetches the build through that node's seat verb kept-artifact, and a want with none from the store (design 47 G section 526) + KeptOn string `json:"kept-on,omitempty"` + Newest string `json:"newest,omitempty"` // the digest of each of the module's parts, by artifact reference; the node-engine fetches and checks each part by it (design 47 G section 379) Parts map[string]string `json:"parts,omitempty"` Put bool `json:"put,omitempty"` diff --git a/contracts/go/seats/meshwants/contract_test.go b/contracts/go/seats/meshwants/contract_test.go index b21c64c9..32f26b23 100644 --- a/contracts/go/seats/meshwants/contract_test.go +++ b/contracts/go/seats/meshwants/contract_test.go @@ -65,6 +65,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a want for a kept build, naming the node that keeps it", subject: "", given: "{\"fp\":\"ab12\",\"kept-on\":\"n1\",\"parts\":{\"x:image\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},\"v\":1,\"version\":\"0.1.15\"}", wire: "{\"fp\":\"ab12\",\"kept-on\":\"n1\",\"parts\":{\"x:image\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},\"v\":1,\"version\":\"0.1.15\"}", + decode: func(b []byte) (any, error) { + var v WantsByWantNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "a node's own want", subject: "", given: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", wire: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", decode: func(b []byte) (any, error) { var v WantsByWantNodeNodeValue diff --git a/contracts/seats/mesh-wants/fixtures/states.json b/contracts/seats/mesh-wants/fixtures/states.json index 818850d5..85e5d74c 100644 --- a/contracts/seats/mesh-wants/fixtures/states.json +++ b/contracts/seats/mesh-wants/fixtures/states.json @@ -8,6 +8,19 @@ "rule": 3 } }, + { + "name": "a want for a kept build, naming the node that keeps it", + "of": "state:wants/want..", + "given": { + "v": 1, + "fp": "ab12", + "version": "0.1.15", + "parts": { + "x:image": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + }, + "kept-on": "n1" + } + }, { "name": "a node's own want", "of": "state:wants/want..node", diff --git a/contracts/seats/mesh-wants/seat.json b/contracts/seats/mesh-wants/seat.json index ca64e17c..445971f7 100644 --- a/contracts/seats/mesh-wants/seat.json +++ b/contracts/seats/mesh-wants/seat.json @@ -542,6 +542,10 @@ "type": "string" }, "description": "the digest of each of the module's parts, by artifact reference; the node-engine fetches and checks each part by it (design 47 G section 379)" + }, + "kept-on": { + "type": "string", + "description": "for a kept build, the build node that keeps it, from the builder's build.ended; the node-engine fetches the build through that node's seat verb kept-artifact, and a want with none from the store (design 47 G section 526)" } } }, diff --git a/contracts/ts/seats/meshwants/index.d.ts b/contracts/ts/seats/meshwants/index.d.ts index 134842cd..81fbc682 100644 --- a/contracts/ts/seats/meshwants/index.d.ts +++ b/contracts/ts/seats/meshwants/index.d.ts @@ -513,6 +513,8 @@ export interface WantsByWantNodeModuleValue { "composed"?: string; "fp"?: string; "hold"?: boolean; + /** for a kept build, the build node that keeps it, from the builder's build.ended; the node-engine fetches the build through that node's seat verb kept-artifact, and a want with none from the store (design 47 G section 526) */ + "kept-on"?: string; "newest"?: string; /** the digest of each of the module's parts, by artifact reference; the node-engine fetches and checks each part by it (design 47 G section 379) */ "parts"?: Record; From 457b08ce15d458c606efaab5482d02ab79158287 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:26:52 +0100 Subject: [PATCH 2/4] Assigner: hears the builder's build log and writes a kept build's parts and keeping node into the want MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The want names parts (by :) and kept-on from the build's end, only for a node whose engine states want-parts. Builds: design 47 G §526, §379, §525, §671 --- .../cmd/mesh-delivery-assigner/buildlog.go | 84 +++++++++++++++++++ .../mesh-delivery-assigner/buildlog_test.go | 46 ++++++++++ .../cmd/mesh-delivery-assigner/main.go | 3 + .../cmd/mesh-delivery-assigner/main_test.go | 4 + .../cmd/mesh-delivery-assigner/pass.go | 1 + .../cmd/mesh-delivery-assigner/world.go | 6 +- .../internal/assign/wants.go | 30 ++++++- .../internal/assign/wants_test.go | 25 ++++++ .../mesh-delivery-assigner/manifest_test.go | 5 +- modules/mesh-delivery-assigner/module.json | 1 + 10 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go create mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go new file mode 100644 index 00000000..d5ece034 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "sync" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/process" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "mesh-delivery-assigner/internal/assign" +) + +// The builder's log, heard (design 47 G §379, §526, §671): each `build.ended` of a done build is kept by module and fingerprint, +// so a want names a kept build's parts by digest and the node that keeps it, taken from the build's end and never from the +// builder's done key (ADR 0596 §2). The log is read on the assigner's own credential as a read of `mesh-delivery-builder.build-log` +// (its manifest's `reads`), every entry from the start of the log and then each new one, and wakes the pass on each. + +// builtEnds are the done builds' ends heard, by module and fingerprint (putBuilt). +type builtEnds struct { + mu sync.Mutex + ends map[string]builds.BuildLogEntry +} + +func (b *builtEnds) key(module, fp string) string { return module + "@" + fp } + +// putBuilt takes one entry of the builder's log: a done build's end is kept, anything else is passed over. +func (b *builtEnds) putBuilt(value []byte) error { + if !builds.IsBuildEntry(value) { + return nil + } + e, err := builds.DecodeBuildLogEntry(value) + if err != nil { + return err + } + if e.Event != builds.BuildEndedEvent || e.Outcome != builds.BuildDone { + return nil + } + if e.Module == "" || e.FP == "" { + return fmt.Errorf("a build's end naming no module or fingerprint: %s", value) + } + b.mu.Lock() + defer b.mu.Unlock() + if b.ends == nil { + b.ends = map[string]builds.BuildLogEntry{} + } + if old, ok := b.ends[b.key(e.Module, e.FP)]; !ok || !e.At.Before(old.At) { + b.ends[b.key(e.Module, e.FP)] = e + } + return nil +} + +// keptBuild is the kept build of module at fp, as its end names it: each artifact's digest by `:`, and the node +// that keeps it. False for a build not done or not kept (it names no keeping node). +func (b *builtEnds) keptBuild(module, fp string) (assign.KeptBuild, bool) { + b.mu.Lock() + e, ok := b.ends[b.key(module, fp)] + b.mu.Unlock() + if !ok || e.KeptOn == "" { + return assign.KeptBuild{}, false + } + parts := map[string]string{} + for _, a := range e.Made { + parts[module+":"+a.Name] = a.Digest + } + return assign.KeptBuild{Parts: parts, KeptOn: e.KeptOn}, true +} + +// hearBuildLog reads the builder's log on conn and wakes the pass on each entry; the watch lasts as long as the connection. +func hearBuildLog(conn *process.Conn, ends *builtEnds, wake func(), logf func(string, ...any)) { + go func() { + err := conn.ReadLog(builds.BuilderModule, builds.BuilderLog).Watch("", stdio.LogFromAll, func(le stdio.LogEntry) error { + if err := ends.putBuilt(le.Value); err != nil { + logf("%s entry %d is not read: %v", builds.BuildLogRead, le.Sequence, err) + return nil + } + wake() + return nil + }) + if err != nil { + logf("%s is not read (%v): no want names a kept build's parts or its keeping node until the assigner starts again", builds.BuildLogRead, err) + } + }() +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go new file mode 100644 index 00000000..841aac5a --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "reflect" + "testing" + "time" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" +) + +// Design 47 G §526, §379, §671: a done build's end in the builder's log, kept by module and fingerprint, names a kept build's +// parts by digest (`:`) and its keeping node; a build not kept, failed or not ended names none. +func TestAKeptBuildsEndNamesItsPartsAndItsNode(t *testing.T) { + at := time.Date(2026, 11, 11, 22, 0, 0, 0, time.UTC) + entry := func(event, outcome, keptOn string, made ...builds.BuiltArtifact) []byte { + raw, err := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: event, Module: "node-engine", FP: "src2:a", Source: "s", Repository: "novox/mesh-host", + Directory: ".", Commit: "c1", Node: "ace", At: at, Outcome: outcome, Made: made, KeptOn: keptOn}) + if err != nil { + t.Fatal(err) + } + return raw + } + made := builds.BuiltArtifact{Name: "program", Kind: "program", Digest: "sha256:1", Reference: "artifact-store://node-engine/program@sha256:1"} + var b builtEnds + for _, raw := range [][]byte{entry(builds.BuildStartedEvent, "", ""), entry(builds.BuildEndedEvent, builds.BuildFailed, "ace")} { + if err := b.putBuilt(raw); err != nil { + t.Fatal(err) + } + } + if _, ok := b.keptBuild("node-engine", "src2:a"); ok { + t.Error("a start and a failed end name a kept build") + } + if err := b.putBuilt(entry(builds.BuildEndedEvent, builds.BuildDone, "", made)); err != nil { + t.Fatal(err) + } + if _, ok := b.keptBuild("node-engine", "src2:a"); ok { + t.Error("a build not kept names a keeping node") + } + if err := b.putBuilt(entry(builds.BuildEndedEvent, builds.BuildDone, "ace", made)); err != nil { + t.Fatal(err) + } + k, ok := b.keptBuild("node-engine", "src2:a") + if !ok || k.KeptOn != "ace" || !reflect.DeepEqual(k.Parts, map[string]string{"node-engine:program": "sha256:1"}) { + t.Errorf("kept build = %+v, %v", k, ok) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index ca6e6d0e..42fb2a7e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -578,6 +578,9 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, } hearReadsChanged(ctx, js, hub, reads, findings, gate.EventsLive, logger.Printf) hearRunning(ctx, js, w, wake, findings, logger.Printf) + if e, ok := bus.(emitting); ok && e.Conn != nil { + hearBuildLog(e.Conn, &w.ends, wake, logger.Printf) + } } if bus != nil && kvs[buckets.Puts] != nil { p.acts = newPutActs(bus, wake, logger.Printf) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go index 5563451a..10b98a5f 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/views" "mesh-delivery-assigner/internal/assign" plc "mesh-delivery-assigner/internal/assignment" @@ -54,6 +55,9 @@ func TestOpensWhatItsManifestNames(t *testing.T) { if slices.Contains(readFamilies, r) { continue // a state kept one bucket per node, read by its prefix (ADR 0593 §6), not one bucket } + if r == builds.BuildLogRead { + continue // the builder's log, read on the connection (buildlog.go), no bucket + } reads = append(reads, strings.Replace(r, ".", "_", 1)) } for _, l := range readFamilies { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 2e6026d2..8e546478 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -325,6 +325,7 @@ func (p *passer) wants(ctx context.Context) { held, _ := mwc.HeldKey(name, fp) grant, _ := grantsstate.PartGrantKey(name, fp) hold, _ := asks.HoldAskKey(name, fp) + m.KeptBuilds = func(fp string) (assign.KeptBuild, bool) { return p.w.ends.keptBuild(name, fp) } m.Released = p.answeredWith(hold, "release") m.Kept = p.answeredWith(hold, "keep") m.Built, m.Held, m.Granted = p.w.built(name, fp), p.w.has(buckets.Wants, held) && !m.Released, p.w.has(grants, grant) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 876e04e0..81856460 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -41,8 +41,8 @@ type world struct { changes map[string]uint64 // meta is, by bucket and key, the revision and the time of the value held now, where the watch said them (putEntry). meta map[string]entryMeta - // trunkBuilt is, by module, its newest done trunk build in the builder's log (putBuilt). - trunkBuilt map[string]builds.BuildLogEntry + // built are the done builds' ends the builder's log says, by module and fingerprint (buildlog.go). + ends builtEnds // assignments is the seat's state assignments assembled node by node from its keys in either form (mesh-sdk // mmac.Keys; research 072 row 37): the one reader of its keys in the assigner. assignmentKeys *mmac.Keys @@ -74,7 +74,7 @@ type parsed struct { func newWorld() *world { return &world{values: map[string]map[string][]byte{}, manifests: map[string]parsed{}, healthy: map[string][]string{}, changes: map[string]uint64{}, - meta: map[string]entryMeta{}, trunkBuilt: map[string]builds.BuildLogEntry{}, assignmentKeys: mmac.NewKeys(), + meta: map[string]entryMeta{}, assignmentKeys: mmac.NewKeys(), sourceGroups: map[uint64]string{}, running: map[string]nodeengine.Running{}, modules: map[string]map[string]meshcatalog.ModulesByModuleValue{}, graphs: map[string]map[string]graph.Node{}} } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 7d4774fe..0daa19d2 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -95,11 +95,39 @@ type Module struct { // source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while // the node was placed does not outlive the assignments (issue 1126). Nil: no node does. Self func(node string) bool + // KeptBuilds answers the kept build of the module at a fingerprint, as the builder's `build.ended` names it (design 47 G §526, + // §671): a want at one names its parts and its keeping node, for a node that states the capability WantParts. Nil: none is read. + KeptBuilds func(fp string) (KeptBuild, bool) // MachineHeld says whether the node's own network condition is open, which holds the machine as a whole: no want moves to it // until that clears (design 47 G §706 *M's own network*). Nil: none is held. MachineHeld func(node string) bool } +// WantParts is the capability a node-engine of G states (ADR 0614 §4, design 47 G §525): it reads a want's `parts` and `kept-on`. +const WantParts = "want-parts" + +// KeptBuild is a kept build as the builder's `build.ended` says it: the digest of each of its parts by `:`, and +// the build node that keeps it. +type KeptBuild struct { + Parts map[string]string + KeptOn string +} + +// withKept is w naming the kept build at its fingerprint, the parts by digest and the keeping node, only while the node's +// engine states WantParts (design 47 G §525, §526); a want of a build that is not kept, or of a node that does not state it, +// is w. +func (n Nodes) withKept(m Module, node string, w mwc.Want) mwc.Want { + if m.KeptBuilds == nil { + return w + } + k, ok := m.KeptBuilds(w.Fp) + if !ok || !slices.Contains(n.Reads[node], WantParts) { + return w + } + w.Parts, w.KeptOn = k.Parts, k.KeptOn + return w +} + func (m Module) selfOn(node string) bool { return m.Self != nil && m.Self(node) } func (m Module) composition(node, fp string) (ComposedDigest, bool) { @@ -514,7 +542,7 @@ func Wants(m Module, n Nodes, wants map[string]mwc.Want, lastHealthy map[string] switch { case ok: d, _ := m.composition(node, target) - next = mwc.Want{Fp: target, Composed: d.Digest} + next = n.withKept(m, node, mwc.Want{Fp: target, Composed: d.Digest}) at := m if target != m.FP { // the first want at the older fingerprint the node runs: held back from F by the rule, and judged as a want at it diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index e3d46708..59ebc1f0 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -1583,3 +1583,28 @@ func TestAProviderRunsHealthyByItsRunning(t *testing.T) { } } } + +// Design 47 G §525, §526: a want at a kept build names its parts and the node that keeps it, only for a node whose engine states +// the capability want-parts. +func TestAWantNamesAKeptBuildsPartsAndNodeForANodeThatReadsThem(t *testing.T) { + kept := KeptBuild{Parts: map[string]string{"mail:image": "sha256:1"}, KeptOn: "g14"} + for name, row := range map[string]struct { + reads []string + kept func(string) (KeptBuild, bool) + wanted bool + }{ + "states it": {[]string{WantParts}, func(fp string) (KeptBuild, bool) { return kept, fp == fNew }, true}, + "does not state": {[]string{"form:oci"}, func(fp string) (KeptBuild, bool) { return kept, fp == fNew }, false}, + "build not kept": {[]string{WantParts}, func(string) (KeptBuild, bool) { return KeptBuild{}, false }, false}, + "no kept builds": {[]string{WantParts}, nil, false}, + "another fingerp": {[]string{WantParts}, func(fp string) (KeptBuild, bool) { return kept, fp == fOld }, false}, + } { + m, n := ready() + n.Reads = map[string][]string{"ace": row.reads, "g14": row.reads} + m.KeptBuilds = row.kept + w := Wants(m, n, wantedOld(), nil).Wants["ace"] + if w.Fp != fNew || (w.KeptOn == "g14") != row.wanted || (len(w.Parts) == 1) != row.wanted { + t.Errorf("%s: ace's want = %+v", name, w) + } + } +} diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index d28cbc9c..6a6c64b3 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -43,9 +43,10 @@ func TestManifest(t *testing.T) { slices.Sort(reads) // ADR 0593 §8: it composes nothing, so it reads no candidate, held or resolved value, compose step or build log; it reads // every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1). + // Design 47 G §526, §379: it reads the builder's log for the `build.ended` that names a kept build's parts and keeping node. // DLV-F2r (ADR 0615 §1, ADR 0631 §1): the indexer's manifests, modules and module graph (whose verb edges a want waits // on, ADR 0614 §2, §3) are read through its first views and views family, none of the indexer's states by reads. - want := []string{"mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", + want := []string{"mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed", "mesh-provisioner.provisions", "node-engine.have"} if !reflect.DeepEqual(reads, want) { @@ -55,7 +56,7 @@ func TestManifest(t *testing.T) { named := []string{buckets.Builds, buckets.Merges, buckets.Source, buckets.Answers, buckets.Presence, buckets.Grants, buckets.ComposedRead, "mesh-provisioner_provisions", buckets.Have} slices.Sort(named) - for i, r := range want { + for i, r := range slices.DeleteFunc(slices.Clone(want), func(r string) bool { return r == "mesh-delivery-builder.build-log" }) { // a log, read on the connection if got := strings.Replace(named[i], "_", ".", 1); got != r { t.Errorf("the buckets package names %q, the manifest reads %q", got, r) } diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 869eb605..b9244115 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -30,6 +30,7 @@ } ], "reads": [ + "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", From d1357239e2d85609c05fe9593d5e7c506e37bfe9 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:30:47 +0100 Subject: [PATCH 3/4] Builder: says build.ended itself on its own stream mesh-delivery-builder-builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each done build's end is published as build.ended.. (escaped) with the module, fingerprint, made digests, kept-on and branch, where the log entry is written. Builds: design 47 G §379, A-96 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-delivery-builder/builds/ended.go | 43 +++++++++++++++++++ .../builds/ended_test.go | 32 ++++++++++++++ .../mesh-delivery-builder/integration_test.go | 16 +++++++ .../cmd/mesh-delivery-builder/main.go | 3 ++ .../cmd/mesh-delivery-builder/reasks.go | 28 ++++++++++++ .../mesh-delivery-builder/manifest_test.go | 7 +-- modules/mesh-delivery-builder/module.json | 1 + 7 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 modules/mesh-delivery-builder/builds/ended.go create mode 100644 modules/mesh-delivery-builder/builds/ended_test.go diff --git a/modules/mesh-delivery-builder/builds/ended.go b/modules/mesh-delivery-builder/builds/ended.go new file mode 100644 index 00000000..0f01a3d1 --- /dev/null +++ b/modules/mesh-delivery-builder/builds/ended.go @@ -0,0 +1,43 @@ +package builds + +import ( + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The builder says each done build's end itself, as an event on its own stream (design 47 G §379, A-96): the stream +// BuildsStream carries `build.ended..` (each part escaped), published where the log entry of the end is +// written. A part that wants a build's parts or its keeping node hears it (the assigner consumes `mesh-delivery-builder.build.ended`). +const ( + // BuildsStream is the builder's stream of build ends, named by its manifest's `streams` entry. + BuildsStream = "mesh-delivery-builder-builds" + // BuildsStreamEvents is the pattern of the events that entry carries. + BuildsStreamEvents = BuildEndedEvent + ".**" +) + +// BuildEnded is the body of the event build.ended: the module, the fingerprint, each artifact made with its digest, the build +// node that keeps a kept build (none for one not kept) and the watched branch a build of a candidate is for (none for the trunk). +type BuildEnded struct { + Module string `json:"module"` + Fingerprint string `json:"fingerprint"` + Made map[string]string `json:"made,omitempty"` + KeptOn string `json:"kept-on,omitempty"` + Branch string `json:"branch,omitempty"` +} + +// BuildEndedEventName is the event's name for a module and fingerprint: `build.ended..`, each escaped. +func BuildEndedEventName(module, fp string) string { + return BuildEndedEvent + "." + keys.EscapeName(module) + "." + keys.EscapeName(fp) +} + +// BuildEndedOf is the event of a log entry of a build's end: false for any entry but the end of a done build, which says no +// digest of any part. +func BuildEndedOf(e BuildLogEntry) (BuildEnded, bool) { + if e.Event != BuildEndedEvent || e.Outcome != BuildDone || e.Module == "" || e.FP == "" { + return BuildEnded{}, false + } + made := map[string]string{} + for _, a := range e.Made { + made[a.Name] = a.Digest + } + return BuildEnded{Module: e.Module, Fingerprint: e.FP, Made: made, KeptOn: e.KeptOn, Branch: e.Ref}, true +} diff --git a/modules/mesh-delivery-builder/builds/ended_test.go b/modules/mesh-delivery-builder/builds/ended_test.go new file mode 100644 index 00000000..33e3f70f --- /dev/null +++ b/modules/mesh-delivery-builder/builds/ended_test.go @@ -0,0 +1,32 @@ +package builds + +import ( + "reflect" + "testing" +) + +// Design 47 G §379, A-96: a done build's end is the event build.ended, its subject escaped by module and fingerprint; any other +// entry is none. +func TestADoneBuildsEndIsTheEventBuildEnded(t *testing.T) { + made := []BuiltArtifact{{Name: "program", Kind: "program", Digest: "sha256:1"}} + e := BuildLogEntry{Event: BuildEndedEvent, Outcome: BuildDone, Module: "node-engine", FP: "src2:a", Ref: "cand", KeptOn: "ace", Made: made} + got, ok := BuildEndedOf(e) + want := BuildEnded{Module: "node-engine", Fingerprint: "src2:a", Made: map[string]string{"program": "sha256:1"}, KeptOn: "ace", Branch: "cand"} + if !ok || !reflect.DeepEqual(got, want) { + t.Errorf("event = %+v, %v", got, ok) + } + if n := BuildEndedEventName("node-engine", "src2:a"); n != "build.ended.node-engine.src2=3aa" { + t.Errorf("event name = %q", n) + } + for name, mut := range map[string]func(*BuildLogEntry){ + "started": func(e *BuildLogEntry) { e.Event = BuildStartedEvent }, + "failed": func(e *BuildLogEntry) { e.Outcome = BuildFailed }, + "no fp": func(e *BuildLogEntry) { e.FP = "" }, + } { + c := e + mut(&c) + if _, ok := BuildEndedOf(c); ok { + t.Errorf("%s: an event", name) + } + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index 7feaa294..aba095dd 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -314,6 +314,10 @@ func startBedWith(t *testing.T, closed bool) *bed { "mesh.stream.mesh-delivery-builder.event.finding-cleared.*", "mesh.stream.mesh-delivery-builder.event.findings-stated.*"}}); err != nil { t.Fatal(err) } + // The builder's stream of build ends (design 47 G A-96), which it says build.ended into. + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "BUILDER_BUILDS", Subjects: []string{"mesh.stream.mesh-delivery-builder.event.build.ended.>"}}); err != nil { + t.Fatal(err) + } b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}} b.fake.closed.Store(closed) b.fake.serve(t, "mesh-build-go", "n1", "n2") @@ -555,6 +559,18 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 ended[1].Manifest != digest || len(ended[1].Made) != 2 || ended[1].Node != "n1" { t.Errorf("logged = %+v, want the start and the end of netcheck's build", ended) } + // Design 47 G §379, A-96: the end is also said as the event build.ended on the builder's stream, its body the module, the + // fingerprint and each artifact's digest. + if st, err := b.js.Stream(b.ctx, "BUILDER_BUILDS"); err != nil { + t.Fatal(err) + } else if raw, err := st.GetLastMsgForSubject(b.ctx, "mesh.stream.mesh-delivery-builder.event."+builds.BuildEndedEventName("netcheck", fp)); err != nil { + t.Errorf("no build.ended of netcheck's build: %v", err) + } else { + var ev builds.BuildEnded + if err := json.Unmarshal(raw.Data, &ev); err != nil || ev.Module != "netcheck" || ev.Fingerprint != fp || len(ev.Made) != 2 { + t.Errorf("build.ended = %s (%v), want netcheck at its fingerprint with two artifacts' digests", raw.Data, err) + } + } for tool, want := range map[string]int{"mesh-build-go.build_go_fetch": 1, "mesh-build-go.build_go_build": 1, "mesh-build-node.build_node_fetch": 1, "mesh-build-node.build_node_build": 1} { if got := b.fake.count(tool); got != want { t.Errorf("%s called %d times, want %d (on one node, ADR 0478 clause 4)", tool, got, want) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index b873b4c2..d8678236 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -253,6 +253,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := appendLog(context.WithoutCancel(ctx), js, cred, e); err != nil { logger.Printf("%v", err) } + if err := emitBuildEnded(context.WithoutCancel(ctx), js, cred, e); err != nil { + logger.Printf("%v", err) + } // A failed trunk build is the builder's finding build-failed, cleared by a later done one (DLV-B7). found.buildEnded(context.WithoutCancel(ctx), e) } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go index 26da5ebb..2082cf87 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go @@ -161,6 +161,34 @@ func appendLog(ctx context.Context, js jetstream.JetStream, cred process.Account return nil } +// emitBuildEnded says the builder's event build.ended of a done build, on the builder's own stream (design 47 G §379, A-96): +// mesh.stream..event.build.ended.., published where the log entry of the end is written. An +// entry that is not a done build's end says none. The id makes a repeated end one event. +func emitBuildEnded(ctx context.Context, js jetstream.JetStream, cred process.Account, e builds.BuildLogEntry) error { + body, ok := builds.BuildEndedOf(e) + if !ok { + return nil + } + data, err := json.Marshal(body) + if err != nil { + return err + } + msg := nats.NewMsg(cred.StreamSubject(builds.BuildEndedEventName(e.Module, e.FP))) + id := "build-ended." + e.Module + "." + e.FP + "." + e.At.Format(time.RFC3339Nano) + msg.Header.Set("x-event-id", id) + msg.Header.Set("x-source", cred.Module) + msg.Header.Set("x-node", cred.Node) + msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano)) + msg.Header.Set("content-type", "application/json") + msg.Data = data + pub, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + if _, err := js.PublishMsg(pub, msg, jetstream.WithMsgID(id)); err != nil { + return fmt.Errorf("saying %s of %s at %s: %w", builds.BuildEndedEvent, e.Module, e.FP, err) + } + return nil +} + // versionActed answers a reasks value of builder_publish_again, builder_floor_again or builder_old_form's drop, a hand act said in the log. func versionActed(ctx context.Context, acts versions.Acts, kv jetstream.KeyValue, e jetstream.KeyValueEntry, r build.ReAsk, logf func(string, ...any)) { var err error diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 79789582..3a9a8c5b 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -175,9 +175,10 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 } // Design 47 G §672: the toolchain waits' stream, kept 7 days within 16 MiB, and the bound's setting, 60 minutes. // Design 47 G A-30, §383: and its own findings' events, which its finding writer says. - if len(m.Streams) != 2 || m.Streams[0].Name != "waits" || !reflect.DeepEqual(m.Streams[0].Events, []string{"wait-held.**", "wait-ended.**"}) || m.Streams[0].MaxAgeDays != 7 || m.Streams[0].MaxMiB != 16 || - m.Streams[1].Name != "mesh-delivery-builder-findings" || !reflect.DeepEqual(m.Streams[1].Events, []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}) { - t.Errorf("streams = %+v, want waits with wait-held.** and wait-ended.**, 7 days, 16 MiB, and the findings' stream", m.Streams) + if len(m.Streams) != 3 || m.Streams[0].Name != "waits" || !reflect.DeepEqual(m.Streams[0].Events, []string{"wait-held.**", "wait-ended.**"}) || m.Streams[0].MaxAgeDays != 7 || m.Streams[0].MaxMiB != 16 || + m.Streams[1].Name != builds.BuildsStream || !reflect.DeepEqual(m.Streams[1].Events, []string{builds.BuildsStreamEvents}) || m.Streams[1].MaxAgeDays != 7 || + m.Streams[2].Name != "mesh-delivery-builder-findings" || !reflect.DeepEqual(m.Streams[2].Events, []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}) { + t.Errorf("streams = %+v, want waits with wait-held.** and wait-ended.**, 7 days, 16 MiB, the builds' stream (build.ended.**, 7 days) and the findings' stream", m.Streams) } if c, ok := m.Config["toolchain-wait-minutes"]; !ok || c.Default != float64(60) || len(m.Config) != 1 { t.Errorf("config = %+v, want toolchain-wait-minutes of 60", m.Config) diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index ac053db9..f784dcb2 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -118,6 +118,7 @@ ], "streams": [ { "name": "waits", "events": ["wait-held.**", "wait-ended.**"], "max-age-days": 7, "max-mib": 16 }, + { "name": "mesh-delivery-builder-builds", "events": ["build.ended.**"], "max-age-days": 7, "max-mib": 16 }, { "name": "mesh-delivery-builder-findings", "events": ["finding-written.*", "finding-cleared.*", "findings-stated.*"], "max-age-days": 7, "max-mib": 16 } ], "config": { From 6c852a8eea4e8636678fdb5e39733b7d8cc16ea1 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:32:50 +0100 Subject: [PATCH 4/4] Assigner: hears build.ended and version-released, reads no build log; a want's version and parts come from them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A kept build's or another branch's build names its parts and keeping node from build.ended; the trunk's entry names its version and parts from the version-released whose source is the kept assignment graph revision's. The parts' contract comment says :. Builds: design 47 G §379, §525, §526, A-94, A-96, A-67 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- contracts/go/seats/meshwants/contract.go | 4 +- contracts/seats/mesh-wants/seat.json | 4 +- contracts/ts/seats/meshwants/index.d.ts | 4 +- .../cmd/mesh-delivery-assigner/buildended.go | 124 ++++++++++++++++++ .../mesh-delivery-assigner/buildended_test.go | 88 +++++++++++++ .../cmd/mesh-delivery-assigner/buildlog.go | 84 ------------ .../mesh-delivery-assigner/buildlog_test.go | 46 ------- .../cmd/mesh-delivery-assigner/graphkept.go | 16 +++ .../cmd/mesh-delivery-assigner/main.go | 6 +- .../cmd/mesh-delivery-assigner/main_test.go | 4 - .../cmd/mesh-delivery-assigner/pass.go | 2 +- .../cmd/mesh-delivery-assigner/world.go | 4 +- .../internal/assign/wants.go | 16 ++- .../internal/assign/wants_test.go | 4 +- .../mesh-delivery-assigner/manifest_test.go | 8 +- modules/mesh-delivery-assigner/module.json | 3 +- 16 files changed, 258 insertions(+), 159 deletions(-) create mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go create mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go delete mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go delete mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go diff --git a/contracts/go/seats/meshwants/contract.go b/contracts/go/seats/meshwants/contract.go index 1030ce77..23b263fc 100644 --- a/contracts/go/seats/meshwants/contract.go +++ b/contracts/go/seats/meshwants/contract.go @@ -865,7 +865,7 @@ type VersionDeliveredEvent struct { Away []string `json:"away,omitempty"` // the module Module string `json:"module"` - // the digest of each of its parts, by artifact reference + // the digest of each of its parts, by `:` Parts map[string]string `json:"parts"` V int64 `json:"v"` // MAJOR.MINOR.PATCH @@ -898,7 +898,7 @@ type WantsByWantNodeModuleValue struct { // for a kept build, the build node that keeps it, from the builder's build.ended; the node-engine fetches the build through that node's seat verb kept-artifact, and a want with none from the store (design 47 G section 526) KeptOn string `json:"kept-on,omitempty"` Newest string `json:"newest,omitempty"` - // the digest of each of the module's parts, by artifact reference; the node-engine fetches and checks each part by it (design 47 G section 379) + // the digest of each of the module's parts, by `:`; the node-engine fetches and checks each part by it (design 47 G section 379) Parts map[string]string `json:"parts,omitempty"` Put bool `json:"put,omitempty"` Release string `json:"release,omitempty"` diff --git a/contracts/seats/mesh-wants/seat.json b/contracts/seats/mesh-wants/seat.json index 445971f7..f149808c 100644 --- a/contracts/seats/mesh-wants/seat.json +++ b/contracts/seats/mesh-wants/seat.json @@ -414,7 +414,7 @@ "additionalProperties": { "type": "string" }, - "description": "the digest of each of its parts, by artifact reference" + "description": "the digest of each of its parts, by `:`" }, "away": { "type": "array", @@ -541,7 +541,7 @@ "additionalProperties": { "type": "string" }, - "description": "the digest of each of the module's parts, by artifact reference; the node-engine fetches and checks each part by it (design 47 G section 379)" + "description": "the digest of each of the module's parts, by `:`; the node-engine fetches and checks each part by it (design 47 G section 379)" }, "kept-on": { "type": "string", diff --git a/contracts/ts/seats/meshwants/index.d.ts b/contracts/ts/seats/meshwants/index.d.ts index 81fbc682..ee5a58f3 100644 --- a/contracts/ts/seats/meshwants/index.d.ts +++ b/contracts/ts/seats/meshwants/index.d.ts @@ -491,7 +491,7 @@ export interface VersionDeliveredEvent { "away"?: string[]; /** the module */ "module": string; - /** the digest of each of its parts, by artifact reference */ + /** the digest of each of its parts, by `:` */ "parts": Record; "v": number; /** MAJOR.MINOR.PATCH */ @@ -516,7 +516,7 @@ export interface WantsByWantNodeModuleValue { /** for a kept build, the build node that keeps it, from the builder's build.ended; the node-engine fetches the build through that node's seat verb kept-artifact, and a want with none from the store (design 47 G section 526) */ "kept-on"?: string; "newest"?: string; - /** the digest of each of the module's parts, by artifact reference; the node-engine fetches and checks each part by it (design 47 G section 379) */ + /** the digest of each of the module's parts, by `:`; the node-engine fetches and checks each part by it (design 47 G section 379) */ "parts"?: Record; "put"?: boolean; "release"?: string; diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go new file mode 100644 index 00000000..0dcbb895 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "fmt" + "sync" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/process" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "mesh-delivery-assigner/internal/assign" +) + +// What a want names of a build is heard, never read from the builder's state (design 47 G §379, §526, A-94, A-96; ADR 0596 §2): the +// builder's event `build.ended` (a done build's parts by digest, its keeping node and its branch) and the catalog's +// `version-released` (a released version and each part's digest, with the source revision it was built from), each kept as heard. + +// buildEndedSubjects is the builder's event build.ended on its own stream, as the manifest's consumes names it +// (`mesh-delivery-builder.build.ended`), each module and fingerprint after it. +var buildEndedSubjects = process.ModuleStreamSubject(builds.BuilderModule, builds.BuildEndedEvent+".>") + +// builtEnds are the done builds' ends heard, by module and fingerprint (hearBuildEnded). +type builtEnds struct { + mu sync.Mutex + ends map[string]builds.BuildEnded +} + +func (b *builtEnds) key(module, fp string) string { return module + "@" + fp } + +// put takes one build.ended in. +func (b *builtEnds) put(data []byte) error { + var e builds.BuildEnded + if err := json.Unmarshal(data, &e); err != nil { + return fmt.Errorf("a build.ended that is no end: %w", err) + } + if e.Module == "" || e.Fingerprint == "" { + return fmt.Errorf("a build's end naming no module or fingerprint: %s", data) + } + b.mu.Lock() + defer b.mu.Unlock() + if b.ends == nil { + b.ends = map[string]builds.BuildEnded{} + } + b.ends[b.key(e.Module, e.Fingerprint)] = e + return nil +} + +// partsOf is the parts of module's build at fp as its end names them (each artifact's digest by `:`), with the +// node that keeps it, for a kept build or a build of another watched branch; false for a build of the trunk not kept, whose +// parts the version's release says (releasedVersions). +func (b *builtEnds) partsOf(module, fp string) (assign.KeptBuild, bool) { + b.mu.Lock() + e, ok := b.ends[b.key(module, fp)] + b.mu.Unlock() + if !ok || (e.KeptOn == "" && e.Branch == "") { + return assign.KeptBuild{}, false + } + parts := map[string]string{} + for name, digest := range e.Made { + parts[module+":"+name] = digest + } + return assign.KeptBuild{Parts: parts, KeptOn: e.KeptOn}, true +} + +// releasedVersions are the catalog's version-released heard, by module and source revision. +type releasedVersions struct { + mu sync.Mutex + seen map[string]assign.KeptBuild +} + +func (r *releasedVersions) key(module string, source int64) string { + return fmt.Sprintf("%s@%d", module, source) +} + +// put takes one version-released in. +func (r *releasedVersions) put(data []byte) error { + var e meshcatalog.VersionReleasedEvent + if err := json.Unmarshal(data, &e); err != nil { + return fmt.Errorf("a version-released that is none: %w", err) + } + if e.Module == "" || e.Version == "" { + return fmt.Errorf("a version-released naming no module or version: %s", data) + } + parts := map[string]string{} + for _, a := range e.Artifacts { + parts[e.Module+":"+a.Artifact] = a.Digest + } + r.mu.Lock() + defer r.mu.Unlock() + if r.seen == nil { + r.seen = map[string]assign.KeptBuild{} + } + r.seen[r.key(e.Module, e.Source)] = assign.KeptBuild{Version: e.Version, Parts: parts} + return nil +} + +// of is the version of module released from the source revision, with its parts' digests; false before it is heard. +func (r *releasedVersions) of(module string, source int64) (assign.KeptBuild, bool) { + r.mu.Lock() + defer r.mu.Unlock() + k, ok := r.seen[r.key(module, source)] + return k, ok +} + +// hearBuildsAndReleases registers the hub's takers of build.ended and version-released, each waking the pass; before the hub starts. +func hearBuildsAndReleases(hub *process.EventHub, ends *builtEnds, released *releasedVersions, wake func(), logf func(string, ...any)) error { + taker := func(put func([]byte) error) func(jetstream.Msg) { + return func(m jetstream.Msg) { + if err := put(m.Data()); err != nil { + logf("%s is not kept: %v", m.Subject(), err) + } + if err := m.Ack(); err != nil { + logf("acknowledging %s: %v", m.Subject(), err) + } + wake() + } + } + if err := hub.Handle(buildEndedSubjects, taker(ends.put)); err != nil { + return err + } + return hub.Handle(meshcatalog.SubjectVersionReleased, taker(released.put)) +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go new file mode 100644 index 00000000..ffe0c50f --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "encoding/json" + "reflect" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "mesh-delivery-assigner/internal/assign" +) + +// Design 47 G §379, §526, A-96: a build.ended heard names a kept build's or another branch's parts by digest (`:`) and its +// keeping node; a build of the trunk not kept names none, its parts being the release's. +func TestABuildEndedHeardNamesAKeptBuildsPartsAndItsNode(t *testing.T) { + put := func(b *builtEnds, e builds.BuildEnded) { + raw, _ := json.Marshal(e) + if err := b.put(raw); err != nil { + t.Fatal(err) + } + } + made := map[string]string{"program": "sha256:1"} + var b builtEnds + put(&b, builds.BuildEnded{Module: "node-engine", Fingerprint: "a", Made: made}) + if _, ok := b.partsOf("node-engine", "a"); ok { + t.Error("a trunk build not kept names parts") + } + put(&b, builds.BuildEnded{Module: "node-engine", Fingerprint: "b", Made: made, KeptOn: "ace"}) + if k, ok := b.partsOf("node-engine", "b"); !ok || k.KeptOn != "ace" || !reflect.DeepEqual(k.Parts, map[string]string{"node-engine:program": "sha256:1"}) { + t.Errorf("kept build = %+v, %v", k, ok) + } + put(&b, builds.BuildEnded{Module: "node-engine", Fingerprint: "c", Made: made, Branch: "cand"}) + if k, ok := b.partsOf("node-engine", "c"); !ok || k.KeptOn != "" || len(k.Parts) != 1 { + t.Errorf("branch build = %+v, %v", k, ok) + } + if err := b.put([]byte(`{"module":"x"}`)); err == nil { + t.Error("an end naming no fingerprint is kept") + } +} + +// Design 47 G §379, A-96: a version-released heard names the version and the parts' digests of the module's source revision. +func TestAVersionReleasedHeardNamesItsVersionAndPartsBySource(t *testing.T) { + var r releasedVersions + raw, _ := json.Marshal(meshcatalog.VersionReleasedEvent{Module: "mail", Source: 7, Version: "1.2.3", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "image", Digest: "sha256:9"}}}) + if err := r.put(raw); err != nil { + t.Fatal(err) + } + want := assign.KeptBuild{Version: "1.2.3", Parts: map[string]string{"mail:image": "sha256:9"}} + if k, ok := r.of("mail", 7); !ok || !reflect.DeepEqual(k, want) { + t.Errorf("released = %+v, %v", k, ok) + } + if _, ok := r.of("mail", 8); ok { + t.Error("another source revision names a version") + } +} + +// Design 47 G §379, A-96: the trunk's entry takes its version and parts from the version-released of the kept assignment graph +// revision's source; a kept build from its end; no revision kept, or another fingerprint, names none. +func TestATrunkEntryTakesItsVersionFromTheReleaseOfTheKeptRevisionsSource(t *testing.T) { + w := newWorld() + raw, _ := json.Marshal(meshcatalog.VersionReleasedEvent{Module: "mail", Source: 7, Version: "1.2.3", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "image", Digest: "sha256:9"}}}) + if err := w.released.put(raw); err != nil { + t.Fatal(err) + } + endRaw, _ := json.Marshal(builds.BuildEnded{Module: "mail", Fingerprint: "k", Made: map[string]string{"image": "sha256:2"}, KeptOn: "g14"}) + if err := w.ends.put(endRaw); err != nil { + t.Fatal(err) + } + p := &passer{w: w, graph: &graphKeeper{ok: true, source: 7}} + named := p.namedBuild("mail", "t") + if k, ok := named("t"); !ok || k.Version != "1.2.3" || k.Parts["mail:image"] != "sha256:9" || k.KeptOn != "" { + t.Errorf("trunk entry = %+v, %v", k, ok) + } + if k, ok := named("k"); !ok || k.KeptOn != "g14" || k.Version != "" { + t.Errorf("kept build = %+v, %v", k, ok) + } + if _, ok := named("other"); ok { + t.Error("another fingerprint names a release") + } + p.graph.source = 8 + if _, ok := named("t"); ok { + t.Error("a source with no release names a version") + } + p.graph.ok = false + if _, ok := p.namedBuild("mail", "t")("t"); ok { + t.Error("no revision kept names a release") + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go deleted file mode 100644 index d5ece034..00000000 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "fmt" - "sync" - - stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/process" - - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" - "mesh-delivery-assigner/internal/assign" -) - -// The builder's log, heard (design 47 G §379, §526, §671): each `build.ended` of a done build is kept by module and fingerprint, -// so a want names a kept build's parts by digest and the node that keeps it, taken from the build's end and never from the -// builder's done key (ADR 0596 §2). The log is read on the assigner's own credential as a read of `mesh-delivery-builder.build-log` -// (its manifest's `reads`), every entry from the start of the log and then each new one, and wakes the pass on each. - -// builtEnds are the done builds' ends heard, by module and fingerprint (putBuilt). -type builtEnds struct { - mu sync.Mutex - ends map[string]builds.BuildLogEntry -} - -func (b *builtEnds) key(module, fp string) string { return module + "@" + fp } - -// putBuilt takes one entry of the builder's log: a done build's end is kept, anything else is passed over. -func (b *builtEnds) putBuilt(value []byte) error { - if !builds.IsBuildEntry(value) { - return nil - } - e, err := builds.DecodeBuildLogEntry(value) - if err != nil { - return err - } - if e.Event != builds.BuildEndedEvent || e.Outcome != builds.BuildDone { - return nil - } - if e.Module == "" || e.FP == "" { - return fmt.Errorf("a build's end naming no module or fingerprint: %s", value) - } - b.mu.Lock() - defer b.mu.Unlock() - if b.ends == nil { - b.ends = map[string]builds.BuildLogEntry{} - } - if old, ok := b.ends[b.key(e.Module, e.FP)]; !ok || !e.At.Before(old.At) { - b.ends[b.key(e.Module, e.FP)] = e - } - return nil -} - -// keptBuild is the kept build of module at fp, as its end names it: each artifact's digest by `:`, and the node -// that keeps it. False for a build not done or not kept (it names no keeping node). -func (b *builtEnds) keptBuild(module, fp string) (assign.KeptBuild, bool) { - b.mu.Lock() - e, ok := b.ends[b.key(module, fp)] - b.mu.Unlock() - if !ok || e.KeptOn == "" { - return assign.KeptBuild{}, false - } - parts := map[string]string{} - for _, a := range e.Made { - parts[module+":"+a.Name] = a.Digest - } - return assign.KeptBuild{Parts: parts, KeptOn: e.KeptOn}, true -} - -// hearBuildLog reads the builder's log on conn and wakes the pass on each entry; the watch lasts as long as the connection. -func hearBuildLog(conn *process.Conn, ends *builtEnds, wake func(), logf func(string, ...any)) { - go func() { - err := conn.ReadLog(builds.BuilderModule, builds.BuilderLog).Watch("", stdio.LogFromAll, func(le stdio.LogEntry) error { - if err := ends.putBuilt(le.Value); err != nil { - logf("%s entry %d is not read: %v", builds.BuildLogRead, le.Sequence, err) - return nil - } - wake() - return nil - }) - if err != nil { - logf("%s is not read (%v): no want names a kept build's parts or its keeping node until the assigner starts again", builds.BuildLogRead, err) - } - }() -} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go deleted file mode 100644 index 841aac5a..00000000 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildlog_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "reflect" - "testing" - "time" - - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" -) - -// Design 47 G §526, §379, §671: a done build's end in the builder's log, kept by module and fingerprint, names a kept build's -// parts by digest (`:`) and its keeping node; a build not kept, failed or not ended names none. -func TestAKeptBuildsEndNamesItsPartsAndItsNode(t *testing.T) { - at := time.Date(2026, 11, 11, 22, 0, 0, 0, time.UTC) - entry := func(event, outcome, keptOn string, made ...builds.BuiltArtifact) []byte { - raw, err := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: event, Module: "node-engine", FP: "src2:a", Source: "s", Repository: "novox/mesh-host", - Directory: ".", Commit: "c1", Node: "ace", At: at, Outcome: outcome, Made: made, KeptOn: keptOn}) - if err != nil { - t.Fatal(err) - } - return raw - } - made := builds.BuiltArtifact{Name: "program", Kind: "program", Digest: "sha256:1", Reference: "artifact-store://node-engine/program@sha256:1"} - var b builtEnds - for _, raw := range [][]byte{entry(builds.BuildStartedEvent, "", ""), entry(builds.BuildEndedEvent, builds.BuildFailed, "ace")} { - if err := b.putBuilt(raw); err != nil { - t.Fatal(err) - } - } - if _, ok := b.keptBuild("node-engine", "src2:a"); ok { - t.Error("a start and a failed end name a kept build") - } - if err := b.putBuilt(entry(builds.BuildEndedEvent, builds.BuildDone, "", made)); err != nil { - t.Fatal(err) - } - if _, ok := b.keptBuild("node-engine", "src2:a"); ok { - t.Error("a build not kept names a keeping node") - } - if err := b.putBuilt(entry(builds.BuildEndedEvent, builds.BuildDone, "ace", made)); err != nil { - t.Fatal(err) - } - k, ok := b.keptBuild("node-engine", "src2:a") - if !ok || k.KeptOn != "ace" || !reflect.DeepEqual(k.Parts, map[string]string{"node-engine:program": "sha256:1"}) { - t.Errorf("kept build = %+v, %v", k, ok) - } -} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go index 941d3bc2..9ea4bbec 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go @@ -14,6 +14,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" "git.novox.be/novox/mesh-sdk/go/keys" + "mesh-delivery-assigner/internal/assign" "mesh-delivery-assigner/internal/assigngraph" ) @@ -118,3 +119,18 @@ func (p *passer) sayWanted(node, module, fp string, removed bool) { p.logf("wanted of %s on %s is not said: %v", module, node, err) } } + +// namedBuild answers what a want of module at fp names of its build (design 47 G §379, §526, A-96): a kept build's or another watched +// branch's parts and keeping node from the builder's `build.ended`; for the trunk's entry (fp is the module's, trunkFP), the released +// version and the parts of the `version-released` whose source is the one the kept assignment graph revision was computed from (A-67). +func (p *passer) namedBuild(module, trunkFP string) func(fp string) (assign.KeptBuild, bool) { + return func(fp string) (assign.KeptBuild, bool) { + if k, ok := p.w.ends.partsOf(module, fp); ok { + return k, true + } + if fp != trunkFP || p.graph == nil || !p.graph.ok { + return assign.KeptBuild{}, false + } + return p.w.released.of(module, p.graph.source) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index 42fb2a7e..fc0366c0 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -576,11 +576,11 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, logger.Printf("a pipeline's end or a registration is not heard, so the wants keep the trunk-only form until the assigner starts again: %v", err) } } + if err := hearBuildsAndReleases(hub, &w.ends, &w.released, wake, logger.Printf); err != nil { + logger.Printf("a build's end or a version's release is not heard, so no want names a build's parts, its keeping node or its version until the assigner starts again: %v", err) + } hearReadsChanged(ctx, js, hub, reads, findings, gate.EventsLive, logger.Printf) hearRunning(ctx, js, w, wake, findings, logger.Printf) - if e, ok := bus.(emitting); ok && e.Conn != nil { - hearBuildLog(e.Conn, &w.ends, wake, logger.Printf) - } } if bus != nil && kvs[buckets.Puts] != nil { p.acts = newPutActs(bus, wake, logger.Printf) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go index 10b98a5f..5563451a 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/views" "mesh-delivery-assigner/internal/assign" plc "mesh-delivery-assigner/internal/assignment" @@ -55,9 +54,6 @@ func TestOpensWhatItsManifestNames(t *testing.T) { if slices.Contains(readFamilies, r) { continue // a state kept one bucket per node, read by its prefix (ADR 0593 §6), not one bucket } - if r == builds.BuildLogRead { - continue // the builder's log, read on the connection (buildlog.go), no bucket - } reads = append(reads, strings.Replace(r, ".", "_", 1)) } for _, l := range readFamilies { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 8e546478..49a73c03 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -325,7 +325,7 @@ func (p *passer) wants(ctx context.Context) { held, _ := mwc.HeldKey(name, fp) grant, _ := grantsstate.PartGrantKey(name, fp) hold, _ := asks.HoldAskKey(name, fp) - m.KeptBuilds = func(fp string) (assign.KeptBuild, bool) { return p.w.ends.keptBuild(name, fp) } + m.KeptBuilds = p.namedBuild(name, m.FP) m.Released = p.answeredWith(hold, "release") m.Kept = p.answeredWith(hold, "keep") m.Built, m.Held, m.Granted = p.w.built(name, fp), p.w.has(buckets.Wants, held) && !m.Released, p.w.has(grants, grant) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 81856460..bb0c9fdb 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -41,8 +41,10 @@ type world struct { changes map[string]uint64 // meta is, by bucket and key, the revision and the time of the value held now, where the watch said them (putEntry). meta map[string]entryMeta - // built are the done builds' ends the builder's log says, by module and fingerprint (buildlog.go). + // ends are the done builds' ends the builder says (build.ended), by module and fingerprint (buildlog.go). ends builtEnds + // released are the catalog's version-released heard, by module and source revision (buildended.go). + released releasedVersions // assignments is the seat's state assignments assembled node by node from its keys in either form (mesh-sdk // mmac.Keys; research 072 row 37): the one reader of its keys in the assigner. assignmentKeys *mmac.Keys diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 0daa19d2..1d278f17 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -95,8 +95,8 @@ type Module struct { // source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while // the node was placed does not outlive the assignments (issue 1126). Nil: no node does. Self func(node string) bool - // KeptBuilds answers the kept build of the module at a fingerprint, as the builder's `build.ended` names it (design 47 G §526, - // §671): a want at one names its parts and its keeping node, for a node that states the capability WantParts. Nil: none is read. + // KeptBuilds answers what a want at a fingerprint names of its build: its parts and its keeping node as the builder's `build.ended` + // names them, or the released version and parts of the catalog's `version-released` (design 47 G §379, §526, A-96), for a node that states the capability WantParts. Nil: none is read. KeptBuilds func(fp string) (KeptBuild, bool) // MachineHeld says whether the node's own network condition is open, which holds the machine as a whole: no want moves to it // until that clears (design 47 G §706 *M's own network*). Nil: none is held. @@ -106,11 +106,13 @@ type Module struct { // WantParts is the capability a node-engine of G states (ADR 0614 §4, design 47 G §525): it reads a want's `parts` and `kept-on`. const WantParts = "want-parts" -// KeptBuild is a kept build as the builder's `build.ended` says it: the digest of each of its parts by `:`, and -// the build node that keeps it. +// KeptBuild is what a want names of a build: the digest of each of its parts by `:`, and, as the builder's +// `build.ended` says it, the build node that keeps a kept build; for a trunk build the released version the catalog's +// `version-released` says (design 47 G §379, §526, A-96). type KeptBuild struct { - Parts map[string]string - KeptOn string + Parts map[string]string + KeptOn string + Version string } // withKept is w naming the kept build at its fingerprint, the parts by digest and the keeping node, only while the node's @@ -124,7 +126,7 @@ func (n Nodes) withKept(m Module, node string, w mwc.Want) mwc.Want { if !ok || !slices.Contains(n.Reads[node], WantParts) { return w } - w.Parts, w.KeptOn = k.Parts, k.KeptOn + w.Parts, w.KeptOn, w.Version = k.Parts, k.KeptOn, k.Version return w } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 59ebc1f0..fce06ef2 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -1587,7 +1587,7 @@ func TestAProviderRunsHealthyByItsRunning(t *testing.T) { // Design 47 G §525, §526: a want at a kept build names its parts and the node that keeps it, only for a node whose engine states // the capability want-parts. func TestAWantNamesAKeptBuildsPartsAndNodeForANodeThatReadsThem(t *testing.T) { - kept := KeptBuild{Parts: map[string]string{"mail:image": "sha256:1"}, KeptOn: "g14"} + kept := KeptBuild{Parts: map[string]string{"mail:image": "sha256:1"}, KeptOn: "g14", Version: "1.2.3"} for name, row := range map[string]struct { reads []string kept func(string) (KeptBuild, bool) @@ -1603,7 +1603,7 @@ func TestAWantNamesAKeptBuildsPartsAndNodeForANodeThatReadsThem(t *testing.T) { n.Reads = map[string][]string{"ace": row.reads, "g14": row.reads} m.KeptBuilds = row.kept w := Wants(m, n, wantedOld(), nil).Wants["ace"] - if w.Fp != fNew || (w.KeptOn == "g14") != row.wanted || (len(w.Parts) == 1) != row.wanted { + if w.Fp != fNew || (w.KeptOn == "g14") != row.wanted || (w.Version == "1.2.3") != row.wanted || (len(w.Parts) == 1) != row.wanted { t.Errorf("%s: ace's want = %+v", name, w) } } diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index 6a6c64b3..a536bdf1 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents" "os" "reflect" @@ -43,10 +44,9 @@ func TestManifest(t *testing.T) { slices.Sort(reads) // ADR 0593 §8: it composes nothing, so it reads no candidate, held or resolved value, compose step or build log; it reads // every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1). - // Design 47 G §526, §379: it reads the builder's log for the `build.ended` that names a kept build's parts and keeping node. // DLV-F2r (ADR 0615 §1, ADR 0631 §1): the indexer's manifests, modules and module graph (whose verb edges a want waits // on, ADR 0614 §2, §3) are read through its first views and views family, none of the indexer's states by reads. - want := []string{"mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", + want := []string{"mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed", "mesh-provisioner.provisions", "node-engine.have"} if !reflect.DeepEqual(reads, want) { @@ -56,7 +56,7 @@ func TestManifest(t *testing.T) { named := []string{buckets.Builds, buckets.Merges, buckets.Source, buckets.Answers, buckets.Presence, buckets.Grants, buckets.ComposedRead, "mesh-provisioner_provisions", buckets.Have} slices.Sort(named) - for i, r := range slices.DeleteFunc(slices.Clone(want), func(r string) bool { return r == "mesh-delivery-builder.build-log" }) { // a log, read on the connection + for i, r := range want { if got := strings.Replace(named[i], "_", ".", 1); got != r { t.Errorf("the buckets package names %q, the manifest reads %q", got, r) } @@ -130,7 +130,7 @@ func TestManifest(t *testing.T) { } // ADR 0618 §4: and it hears each node's engine say its reads changed; ADR 0636 §4 (issue 1966): and the seat list's two events; // issue 1966 note 151: and the teller's ask-ended, its subject the one the assigner hears. - if !slices.Equal(m.Consumes, []string{"mesh-conditions.findings-asked", "mesh-conditions.condition-raised", "mesh-conditions.condition-changed", "mesh-conditions.condition-cleared", "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", buckets.TellerModule + "." + tellerevents.AskEnded, "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.group-ended", "node-engine.manifest-reads-changed", "node-engine.running"}) { + if !slices.Equal(m.Consumes, []string{"mesh-conditions.findings-asked", "mesh-conditions.condition-raised", "mesh-conditions.condition-changed", "mesh-conditions.condition-cleared", "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-catalog.version-released", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", builds.BuilderModule + "." + builds.BuildEndedEvent, buckets.TellerModule + "." + tellerevents.AskEnded, "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.group-ended", "node-engine.manifest-reads-changed", "node-engine.running"}) { t.Errorf("consumes = %v, want the catalog half's revisions (design 47 G §195), the teller's ask-ended, the engines' reads changed and what they run", m.Consumes) } if want := []string{"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index b9244115..aaf2d344 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -30,7 +30,6 @@ } ], "reads": [ - "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", @@ -51,8 +50,10 @@ "mesh-conditions.condition-cleared", "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", + "mesh-catalog.version-released", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", + "mesh-delivery-builder.build.ended", "mesh-delivery-teller.ask-ended", "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.group-ended",