From 5524cfa8d807c755952758e90e1bacf97cb96196 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:27:01 +0100 Subject: [PATCH] BL6: the builder follows the catalog half per watched branch through the shared branch follower MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder adopts meshcatalogclient's branch follower on its views reader (A-5, A-15): the modules, their manifests (kept from the entry that holds them, by its digest), their packages and each package's name in its ecosystem are read from the catalog half's entries, never the indexer's modules, manifests, candidates and packages views; an entry of a branch other than the module's first builds its fingerprint and supersedes only its own branch's older one; the start's sweep runs once every followed half's first view is in; the builder's tools bundle reads package names through the same follower. A run's candidate is no input of the builder (A-84): its read of the indexer's candidates view goes, and the entry point for a run's build ask is not built (no line gives the ask's form). The seats' events the builder's own handler takes are named per seat so the follower's events have one taker. Builds: design 47 G §348, §479, §614, A-5, A-15, A-84 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-builder-tools/main.go | 25 +- .../cmd/mesh-delivery-builder/bus.go | 24 -- .../cmd/mesh-delivery-builder/catalog.go | 191 ++++++++++++++++ .../cmd/mesh-delivery-builder/catalog_test.go | 27 +++ .../catalogholder_integration_test.go | 94 ++++++++ .../mesh-delivery-builder/integration_test.go | 214 ++++++------------ .../cmd/mesh-delivery-builder/main.go | 116 +++------- .../cmd/mesh-delivery-builder/names.go | 28 ++- .../cmd/mesh-delivery-builder/names_test.go | 12 +- .../cmd/mesh-delivery-builder/versions.go | 49 ---- .../internal/build/entry_test.go | 28 +++ .../internal/build/run.go | 87 ++++++- .../internal/versions/entrynames.go | 82 +++++++ .../internal/versions/entrynames_test.go | 55 +++++ .../internal/versions/names.go | 6 +- .../mesh-delivery-builder/manifest_test.go | 16 +- modules/mesh-delivery-builder/module.json | 15 +- 17 files changed, 711 insertions(+), 358 deletions(-) create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go create mode 100644 modules/mesh-delivery-builder/internal/build/entry_test.go create mode 100644 modules/mesh-delivery-builder/internal/versions/entrynames.go create mode 100644 modules/mesh-delivery-builder/internal/versions/entrynames_test.go diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index 221653b6..29740838 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -15,6 +15,7 @@ import ( "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" @@ -328,26 +329,12 @@ func written(s stdio.KeptState, key string, revision uint64) time.Time { return time.Time{} } -// The state the bundle follows, its owner's tool, and the seat verbs it calls (the manifest's invokes). +// The seat verbs the bundle calls (the manifest's invokes). const ( - indexerModule = "mesh-delivery-indexer" - packagesState = "packages" - packagesTool = indexerModule + ".indexer_read_packages" catalogVersionsVerb = "seat:mesh-catalog.versions" reservedVerb = "seat:mesh-builds.reserved" ) -// followedPackages is the catalog half's packages state as the bundle's picture of it holds it (versions.PackagesState). -type followedPackages struct{ r *views.Reader } - -func (f followedPackages) Keys(context.Context) ([]string, error) { - return f.r.Keys(indexerModule + "." + packagesState) -} - -func (f followedPackages) Get(_ context.Context, key string) ([]byte, bool, error) { - return f.r.Get(indexerModule+"."+packagesState, key) -} - func main() { steps := map[string]build.StateRead{} for _, bm := range build.StuckBuildModules { @@ -407,9 +394,13 @@ func main() { // The catalog half's view of the packages the bundle follows itself (design 47 G §572, A-70), as other bundles follow views. pk := &views.Reader{Module: builds.BuilderModule, Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}} - pk.Follow(indexerModule, packagesState, packagesTool) + branches, err := meshcatalogclient.FollowBranches(pk, stdio.Ask) + if err != nil { + fmt.Fprintln(os.Stderr, "mesh-delivery-builder-tools:", err) + os.Exit(1) + } pk.Start(context.Background()) - carriers := versions.Carriers(followedPackages{pk}) + carriers := versions.CarriersFrom(versions.EntriesOf(branches)) released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) { raw, err := stdio.Ask(catalogVersionsVerb, meshcatalog.VersionsInput{Module: module}) if err != nil { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go index 34654335..694d507d 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go @@ -3,13 +3,9 @@ package main import ( "context" "errors" - "fmt" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" - "git.novox.be/novox/mesh-sdk/go/catalogue" - "mesh-delivery-builder/internal/build" ) @@ -106,23 +102,3 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key } } } - -// manifestReader reads manifests. of the indexer and parses the manifest it holds. -type manifestReader struct{ kv jetstream.KeyValue } - -func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) { - key, err := meshcatalogclient.ManifestsKey(digest) - if err != nil { - return catalogue.Manifest{}, err - } - e, err := m.kv.Get(ctx, key) - if err != nil { - return catalogue.Manifest{}, fmt.Errorf("manifests.%s: %w", key, err) - } - kept, err := meshcatalogclient.DecodeManifest(e.Value()) - if err != nil { - return catalogue.Manifest{}, err - } - // Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside. - return catalogue.Decode([]byte(kept.Manifest)) -} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go new file mode 100644 index 00000000..19515895 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go @@ -0,0 +1,191 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "sync" + "time" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/views" + "mesh-delivery-builder/internal/build" +) + +// **The builder reads the catalog half per watched branch** (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower +// of meshcatalogclient, adopted on the builder's views reader, gives each watched branch's entries; the manifests, the packages +// and each package's name in its ecosystem are read from the entries, never the indexer's states. + +// entryManifests keeps the manifest of each entry heard, by the entry's manifest digest (the entry holds the manifest whole). +type entryManifests struct { + mu sync.Mutex + byFp map[string]catalogue.Manifest +} + +// keep takes the manifest of e in; an entry with no manifest or digest keeps none. +func (m *entryManifests) keep(e meshcatalog.CatalogEntry) error { + if e.ManifestDigest == "" || len(e.Manifest) == 0 { + return nil + } + raw, err := json.Marshal(e.Manifest) + if err != nil { + return err + } + // Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside. + mf, err := catalogue.Decode(raw) + if err != nil { + return fmt.Errorf("the manifest of the entry of %s does not decode: %w", e.Module, err) + } + m.mu.Lock() + defer m.mu.Unlock() + if m.byFp == nil { + m.byFp = map[string]catalogue.Manifest{} + } + m.byFp[e.ManifestDigest] = mf + return nil +} + +// Manifest is the manifest the entry heard with that digest holds. +func (m *entryManifests) Manifest(_ context.Context, digest string) (catalogue.Manifest, error) { + m.mu.Lock() + defer m.mu.Unlock() + mf, ok := m.byFp[digest] + if !ok { + return catalogue.Manifest{}, fmt.Errorf("no entry of the catalog half holds the manifest %s", digest) + } + return mf, nil +} + +// catalogHalf is the builder's part over the branch follower: what changes in a branch's half goes to the builder, and the +// entries of every followed half are what the package names are read from. +type catalogHalf struct { + branches *meshcatalogclient.Branches + b *build.Builder + manifests *entryManifests + logf func(string, ...any) + // named is, by module, the package names of its first entry last heard, so a name newly heard looks at its module again. + mu sync.Mutex + named map[string]string + // ready is told when every followed branch's first view is in, once; halves are the views adopted (sweepWiring). + ready func() + halves map[string]*views.Follower + once sync.Once + ctx context.Context +} + +// first says whether branch is the first watched branch of module's registration (design 47 G §371); a module with no +// registration heard is taken as watched on this branch, the first one. +func (c *catalogHalf) first(branch, module string) bool { + reg, ok := c.branches.Registrations.Registration(module) + return !ok || len(reg.Branches) == 0 || reg.Branches[0] == branch +} + +// Changed is the follower's Changed: one module's entry of a branch came, or went. +func (c *catalogHalf) Changed(ctx context.Context) func(branch, module string, entry json.RawMessage, deleted bool, at time.Time) { + return func(branch, module string, entry json.RawMessage, deleted bool, _ time.Time) { + first := c.first(branch, module) + if deleted { + c.b.EntryGone(branch, first, module) + return + } + var e meshcatalog.CatalogEntry + if err := json.Unmarshal(entry, &e); err != nil || e.Module != module { + c.logf("the catalog half of the branch %q holds what is not the entry of %s, left: %v", branch, module, err) + return + } + if err := c.manifests.keep(e); err != nil { + c.logf("%v", err) + } + c.b.Entry(ctx, branch, first, e) + c.mu.Lock() + was := c.named[module] + now := namesOf(e) + if c.named == nil { + c.named = map[string]string{} + } + c.named[module] = now + c.mu.Unlock() + if first && now != "" && now != was { + c.logf("the catalog half's entry names a package of %s: its build is looked at again", module) + c.b.PackageNamed(ctx, module) + } + } +} + +// namesOf are the package names an entry's artifacts carry, in one line. +func namesOf(e meshcatalog.CatalogEntry) string { + var names []string + for _, a := range e.Artifacts { + if a.Package != nil && a.Package.Name != "" { + names = append(names, a.Ref+"="+a.Package.Name) + } + } + slices.Sort(names) + return fmt.Sprint(names) +} + +// sweepWiring is the follower's wiring on the builder's reader that learns each catalog half view adopted, so ready is told once +// when the first view of every branch followed is in (a branch with no revision yet is ready and empty at once). +type sweepWiring struct { + meshcatalogclient.Wiring + half *catalogHalf +} + +func (w sweepWiring) Adopt(f *views.Follower, seatName, event string, hear func(context.Context, []byte) error) error { + if err := w.Wiring.Adopt(f, seatName, event, hear); err != nil { + return err + } + if seatName == meshcatalog.Seat && event == meshcatalog.EventCatalogRevision { + w.half.adopted(f) + } + return nil +} + +func (w sweepWiring) Drop(owner, state string) error { + err := w.Wiring.Drop(owner, state) + w.half.dropped(state) + return err +} + +func (c *catalogHalf) adopted(f *views.Follower) { + c.mu.Lock() + if c.halves == nil { + c.halves = map[string]*views.Follower{} + } + c.halves[f.State] = f + c.mu.Unlock() + go func() { + select { + case <-c.ctx.Done(): + case <-f.Ready(): + c.checkReady() + } + }() +} + +func (c *catalogHalf) dropped(state string) { + c.mu.Lock() + delete(c.halves, state) + c.mu.Unlock() + c.checkReady() +} + +// checkReady calls ready once every adopted half's first view is in. +func (c *catalogHalf) checkReady() { + c.mu.Lock() + all := len(c.halves) > 0 + for _, f := range c.halves { + select { + case <-f.Ready(): + default: + all = false + } + } + c.mu.Unlock() + if all && c.ready != nil { + c.once.Do(c.ready) + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go new file mode 100644 index 00000000..87768101 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// TestTheManifestOfAnEntryIsReadByItsDigest: the builder reads a manifest from the entry that holds it, by the entry's manifest +// digest (design 47 G §348), and an entry with no manifest keeps none. +func TestTheManifestOfAnEntryIsReadByItsDigest(t *testing.T) { + m := &entryManifests{} + e := meshcatalog.CatalogEntry{Module: "netcheck", ManifestDigest: "sha256:aa", Manifest: map[string]any{"module": "netcheck", "version": "1"}} + if err := m.keep(e); err != nil { + t.Fatal(err) + } + if mf, err := m.Manifest(context.Background(), "sha256:aa"); err != nil || mf.Module != "netcheck" { + t.Errorf("Manifest(sha256:aa) = %+v, %v", mf, err) + } + if _, err := m.Manifest(context.Background(), "sha256:bb"); err == nil { + t.Error("Manifest of a digest no entry holds: want an error") + } + if err := m.keep(meshcatalog.CatalogEntry{Module: "bare"}); err != nil || len(m.byFp) != 1 { + t.Errorf("an entry with no manifest kept one: %v, %v", err, m.byFp) + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go new file mode 100644 index 00000000..6d2950fe --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go @@ -0,0 +1,94 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "sync" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" +) + +// catalogHolder is the holder of the seat mesh-catalog as the bed needs it (design 47 G §348, §614): it answers `revision` for the +// branch main with the entries put so far, `registrations` with none (so the follower follows the branches the gate's `source` +// answers) and `entry`, and says `catalog-revision` at each put, as the follower and the builder hear it. +type catalogHolder struct { + ctx context.Context + js jetstream.JetStream + + mu sync.Mutex + entries map[string]meshcatalog.CatalogEntry + revision int64 +} + +func newCatalogHolder(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) *catalogHolder { + t.Helper() + h := &catalogHolder{ctx: ctx, js: js, entries: map[string]meshcatalog.CatalogEntry{}} + answer := func(subject string, with func(in []byte) any) { + s, err := nc.Subscribe(subject, func(m *nats.Msg) { + reply, _ := json.Marshal(map[string]any{"result": with(m.Data), "node": "ctl"}) + _ = m.Respond(reply) + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = s.Unsubscribe() }) + } + answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbRevision+".>", func([]byte) any { return h.whole() }) + answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbRegistrations+".>", func([]byte) any { + return meshcatalog.RegistrationsAnswer{Registrations: []meshcatalog.Registration{}} + }) + answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbEntry+".>", func(in []byte) any { + var q meshcatalog.EntryInput + _ = json.Unmarshal(in, &q) + h.mu.Lock() + defer h.mu.Unlock() + for _, e := range h.entries { + if e.Fp == q.Fingerprint { + return meshcatalog.EntryAnswer{Entry: e} + } + } + return map[string]any{"error": "no entry of that fingerprint"} + }) + answer("mesh.seat."+meshdelivery.Seat+".call."+meshdelivery.VerbSource+".>", func([]byte) any { + return meshdelivery.SourceAnswer{Branches: []string{"main"}} + }) + return h +} + +// whole is the newest trunk revision of main, every entry in one page; the branch has none yet before the first put. +func (h *catalogHolder) whole() meshcatalog.RevisionAnswer { + h.mu.Lock() + defer h.mu.Unlock() + if h.revision == 0 { + return meshcatalog.RevisionAnswer{NoneYet: true} + } + var refs []meshcatalog.EntryRef + var all []meshcatalog.CatalogEntry + for m, e := range h.entries { + refs = append(refs, meshcatalog.EntryRef{Module: m, Digest: "sha256:" + e.Fp}) + all = append(all, e) + } + at := time.Now() + return meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Revision: h.revision, Kind: "trunk", Branch: "main", Entries: refs, Source: 1, At: &at}, Entries: all} +} + +// put sets one module's entry in a new trunk revision of main and says catalog-revision. +func (h *catalogHolder) put(t *testing.T, e meshcatalog.CatalogEntry) { + t.Helper() + h.mu.Lock() + h.revision++ + h.entries[e.Module] = e + ev, _ := json.Marshal(meshcatalog.CatalogRevisionEvent{Branch: "main", Kind: "trunk", Revision: h.revision, Source: 1, Entries: int64(len(h.entries)), Changed: []string{e.Module}, V: 1}) + h.mu.Unlock() + if _, err := h.js.Publish(h.ctx, meshcatalog.SubjectCatalogRevision("main"), ev); err != nil { + t.Fatal(err) + } +} 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 19619406..3d5831a7 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 @@ -219,20 +219,18 @@ func (b *bed) trunk(t *testing.T, head string) uint64 { } type bed struct { - ctx context.Context - js jetstream.JetStream - modules jetstream.KeyValue - candidates jetstream.KeyValue - source jetstream.KeyValue - builds jetstream.KeyValue - heard jetstream.KeyValue // the state toolchains-heard - instances jetstream.KeyValue - packages jetstream.KeyValue // the indexer's packages, served by its read tool and views family - out *saidLog // the builder's process log, every instance's - stop func() // stops the builder started last - fake *fakeBuildModules - srvURL string - dir string + ctx context.Context + js jetstream.JetStream + catalog *catalogHolder // the seat mesh-catalog, as its holder answers the branch follower + source jetstream.KeyValue + builds jetstream.KeyValue + heard jetstream.KeyValue // the state toolchains-heard + instances jetstream.KeyValue + out *saidLog // the builder's process log, every instance's + stop func() // stops the builder started last + fake *fakeBuildModules + srvURL string + dir string } // startBed starts the bus, the buckets the controller makes from the builder's and the build modules' manifests, the @@ -300,17 +298,12 @@ func startBedWith(t *testing.T, closed bool) *bed { } return kv } - b.modules = mk(indexerBucket(modulesState), modulesHistory) - b.candidates = mk(indexerBucket(candidatesState), candidatesHistory) b.source = mk(sourceBucket, sourceHistory) - mk(indexerBucket(manifestsState), manifestsHistory) - // The indexer and the assigner as the owners of the states the builder follows: their read tools and views families - // (ADR 0615, DLV-M1); n1 and n2 each have their durable consumer. + // The seat mesh-catalog's holder and the assigner as the owners of what the builder follows: the catalog half through the seat's + // verbs and events (design 47 G §348, §614), the assignments by their read tool and views family (ADR 0615, DLV-M1); n1 and n2 + // each have their durable consumer. + b.catalog = newCatalogHolder(t, ctx, js, ctl) readers := []string{"n1_" + builds.BuilderModule, "n2_" + builds.BuilderModule} - for _, st := range []string{modulesState, manifestsState, candidatesState} { - viewstest.Serve(t, ctx, js, ctl, indexerModule, st, indexerModule+".indexer_read_"+st, readers...) - } - b.packages = viewstest.Serve(t, ctx, js, ctl, indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages", readers...) viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...) b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory) b.instances = mk(instancesBucket, 1) @@ -400,28 +393,34 @@ func (b *bed) moduleFP(t *testing.T, name, fixture, path string, toolchains map[ t.Fatal(err) } digest := meshcatalogclient.ManifestDigest(raw) - manifests, err := b.js.KeyValue(b.ctx, indexerBucket(manifestsState)) - if err != nil { - t.Fatal(err) - } - mv, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: name, Manifest: string(raw)}) - if err != nil { - t.Fatal(err) - } - if _, err := manifests.Put(b.ctx, digest, mv); err != nil { - t.Fatal(err) - } - data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: at, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2, - Manifest: digest, Path: path, Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)}) - if err != nil { - t.Fatal(err) - } - if _, err := b.modules.Put(b.ctx, name, data); err != nil { + var manifest map[string]any + if err := json.Unmarshal(raw, &manifest); err != nil { t.Fatal(err) } + b.catalog.put(t, b.entry(name, path, at, fp2, digest, manifest, toolchains, src)) return digest } +// manifestOf is a fixture manifest as the entry holds it. +func (b *bed) manifestOf(t *testing.T, fixture string) map[string]any { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", fixture)) + if err != nil { + t.Fatal(err) + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + return m +} + +// entry is the catalog graph entry of a module as the holder answers it. +func (b *bed) entry(name, path, at, fp2, digest string, manifest map[string]any, toolchains map[string]builds.ToolchainUsed, src uint64) meshcatalog.CatalogEntry { + return meshcatalog.CatalogEntry{Module: name, Repository: "novox/mesh-catalog", Commit: at, Fp: fp2, Path: path, ManifestDigest: digest, Manifest: manifest, + Inputs: &meshcatalog.EntryInputs{Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)}} +} + // archive writes modules. as the real indexer does for a module of an archive only: its fingerprint names no // toolchain (mesh-delivery-indexer internal/index inputs.go languagesOf gives an archive no language), so Toolchains is empty. func (b *bed) archive(t *testing.T, name, path, at string) string { @@ -455,6 +454,12 @@ type saidLog struct { buf strings.Builder } +func (l *saidLog) String() string { + l.mu.Lock() + defer l.mu.Unlock() + return l.buf.String() +} + func (l *saidLog) Write(p []byte) (int, error) { l.mu.Lock() defer l.mu.Unlock() @@ -572,11 +577,12 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 t.Errorf("%s called %d times, want %d (on one node, ADR 0478 clause 4)", tool, got, want) } } - // Design 32, ADR 0615 §1: a package's name the indexer writes later reaches the builder by its packages' views family - // (never their bucket), and looks at the module again; the done bundles are kept, not numbered twice. - if _, err := b.packages.Put(b.ctx, "netcheck.0.1", []byte(`{"v":1,"name":"netcheck","package":"netcheck/lib","series":"0.1"}`)); err != nil { - t.Fatal(err) - } + // Design 32, design 47 G §348: a package's name the catalog half's entry of the module gives later reaches the builder by the + // branch follower's view (never the indexer's state), and looks at the module again; the done bundles are kept, not numbered twice. + named := b.entry("netcheck", "modules/netcheck", commit, fp, digest, nil, bothToolchains, 0) + named.Manifest = b.manifestOf(t, "catalogue-a67f0196-netcheck.module.json") + named.Artifacts = []meshcatalog.Artifact{{Ref: "netcheck:lib", Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: "netcheck"}}} + b.catalog.put(t, named) b.awaitSaid(t, "names a package of netcheck") // Design 47 G §349: version.numbered is no longer said, for a number or for a late name. if got := reg.numberedOf("netcheck/tools-go"); len(got) != 0 { @@ -639,7 +645,7 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 reg.awaitFinding(t, failed, true) b.fake.hang.Store(nil) close(release) - b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, commit, third, 3) // the indexer writes it again + b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, "89abcdef0123456789abcdef0123456789abcdef", third, 3) // the entry moves to a later commit, same fingerprint b.awaitFP(t, "netcheck", third, isState(builds.BuildDone)) b.awaitSaid(t, "kept: fingerprint "+third+" is numbered under it") if !slices.ContainsFunc(b.logged(t, "netcheck"), func(e builds.BuildLogEntry) bool { @@ -739,7 +745,7 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A map[string]builds.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}}, commit) b.await(t, "netcheck2", isState(builds.BuildDone)) if _, err := b.builds.Get(b.ctx, mustKey(t, "netcheck")); err == nil { - t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2") + t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2; the log: " + b.out.String()) } if n := b.fake.count("mesh-build-go.build_go_build"); n != 1 { t.Errorf("build_go_build was called %d times, want 1 (the new fingerprint on one node only)", n) @@ -778,77 +784,14 @@ func TestAKeyLeftBuildingForAModuleThatIsGoneIsClosedAtStart(t *testing.T) { } } -// TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk: the trunk's record of netcheck carries a fingerprint of its -// own; a candidate the indexer wrote gives netcheck the fingerprint fp at a pull request's head; the program writes -// builds.netcheck. done, its fetch given the head, the ref that carries it and the trunk head of the candidate's source -// revision, newer than the module value's commit (issue 1684; the operator 2026-10-25: a value names the commit its -// fingerprint first appeared at). -func TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk(t *testing.T) { // N7/AC1 +// TestAnImageIsBuiltOnOneNodeAndEveryFingerprintOfTheCatalogHalfPublishes: the catalog half's entry of imaged (one image +// artifact) is built on n1, where mesh-build-image runs: its key is done with the digest and the reference, its build call carrying +// publish; the entry's next fingerprint is built again with publish and ends with a reference of its own (ADR 0487 clauses 5 and +// 12; a run's candidate is no input of the builder: design 47 G A-84). +func TestAnImageIsBuiltOnOneNodeAndEveryFingerprintOfTheCatalogHalfPublishes(t *testing.T) { // 1705 b := startBed(t) other := "src2:" + strings.Repeat("e", 64) - digest := b.module(t, "netcheck", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck") - data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: other, - Manifest: digest, Path: "modules/netcheck", Toolchains: build.CatalogToolchains(map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}})}) - if err != nil { - t.Fatal(err) - } - if _, err := b.modules.Put(b.ctx, "netcheck", data); err != nil { - t.Fatal(err) - } - const head = "1111111111111111111111111111111111111111" - pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"netcheck"}, - Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"netcheck": {Fp2: fp, Manifest: digest}}}} - hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) - cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) - if err != nil { - t.Fatal(err) - } - ckey, err := keys.EscapeName("feat/x"), error(nil) - if err != nil { - t.Fatal(err) - } - if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil { - t.Fatal(err) - } - b.start(t, "n1", nil) - v := b.await(t, "netcheck", isState(builds.BuildDone)) - if len(v.Artifacts) != 2 { - t.Fatalf("build = %+v", v) - } - b.fake.mu.Lock() - defer b.fake.mu.Unlock() - found := false - for _, f := range b.fake.fetches { - if f["commit"] == head && f["ref"] == "refs/pull/7/head" && f["base"] == trunkHead { - found = true - } - } - if !found { - t.Errorf("fetches = %v, want one of the candidate's head with its ref and the trunk head it was previewed onto, not the module value's commit", b.fake.fetches) - } -} - -// TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes: modules.imaged (one image artifact) is built on n1, where -// mesh-build-image runs: a trunk record's key is done with the digest and the reference, its build call carrying publish; a -// candidate's key for the same module is built first with no publish and no reference; when the pull request is merged and the -// trunk's record names the candidate's fingerprint, the key is built again with publish and ends with a reference -// (ADR 0487 clauses 5 and 12). -func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 1705 - b := startBed(t) - other := "src2:" + strings.Repeat("e", 64) - digest := b.moduleCommit(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit) - // The trunk carries `other`; its key is built and published. - trunk := func(fp2 string) { - data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2, - Manifest: digest, Path: "modules/imaged"}) - if err != nil { - t.Fatal(err) - } - if _, err := b.modules.Put(b.ctx, "imaged", data); err != nil { - t.Fatal(err) - } - } - trunk(other) + digest := b.moduleFP(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit, other, 0) b.start(t, "n1", nil) okey, _ := builds.BuilderBuildKey("imaged", other) awaitKey := func(key string, pred func(builds.BuilderBuild) bool) builds.BuilderBuild { @@ -860,42 +803,23 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 } } } - t.Fatalf("builds.%s = %+v within 20 s", key, last) + t.Fatalf("builds.%s = %+v within 20 s; the log: %s", key, last, b.out.String()) return last } v := awaitKey(okey, isState(builds.BuildDone)) if len(v.Artifacts) != 1 || v.Artifacts[0].Digest != digestOf("server") || !strings.HasSuffix(v.Artifacts[0].Reference, "@"+digestOf("server")) { - t.Fatalf("trunk build = %+v, want the digest and the reference", v) + t.Fatalf("build = %+v, want the digest and the reference", v) } - // The candidate carries fp: built, not published. - const head = "1111111111111111111111111111111111111111" - pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"imaged"}, - Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"imaged": {Fp2: fp, Manifest: digest}}}} - hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) - cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) - if err != nil { - t.Fatal(err) + // The module's next fingerprint: built once more, published. + if got := b.moduleFP(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit, fp, 0); got != digest { + t.Fatalf("manifest digest = %s, want %s", got, digest) } - ckey := keys.EscapeName("feat/x") - if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil { - t.Fatal(err) - } - v = b.await(t, "imaged", isState(builds.BuildDone)) - if len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" { - t.Fatalf("candidate build = %+v, want done with no reference", v) - } - b.fake.mu.Lock() - if len(b.fake.builds) != 2 || b.fake.builds[0]["publish"] != true || b.fake.builds[1]["publish"] != nil { - t.Errorf("builds = %v, want the trunk's with publish, then the candidate's with none", b.fake.builds) - } - b.fake.mu.Unlock() - // The pull request is merged: the trunk's record names fp, and its key ends published, built once more with publish. - trunk(fp) - v = b.await(t, "imaged", func(v builds.BuilderBuild) bool { + fkey, _ := builds.BuilderBuildKey("imaged", fp) + v = awaitKey(fkey, func(v builds.BuilderBuild) bool { return v.State == builds.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Reference != "" }) if v.Artifacts[0].Digest != digestOf("server") { - t.Errorf("merged build = %+v", v) + t.Errorf("second build = %+v", v) } b.fake.mu.Lock() defer b.fake.mu.Unlock() @@ -905,8 +829,8 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 published++ } } - if len(b.fake.builds) != 3 || published != 2 { - t.Errorf("builds = %v, want three, two of them with publish (the trunk's two fingerprints)", b.fake.builds) + if len(b.fake.builds) != 2 || published != 2 { + t.Errorf("builds = %v, want two, both with publish", b.fake.builds) } } 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 adac30c3..6f2702b0 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -30,10 +30,10 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" - "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/trunk" @@ -78,10 +78,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err != nil { return err } - // The indexer's modules, manifests, candidates and packages and the assigner's assignments are their owners' states, read by the - // first view of their read tools and then their views families, never their buckets (ADR 0615 §1, DLV-M1). An owner that - // is down never holds the start: each is read on a goroutine once its first view is answered, and until then the - // unanswered first view is the builder's finding. + // The assigner's assignments are its state, read by the first view of its read tool and then its views family, never its + // bucket (ADR 0615 §1, DLV-M1); the catalog half is read through the seat mesh-catalog. An owner that is down never holds the + // start: each is read on a goroutine once its first view is answered, and until then the unanswered first view is the + // builder's finding. calls, err := process.Connect(cred, nats.Name(cred.Node+"."+cred.Module+".calls"), nats.RetryOnFailedConnect(true)) if err != nil { return err @@ -105,24 +105,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } }) reader.Findings = func() views.WireWriter { return fw } - modules, err := reader.Follow(indexerModule, modulesState, indexerModule+".indexer_read_modules") - if err != nil { - return err - } - manifests, err := reader.Follow(indexerModule, manifestsState, indexerModule+".indexer_read_manifests") - if err != nil { - return err - } - // The candidates' fingerprints are built too (issue 1684). - candidates, err := reader.Follow(indexerModule, candidatesState, indexerModule+".indexer_read_candidates") - if err != nil { - return err - } - // The indexer's packages, whose names a package is numbered under (design 32). - packages, err := reader.Follow(indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages") + // The catalog half per watched branch (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower of + // meshcatalogclient on the builder's reader, so the modules, their manifests, their packages and each package's name in its + // ecosystem are read from the entries, never the indexer's states. + half := &catalogHalf{manifests: &entryManifests{}, logf: logger.Printf, ctx: ctx} + branches, err := meshcatalogclient.NewBranches(sweepWiring{meshcatalogclient.ProcessWiring(reader), half}, calls.Call) if err != nil { return err } + branches.Logf = logger.Printf + half.branches = branches assignments, err := reader.Follow(assignerModule, "assignments", assignerModule+".assigner_read_assignments") if err != nil { return err @@ -171,9 +163,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Ready(func(kv jetstream.KeyValue) { floorWaits.Keep(kvKeeper{ctx: ctx, kv: kv}, heldFloorWaits(ctx, kv, logger.Printf)) }) - // A package is numbered under its name in its ecosystem, read from the indexer's packages state (design 32). + // A package is numbered under its name in its ecosystem, read from the catalog half's entries (design 32, design 47 G §348). numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call), Waits: floorWaits, - Names: versions.NamesIn(indexerPackages{packages})} + Names: versions.NamesFrom(versions.EntriesOf(branches))} // A publish that ends badly is a finding (design 51 *Health and conditions*): a holder's refused, a version no holder // answered within its bound (a key of the state bounds with its own time to live), a seat's outcome that does not read. pubBoundsKV := process.AwaitBucket(ctx, js, conn, versions.BoundsBucket, "a publish no holder answers, and a node holding the old form seven days, are not raised", logger.Printf) @@ -187,7 +179,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Gone: heldOnTheBus{js}, CheckAfter: bound, Toolchains: tools, - Manifests: manifestReader{manifests}, + Manifests: half.manifests, Logf: logger.Printf, Stored: storedVia(calls.Call), Node: cred.Node, @@ -245,7 +237,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro trunks.start() b.Trunk = trunks.head b.MemberBounds = &memberBounds{bounds: pubBoundsKV.KV, now: time.Now, logf: logger.Printf} - b.Members = versions.ModuleOf(indexerPackages{packages}) // the build of a group's package member (design 47 G §373, §465) + b.Members = versions.ModuleFrom(versions.EntriesOf(branches)) // the build of a group's package member (design 47 G §373, §465) // The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the // indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since. b.Log = func(e builds.BuildLogEntry) { @@ -380,11 +372,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } heardGone(ctx, b, m, logger.Printf) } - // The seats' events (artifacts-gone, the registry seats' words, manifest-accepted and manifest-reads-changed and the - // seat mesh-conditions' condition-cleared, which this pattern covers: a consumer takes no two overlapping filters); the - // owners' views events are the views reader's. - if err := hub.Handle("mesh.seat.*.event.>", events); err != nil { - return err + // The seats' events (artifacts-gone, the registry seats' words, the build seats' toolchain-reported, manifest-accepted and + // manifest-reads-changed and the seat mesh-conditions' condition-cleared: seatEventPatterns); the owners' views events and the + // catalog half's catalog-revision and registration-changed and the gate's source-moved are the views reader's (a message has + // one taker, so no pattern here covers them). + for _, pattern := range seatEventPatterns { + if err := hub.Handle(pattern, events); err != nil { + return err + } } hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4) if stopped != nil { @@ -397,10 +392,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return hub.Start(ctx, js, cred) }) go func() { <-ctx.Done(); hub.Stop() }() - // The modules, once their first views and the manifests' are answered: what a module builds is read from the manifests. - go func() { errs <- buildFromModules(ctx, b, modules, manifests, candidates) }() - // A package's name heard in the indexer's packages looks at its module again, so a done build is numbered then. - go func() { errs <- followPackages(ctx, packages, b, logger.Printf) }() + // The catalog half's entries as they come: a module's entry of a branch goes to the builder, and a key left building for a + // fingerprint no branch carries is closed (issue 1660) once the first view of every followed branch is in (never at the start: + // a holder of the seat that is down holds no other part of it). + half.b = b + half.ready = func() { + if err := b.Sweep(ctx); err != nil { + errs <- err + } + } + branches.Changed = half.Changed(ctx) // A person asks a key's build again with builder_ask_again, which writes the reasks state (issue 1945): watched, as // every other input. The builder asks again by itself without it. // A state not on the bus yet at this start is watched once it appears (issue 1790). @@ -482,55 +483,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // keeper raises a first view unanswered past its bound (ADR 0615 §6). var firstViewsBucket = builds.BuilderModule + "_" + views.StatusState -// The indexer's states the builder follows (indexerBucket(modulesState), ManifestsBucket, CandidatesBucket), and the owners' -// views families as their manifests' `streams` name them. -const ( - modulesState = "modules" - manifestsState = "manifests" - candidatesState = "candidates" -) - -// buildFromModules builds from the indexer's modules and candidates once the first views of the modules, the manifests and -// the candidates are answered (never at the start: an indexer that is down holds no other part of it), and follows them. -func buildFromModules(ctx context.Context, b *build.Builder, modules, manifests, candidates *views.Picture) error { - for _, p := range []*views.Picture{modules, manifests, candidates} { - select { - case <-ctx.Done(): - return nil - case <-p.Follower().Ready(): - } - } - each := func(e jetstream.KeyValueEntry) { - if opOf(e) { - b.Module(ctx, e.Key(), e.Value()) - } else { - b.Forget(e.Key()) - } - } - w, err := settle(ctx, modules, each) - if err != nil { - return err - } - // A key left building for a fingerprint its module no longer has is closed (issue 1660); the modules are all known now. - if err := b.Sweep(ctx); err != nil { - return err - } - // The fingerprints the indexer gives the candidates' modules are built too (issue 1684), once the trunk's modules are known. - eachCandidate := func(e jetstream.KeyValueEntry) { - if opOf(e) { - b.Candidate(ctx, e.Key(), e.Value()) - } else { - b.ForgetCandidate(e.Key()) - } - } - cw, err := settle(ctx, candidates, eachCandidate) - if err != nil { - return err - } - go func() { _ = follow(ctx, cw, eachCandidate) }() - return follow(ctx, w, each) -} - // sourceHeads are the trunk heads of each revision of the gate's source.main the watch delivered, the newest // sourceHistory of them: what a candidate's base revision names. type sourceHeads struct { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go index b52510ea..53c8b837 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go @@ -21,17 +21,27 @@ const ( // sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names. sourceBucket = gateModule + "_source" - // packagesBucket is the indexer's state packages. - packagesBucket = indexerModule + "_" + meshcatalog.StatePackages - // The revisions each keeps per key (the beds of the integration test create them alike). - sourceHistory = 64 - candidatesHistory = 10 - modulesHistory = 20 - manifestsHistory = 1 + // The revisions the gate's source keeps per key (the bed of the integration test creates it alike). + sourceHistory = 64 ) -func indexerBucket(state string) string { return indexerModule + "_" + state } - // namedTreePrefix opens the tree input of a module whose every artifact names its paths. const namedTreePrefix = "named:" + +// seatEventPatterns are the seats' events the builder's own handler takes off its durable consumer (its manifest's `consumes` of +// them): the registry seats' words, artifacts-gone, the build seats' toolchain-reported, condition-cleared, the node-engine +// seat's events and, of the seat mesh-catalog, manifest-accepted, version-released and versions-filled. The seat mesh-catalog's +// catalog-revision and registration-changed and the seat mesh-delivery's source-moved are the views reader's (the branch +// follower's), so no pattern here covers them: a message has one taker. +var seatEventPatterns = func() []string { + var out []string + for _, seat := range []string{"container-image-registry", "go-package-registry", "npm-package-registry", "oci-artifact-registry", + "mesh-artifact-sweep", "mesh-conditions", "node-engine", "node-build-go", "node-build-image", "node-build-node"} { + out = append(out, "mesh.seat."+seat+".event.>") + } + for _, event := range []string{meshcatalog.EventManifestAccepted, meshcatalog.EventVersionReleased, meshcatalog.EventVersionsFilled} { + out = append(out, "mesh.seat."+meshcatalog.Seat+".event."+event, "mesh.seat."+meshcatalog.Seat+".event."+event+".>") + } + return out +}() diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go index 2c1549b5..3b3efee3 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "slices" + "strings" "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" @@ -32,15 +33,20 @@ func TestTheNamesAreTheManifests(t *testing.T) { t.Errorf("the builder reads %s, its manifest's reads are %v", read, m.Reads) } } - for _, view := range []string{indexerModule + ".views.modules", indexerModule + ".views.manifests", indexerModule + ".views.candidates", - indexerModule + ".views.packages", assignerModule + ".views.assignments"} { + // The catalog half (design 47 G §348, §614) is followed through the seat, and no state of the indexer is. + for _, c := range m.Consumes { + if strings.HasPrefix(c, indexerModule+".") { + t.Errorf("the builder follows %s, an indexer state, its manifest consumes it", c) + } + } + for _, view := range []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", assignerModule + ".views.assignments"} { if !slices.Contains(m.Consumes, view) { t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes) } } for want, got := range map[string]string{ "mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"), - buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard", "mesh-delivery-indexer_packages": packagesBucket, + buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard", "mesh-delivery-assigner_assignments": buildrun.AssignmentsBucket, } { if want != got { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go index ed1460ab..24565e05 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go @@ -61,55 +61,6 @@ func (s awaitedVersions) Get(ctx context.Context, key string) ([]byte, uint64, b return e.Value(), e.Revision(), true, nil } -// indexerPackages is the indexer's packages state as the builder reads a package's name in its ecosystem from it (design -// 32): the picture of its read tool's first view and its views family (ADR 0615 §1), never its bucket. Until the first -// view is answered a package is built unnumbered and said (versions.Numberer.Names), and numbered once its name is heard -// (followPackages). -type indexerPackages struct{ kv jetstream.KeyValue } - -func (s indexerPackages) Keys(ctx context.Context) ([]string, error) { - keys, err := s.kv.Keys(ctx) - if errors.Is(err, jetstream.ErrNoKeysFound) { - return nil, nil - } - return keys, err -} - -func (s indexerPackages) Get(ctx context.Context, key string) ([]byte, bool, error) { - e, err := s.kv.Get(ctx, key) - if errors.Is(err, jetstream.ErrKeyNotFound) || errors.Is(err, jetstream.ErrKeyDeleted) { - return nil, false, nil - } - if err != nil { - return nil, false, err - } - return e.Value(), true, nil -} - -// followPackages watches the picture of the indexer's packages state: each series key heard that names a package's name -// (versions.NamedModule), its first view's included, looks at its module again (build.Builder.PackageNamed), so a done -// build that left the package unnumbered is numbered then with its digests. Woken by the views family, never a timer; a -// key heard again naming the name and module it named (the indexer writes a series at each publish) looks at nothing. -func followPackages(ctx context.Context, kv jetstream.KeyValue, b *build.Builder, logf func(string, ...any)) error { - w, err := kv.WatchAll(ctx) - if err != nil { - return err - } - heard := map[string]string{} - return follow(ctx, w, func(e jetstream.KeyValueEntry) { - if e.Operation() != jetstream.KeyValuePut { - delete(heard, e.Key()) - return - } - module, name, named := versions.NamedModule(e.Key(), e.Value()) - if named && heard[e.Key()] != module+" "+name { - heard[e.Key()] = module + " " + name - logf("%s.%s names a package of %s: its build is looked at again", packagesBucket, e.Key(), module) - b.PackageNamed(ctx, module) - } - }) -} - func versionsConflict(err error) error { if errors.Is(err, jetstream.ErrKeyExists) || errors.Is(err, jetstream.ErrKeyRevisionMismatch) { return versions.ErrConflict diff --git a/modules/mesh-delivery-builder/internal/build/entry_test.go b/modules/mesh-delivery-builder/internal/build/entry_test.go new file mode 100644 index 00000000..0eaa2008 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/build/entry_test.go @@ -0,0 +1,28 @@ +package build + +import ( + "reflect" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// TestAnEntryOfTheCatalogHalfIsTheFormTheBuilderBuildsBy: a catalog graph entry gives the builder its fingerprint, manifest digest, +// source, the toolchains its fingerprint was computed with, its bases, packages, needs and the named paths of its artifacts by path +// (design 47 G §348, §614). +func TestAnEntryOfTheCatalogHalfIsTheFormTheBuilderBuildsBy(t *testing.T) { + e := meshcatalog.CatalogEntry{Module: "netcheck", Repository: "novox/mesh-catalog", Path: "modules/netcheck", Commit: "abc", Fp: "src2:f", + ManifestDigest: "sha256:m", Bases: []string{"base"}, Packages: map[string]string{"p": "1"}, Needs: []string{"n"}, + Inputs: &meshcatalog.EntryInputs{Src: 7, Toolchains: map[string]meshcatalog.ToolchainUsed{"go": {Version: "go1", Recipe: "1"}}, + Paths: map[string][]string{"lib": {"a/b", "c"}}}} + got := ModuleOf(e) + if got.Fp2 != "src2:f" || got.Manifest != "sha256:m" || got.Path != "modules/netcheck" || got.Commit != "abc" || got.Src != 7 || + got.Toolchains["go"].Version != "go1" || got.Packages["p"] != "1" || !reflect.DeepEqual(got.Needs, []string{"n"}) || + got.Inputs2 == nil || !reflect.DeepEqual(got.Inputs2.Bases, []string{"base"}) || + !reflect.DeepEqual(got.Inputs2.Paths, map[string]string{"a/b": "lib", "c": "lib"}) { + t.Errorf("ModuleOf = %+v", got) + } + if bare := ModuleOf(meshcatalog.CatalogEntry{Module: "m"}); bare.Inputs2 != nil || bare.Fp2 != "" { + t.Errorf("ModuleOf of an entry with none = %+v, want nothing set", bare) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 52402be3..f6f5574b 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -117,7 +117,8 @@ type Builder struct { mu sync.Mutex // fillWaits are the builds that said wait-held for the fill (module.fingerprint), so it is said once. fillWaits sync.Map - known map[string]meshcatalog.ModulesByModuleValue // by modules key + known map[string]meshcatalog.ModulesByModuleValue // by modules key: the first watched branch's + heard map[string]meshcatalog.ModulesByModuleValue // by branch and module (take): what supersedes what inflight map[string]bool // by build key again map[string]meshcatalog.ModulesByModuleValue // by build key: the newest value that arrived while the key was being built plans map[string]planned // by manifest digest @@ -169,7 +170,8 @@ func (b *Builder) init() { } b.sem = make(chan struct{}, limit) b.cands, b.via, b.trunkFP = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]via{}, map[string]bool{} - b.known, b.inflight, b.again, b.plans, b.tried = map[string]meshcatalog.ModulesByModuleValue{}, map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} + b.heard, b.known = map[string]meshcatalog.ModulesByModuleValue{}, map[string]meshcatalog.ModulesByModuleValue{} + b.inflight, b.again, b.plans, b.tried = map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} b.published, b.held, b.waiting = map[string]bool{}, map[string]bool{}, map[string]map[string]chan string{} b.hands = map[string]*handWatch{} b.memberWaits, b.memberExpired, b.fetched = map[string]bool{}, map[string]bool{}, map[string][]sdkstep.FetchBuild{} @@ -186,8 +188,9 @@ func (b *Builder) logf(format string, args ...any) { // Wait returns when every build started so far has ended. func (b *Builder) Wait() { b.wg.Wait() } -// Module takes one value of the indexer's modules bucket in (the key is the module's name) and builds it if it is -// to be built. A value that is not a module is said and left. +// Module takes one value of a modules bucket in (the key is the module's name) and builds it if it is to be built. A value that +// is not a module is said and left. The builder takes its modules from the catalog half (Entry); this is the same take of one +// module's value, with no branch, the first one's. func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.init() m, err := meshcatalogclient.DecodeModule(value) @@ -195,9 +198,41 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.logf("modules.%s holds what is not a module, left: %v", key, err) return } + b.take(ctx, "", true, key, m) +} + +// Entry takes the catalog graph entry of one module on one watched branch in (design 47 G §348, §614) and builds its +// fingerprint if it is to be built. first says the branch is the first of the module's registration, whose entry is the module's +// value for what the builder reads by name (a version, a build again); an entry of another branch builds its fingerprint and +// supersedes only that branch's older one. An entry naming no module is said and left. +func (b *Builder) Entry(ctx context.Context, branch string, first bool, e meshcatalog.CatalogEntry) { + b.init() + if e.Module == "" { + b.logf("the catalog half of the branch %q holds an entry that names no module, left", branch) + return + } + b.take(ctx, branch, first, e.Module, ModuleOf(e)) +} + +// EntryGone takes a module's entry out of a branch's half (the module was dropped from it, or the branch is followed no more). +func (b *Builder) EntryGone(branch string, first bool, module string) { + b.init() b.mu.Lock() - old, had := b.known[key] - b.known[key] = m + delete(b.heard, branch+"\x00"+module) + b.mu.Unlock() + if first { + b.Forget(module) + } +} + +func (b *Builder) take(ctx context.Context, branch string, first bool, key string, m meshcatalog.ModulesByModuleValue) { + b.mu.Lock() + hk := branch + "\x00" + key + old, had := b.heard[hk] + b.heard[hk] = m + if first { + b.known[key] = m + } var superseded context.CancelCauseFunc if had && old.Fp2 != "" && old.Fp2 != m.Fp2 { if ok, err := builds.BuilderBuildKey(key, old.Fp2); err == nil { @@ -205,7 +240,7 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { } } if bk, err := builds.BuilderBuildKey(key, m.Fp2); err == nil && m.Fp2 != "" { - // A fingerprint the trunk carries is built from the trunk's commit, whoever asked for it first. + // A fingerprint a watched branch carries is built from that branch's commit, whoever asked for it first. b.trunkFP[bk] = true delete(b.via, bk) } @@ -218,6 +253,32 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.consider(ctx, key, m) } +// ModuleOf is a catalog entry in the form the builder builds by: its fingerprint, manifest digest, source (repository, +// directory, commit, the source revision it was taken in at), the toolchains its fingerprint was computed with, the bases it is +// built against, its packages and needs, and the named paths of its artifacts. +func ModuleOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue { + m := meshcatalog.ModulesByModuleValue{Repository: e.Repository, Path: e.Path, Commit: e.Commit, Fp2: e.Fp, None2: e.None, + Manifest: e.ManifestDigest, Packages: e.Packages, Needs: e.Needs} + if len(e.Bases) > 0 { + m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{Bases: e.Bases} + } + if in := e.Inputs; in != nil { + m.Src, m.Toolchains, m.Problems = in.Src, in.Toolchains, in.Problems + if len(in.Paths) > 0 { + if m.Inputs2 == nil { + m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{} + } + m.Inputs2.Paths = map[string]string{} + for artifact, paths := range in.Paths { + for _, p := range paths { + m.Inputs2.Paths[p] = artifact + } + } + } + } + return m +} + // NodesChanged is Reconsider after a toolchain moved, so a build that waited for the nodes to agree on one may go on. func (b *Builder) NodesChanged(ctx context.Context) { b.Reconsider(ctx) } @@ -234,10 +295,14 @@ func (b *Builder) Sweep(ctx context.Context) error { } b.mu.Lock() current := map[string]bool{} - for name, m := range b.known { - if m.Fp2 != "" { - if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil { - current[k] = true + // What every followed branch carries now is current, not only the first branch's (design 47 G §348). + for _, held := range []map[string]meshcatalog.ModulesByModuleValue{b.known, b.heard} { + for hk, m := range held { + name := hk[strings.LastIndex(hk, "\x00")+1:] + if m.Fp2 != "" { + if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil { + current[k] = true + } } } } diff --git a/modules/mesh-delivery-builder/internal/versions/entrynames.go b/modules/mesh-delivery-builder/internal/versions/entrynames.go new file mode 100644 index 00000000..a140613f --- /dev/null +++ b/modules/mesh-delivery-builder/internal/versions/entrynames.go @@ -0,0 +1,82 @@ +package versions + +import ( + "context" + "sort" + "strings" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// Entries are the catalog graph entries of the halves the builder follows, one per module and watched branch (design 47 G §348, +// §614): where a package's name in its ecosystem is read from, each artifact's `package` (§676), never the indexer's state. +type Entries func() []meshcatalog.CatalogEntry + +// artifactOf is the artifact name of an artifact reference `:` of module. +func artifactOf(module, ref string) (string, bool) { + m, a, ok := strings.Cut(ref, ":") + return a, ok && m == module && a != "" +} + +// NamesFrom answers a package's name in its ecosystem from the entries (design 47 G §348): the name the entry of module gives the +// artifact; known is false when no entry names one for it. +func NamesFrom(all Entries) PackageNames { + return func(_ context.Context, module, artifact string) (string, bool, error) { + for _, e := range all() { + if e.Module != module { + continue + } + for _, a := range e.Artifacts { + if name, ok := artifactOf(module, a.Ref); ok && name == artifact && a.Package != nil && a.Package.Name != "" { + return a.Package.Name, true, nil + } + } + } + return "", false, nil + } +} + +// CarriersFrom are the module artifacts a package's name in its ecosystem is the name of, as the entries hold them (design 47 G +// §572, §478), each distinct module and artifact once, sorted; a name no entry carries answers none. +func CarriersFrom(all Entries) func(ctx context.Context, name string) ([]Built, error) { + return func(_ context.Context, name string) ([]Built, error) { + seen := map[Built]bool{} + var out []Built + for _, e := range all() { + for _, a := range e.Artifacts { + artifact, ok := artifactOf(e.Module, a.Ref) + if !ok || a.Package == nil || a.Package.Name != name || seen[Built{e.Module, artifact}] { + continue + } + seen[Built{e.Module, artifact}] = true + out = append(out, Built{e.Module, artifact}) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].Module+"/"+out[i].Artifact < out[j].Module+"/"+out[j].Artifact }) + return out, nil + } +} + +// ModuleFrom is the module and artifact a package's name is built from, from the entries (CarriersFrom); more than one entry +// carrying it is an error naming them. +func ModuleFrom(all Entries) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { + return moduleOfCarriers(CarriersFrom(all)) +} + +// EntriesOf are the entries of every half the branch follower holds, the followed branches in the order they came. +func EntriesOf(branches *meshcatalogclient.Branches) Entries { + return func() []meshcatalog.CatalogEntry { + var out []meshcatalog.CatalogEntry + for _, br := range branches.Followed() { + if cat, ok := branches.Catalog(br); ok { + if half, ok := cat.View(); ok { + for _, e := range half.Entries { + out = append(out, e) + } + } + } + } + return out + } +} diff --git a/modules/mesh-delivery-builder/internal/versions/entrynames_test.go b/modules/mesh-delivery-builder/internal/versions/entrynames_test.go new file mode 100644 index 00000000..32097c78 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/versions/entrynames_test.go @@ -0,0 +1,55 @@ +package versions + +import ( + "context" + "reflect" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +func named(module, artifact, name string) meshcatalog.CatalogEntry { + return meshcatalog.CatalogEntry{Module: module, Artifacts: []meshcatalog.Artifact{ + {Ref: module + ":" + artifact, Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: name}}}} +} + +// TestPackageNamesAreReadFromTheCatalogHalfsEntries: a package's name in its ecosystem is the name the entry of its module gives the +// artifact (design 47 G §348, §676), the module and artifact a name is built from are the entries carrying it, and a name two +// entries carry is an error naming both (§491); no state of the indexer is read. +func TestPackageNamesAreReadFromTheCatalogHalfsEntries(t *testing.T) { + entries := func() []meshcatalog.CatalogEntry { + return []meshcatalog.CatalogEntry{named("mesh-sdk", "go", "git.novox.be/novox/mesh-sdk/go"), named("netcheck", "lib", "netcheck"), + named("twin-a", "lib", "twin"), named("twin-b", "lib", "twin"), {Module: "bare"}} + } + ctx := context.Background() + for _, c := range []struct { + module, artifact string + name string + known bool + }{ + {"mesh-sdk", "go", "git.novox.be/novox/mesh-sdk/go", true}, + {"netcheck", "lib", "netcheck", true}, + {"netcheck", "other", "", false}, + {"bare", "lib", "", false}, + {"unknown", "lib", "", false}, + } { + got, known, err := NamesFrom(entries)(ctx, c.module, c.artifact) + if err != nil || got != c.name || known != c.known { + t.Errorf("NamesFrom(%s/%s) = %q, %v, %v; want %q, %v", c.module, c.artifact, got, known, err, c.name, c.known) + } + } + module, artifact, found, err := ModuleFrom(entries)(ctx, "netcheck") + if module != "netcheck" || artifact != "lib" || !found || err != nil { + t.Errorf("ModuleFrom(netcheck) = %q, %q, %v, %v", module, artifact, found, err) + } + if _, _, found, err := ModuleFrom(entries)(ctx, "nobody"); found || err != nil { + t.Errorf("ModuleFrom(nobody) = %v, %v; want none", found, err) + } + if _, _, _, err := ModuleFrom(entries)(ctx, "twin"); err == nil { + t.Error("ModuleFrom(twin): two entries carry it, want an error") + } + got, _ := CarriersFrom(entries)(ctx, "twin") + if want := []Built{{"twin-a", "lib"}, {"twin-b", "lib"}}; !reflect.DeepEqual(got, want) { + t.Errorf("CarriersFrom(twin) = %v, want %v", got, want) + } +} diff --git a/modules/mesh-delivery-builder/internal/versions/names.go b/modules/mesh-delivery-builder/internal/versions/names.go index bb1881c0..a4b7e796 100644 --- a/modules/mesh-delivery-builder/internal/versions/names.go +++ b/modules/mesh-delivery-builder/internal/versions/names.go @@ -115,7 +115,11 @@ func Carriers(s PackagesState) func(ctx context.Context, name string) ([]Built, // ModuleOf is the module and artifact a package's name is built from (Carriers), found false for a name no entry carries; more // than one entry carrying it is an error naming them. func ModuleOf(s PackagesState) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { - carriers := Carriers(s) + return moduleOfCarriers(Carriers(s)) +} + +// moduleOfCarriers is ModuleOf over one way of finding the carriers. +func moduleOfCarriers(carriers func(ctx context.Context, name string) ([]Built, error)) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { return func(ctx context.Context, name string) (string, string, bool, error) { got, err := carriers(ctx, name) switch { diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 0a6d1dc5..075fe81e 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -128,9 +128,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 // 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no // other tool; it holds no capacity. // 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955). - // DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's assignments are asked by their - // read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is - // numbered under (design 32). + // DLV-M1 (ADR 0615 §1): the assigner's assignments are asked by their read tool and followed by their views family, never + // read; the catalog half (the modules, their manifests, their packages and the package names a package is numbered under) is + // read through the seat mesh-catalog's branch follower (design 47 G §348, §614), and the indexer's states not at all. for _, r := range m.Reads { // The toolchain gate follows the assigner's assignments through meshmoduleassignerclient (buildrun.AssignmentsBucket), so // it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools. @@ -140,11 +140,10 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 } // Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements. // The watchdogs seat's presence says a node's machine is heard (issue 1966, ADR 0596 §2, ADR 0668 §3). - if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", - "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions", + if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_requirements", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "seat:git.tag", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove", - "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // The builder hears the build seats' toolchain-reported and the node-engine seat's running (design 47 G §492, §546) and reads no @@ -157,10 +156,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 "go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed", "mesh-artifact-sweep.artifacts-gone", "mesh-catalog.manifest-accepted", "mesh-catalog.version-released", "mesh-catalog.versions-filled", "mesh-conditions.condition-cleared", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-forge-bridge.forge.**", - "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.packages", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running", + "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running", "npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed", - "oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + "oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed"}; !reflect.DeepEqual(sorted(m.Consumes), sorted(want)) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" { diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 8995002a..6f0808a6 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -68,10 +68,9 @@ "oci-artifact-registry.refused", "oci-artifact-registry.removed", "mesh-delivery-assigner.views.assignments", - "mesh-delivery-indexer.views.candidates", - "mesh-delivery-indexer.views.manifests", - "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.packages" + "mesh-catalog.catalog-revision", + "mesh-catalog.registration-changed", + "mesh-delivery.source-moved" ], "invokes": [ "mesh-delivery-forge-bridge.bridge_tags", @@ -95,10 +94,10 @@ "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions", "mesh-delivery-assigner.assigner_read_assignments", - "mesh-delivery-indexer.indexer_read_candidates", - "mesh-delivery-indexer.indexer_read_manifests", - "mesh-delivery-indexer.indexer_read_modules", - "mesh-delivery-indexer.indexer_read_packages" + "seat:mesh-catalog.entry", + "seat:mesh-catalog.registrations", + "seat:mesh-catalog.revision", + "seat:mesh-delivery.source" ], "state": [ { "name": "builds", "history": 5 },