From a6fdb121fe4ded3bb792a37b2eda17daff49d155 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 14:13:28 +0100 Subject: [PATCH] DLV-M2: the checker follows the indexer's candidates, modules, manifests, packages and predefined keys and the assigner's placement by first views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0615 §1 and ADR 0596: the checker invokes the six read tools, consumes their views families and reads none of the six buckets; this ends its direct read of the packages state (the lead's ruling 2). It keeps first-views and findings and announces both as its own. The lead's ruling: an owner that is down never holds the start; the checks, the tries and the hand asks start once every first view is answered, the placement is followed into the toolchain gate once its first view is. firstview.Reader opens a followed state's picture as a process.BucketOpener (the builder uses it too). Tools checker_first_views and checker_first_view_again. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../cmd/mesh-delivery-builder/main.go | 12 +- .../cmd/mesh-delivery-checker-tools/main.go | 4 + .../mesh-delivery-checker/integration_test.go | 11 +- .../cmd/mesh-delivery-checker/judge.go | 8 +- .../cmd/mesh-delivery-checker/main.go | 212 +++++++++++------- .../mesh-delivery-checker/manifest_test.go | 20 +- modules/mesh-delivery-checker/module.json | 24 +- sdk/firstview/firstview.go | 10 + 8 files changed, 192 insertions(+), 109 deletions(-) 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 f0d2308f..b9b21eac 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -178,7 +178,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return case <-placement.Follower().Ready(): } - pe, err := buildrun.FollowPlacement(ctx, pictures{placement}, gate, b.NodesChanged, logger.Printf) + pe, err := buildrun.FollowPlacement(ctx, reader, gate, b.NodesChanged, logger.Printf) if err != nil { placementErrs <- err return @@ -270,16 +270,6 @@ const ( candidatesState = "candidates" ) -// pictures opens the placement's picture where the gate's follower opens its bucket. -type pictures struct{ placement *firstview.Picture } - -func (p pictures) KeyValue(ctx context.Context, bucket string) (jetstream.KeyValue, error) { - if bucket != p.placement.Bucket() { - return nil, fmt.Errorf("the builder reads %s by its owner's first view alone", bucket) - } - return p.placement, nil -} - // 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 *firstview.Picture) error { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index fd39fe86..f01e94b0 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -13,6 +13,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/views" "mesh-delivery-checker/internal/check" ) @@ -326,6 +327,9 @@ func main() { return err }} tools := append(append(append(Tools(r), TryTools(trying)...), SeatTryTools(trying)...), RecoveryTools(r, written)...) + // checker_first_views and checker_first_view_again: where the checker's first views of the indexer's and the assigner's + // states stand, and asking one again by hand (ADR 0615, DLV-M2). + tools = append(tools, views.StatusTools(delivery.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), time.Now)...) if err := stdio.Serve("", logged(tools)); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err) os.Exit(1) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index 719773f5..6e804bf2 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -28,6 +28,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-sdk/go/announce" + "git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -440,6 +441,12 @@ func startBed(t *testing.T) *bed { } b.buckets[name] = kv } + // The indexer and the assigner as the owners of the states the checker follows: their read tools and views families + // (ADR 0615, DLV-M2). + for _, st := range []string{candidatesState, modulesState, manifestsState, delivery.PackagesState, delivery.IndexerPredefinedState} { + firstviewtest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, "n1_"+delivery.CheckerModule) + } + firstviewtest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, "n1_"+delivery.CheckerModule) // 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone // judges the repository (an unknown repository is judged by both, issue 1887 A12). known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"}) @@ -1071,7 +1078,7 @@ func TestTheCheckerSaysOwedReadableOnTheBus(t *testing.T) { // R2-1 } if said.Module != "mesh-delivery-checker" || said.Node != "n1" || !said.Keeps("owed") || !said.Readable("owed") || !said.Readable("previews") || !said.Readable("verdicts") || !said.Readable("kept") || !said.Readable("tries") || said.Readable("hearing") || - !said.Keeps("asks") || said.Readable("asks") || len(said.States) != 7 { - t.Fatalf("the checker said %+v, want its seven states, asks and hearing its own, with kept, owed, previews, tries and verdicts readable, tries by the delivery view (1929/AC4; 1887 A5b; build_usage)", said) + !said.Keeps("asks") || said.Readable("asks") || !said.Keeps("first-views") || said.Readable("first-views") || !said.Keeps("findings") || len(said.States) != 9 { + t.Fatalf("the checker said %+v, want its nine states, asks, hearing and first-views its own (DLV-M2), with kept, owed, previews, tries and verdicts readable, tries by the delivery view (1929/AC4; 1887 A5b; build_usage)", said) } } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index 4b1974e3..acd86ce7 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -51,14 +51,10 @@ type judgeDeps struct { } // keptPredefined reads the indexer's key predefined.list (delivery.PredefinedKept), bound once its state appears. -type keptPredefined struct{ b *process.AwaitedBucket } +type keptPredefined struct{ kv jetstream.KeyValue } func (k keptPredefined) PredefinedList(ctx context.Context) (json.RawMessage, error) { - kv := k.b.KV() - if kv == nil { - return nil, process.ErrNotYet - } - e, err := kv.Get(ctx, delivery.PredefinedListKey) + e, err := k.kv.Get(ctx, delivery.PredefinedListKey) if errors.Is(err, jetstream.ErrKeyNotFound) { return nil, nil } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index b2c409f0..eda2e94f 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -24,6 +24,9 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/views" + + "git.novox.be/novox/mesh-delivery/sdk/firstview" "mesh-delivery-checker/internal/check" ) @@ -61,6 +64,20 @@ func durationEnv(getenv func(string) string, name string, def time.Duration) (ti return d, nil } +// The checker's own states of its first views (views.StatusState) and of its findings, where the condition keeper raises a +// first view unanswered past its bound (ADR 0615 §6); and the indexer's states it follows (delivery.CandidatesBucket, +// ModulesBucket, ManifestsBucket). +var ( + firstViewsBucket = delivery.CheckerModule + "_" + views.StatusState + findingsBucket = delivery.CheckerModule + "_findings" +) + +const ( + candidatesState = "candidates" + modulesState = "modules" + manifestsState = "manifests" +) + // run is the program: it returns when ctx ends or the bus cannot be used. // keptStates is every state the checker keeps and which others may read (ADR 0495 §2-§3): owed by the forge module, // which writes mesh/check from it (ADR 0521), and by the forge bridge; previews by the indexer; verdicts by the gate; tries by the delivery view. @@ -77,6 +94,10 @@ var keptStates = []announce.State{ // The tries are read by the delivery view, which tells a try's asks from a candidate's by the asks each names (build_usage). {Name: "tries", Readable: true}, {Name: "verdicts", Readable: true}, + // Where its first views of the indexer's and the assigner's states stand (read by checker_first_views alone), and its + // findings of one unanswered past its bound, which the condition keeper observes as it does every reader's (ADR 0615 §6). + {Name: views.StatusState}, + {Name: "findings"}, } func run(ctx context.Context, getenv func(string) string, logOut io.Writer) error { @@ -90,11 +111,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return err } defer conn.Close() - // Its own states, the candidates it checks, and the modules and manifests that say which build modules judge a repository: - // a check without one of them would be wrong or unwritten, so each stops the start, naming its grant (issue 1945). + // Its own states: a check without one of them would be unwritten, so each stops the start, naming its grant (issue 1945). buckets := map[string]jetstream.KeyValue{} for _, name := range []string{delivery.CheckerVerdictsBucket, delivery.CheckerKeptBucket, delivery.CheckerOwedBucket, delivery.PreviewsBucket, - delivery.CandidatesBucket, delivery.ModulesBucket, delivery.ManifestsBucket, check.TriesBucket} { + check.TriesBucket} { if buckets[name], err = process.EssentialBucket(ctx, js, "checker", name, "judge or keep a check"); err != nil { return err } @@ -111,6 +131,19 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := caller.Keeps(keptStates...); err != nil { logger.Printf("what the checker keeps could not be said on the bus, so readers of owed, previews and verdicts are not granted until it is: %v", err) } + // The candidates it checks, the modules and manifests that say which build modules judge a repository, the published + // packages, the predefined keys and the placement are their owners' states (the indexer's, the assigner's), read by the + // first view of their read tools and then their views families, never their buckets (ADR 0615 §1, ADR 0596, DLV-M2). + // An owner that is down never holds the start: the checks start once every first view is answered, and until then the + // unanswered first view is the checker's finding. + reader := &firstview.Reader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf} + reader.Findings = process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf).KV + candidates := reader.Follow(delivery.IndexerModule, candidatesState) + modules := reader.Follow(delivery.IndexerModule, modulesState) + manifests := reader.Follow(delivery.IndexerModule, manifestsState) + packages := reader.Follow(delivery.IndexerModule, delivery.PackagesState) + predefined := reader.Follow(delivery.IndexerModule, delivery.IndexerPredefinedState) + placement := reader.Follow(delivery.AssignerModule, delivery.PlacementState) stepBound, err := durationEnv(getenv, "MESH_CHECKER_STEP_BOUND", check.DefaultStepBound) if err != nil { @@ -183,7 +216,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro verdicts := &manifestVerdicts{} compositions := check.EngineCompositions{ Asker: engineAsks{call: caller.CallOn}, - Placed: placedNodes{kv: func(at context.Context) (jetstream.KeyValue, error) { return js.KeyValue(at, delivery.PlacementBucket) }}.Placed, + Placed: placedNodes{kv: func(context.Context) (jetstream.KeyValue, error) { return placement, nil }}.Placed, Bound: answerBound, Sandbox: seatSandbox{asker: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}}, Verdicts: verdicts.all, @@ -196,23 +229,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro logger.Printf("%s of %s was written and the part that reads it could not be told: %v", event, key, err) } } - repos := &repoModules{manifests: kvStore{buckets[delivery.ManifestsBucket]}, logf: logger.Printf, verdicts: verdicts} + repos := &repoModules{manifests: kvStore{manifests}, logf: logger.Printf, verdicts: verdicts} compositions.Composer = func() string { return repos.CommitOf(check.NodeEngineModule) } compositions.Manifests = repos - // The indexer's packages state (ADR 0593 §2): followed once the bus has it, so a Go check builds every mesh package at - // its published version; until then a check builds each at the version its go.mod names. + // The indexer's packages state (ADR 0593 §2), so a Go check builds every mesh package at its published version: read + // once the first views are answered, before any check starts (afterFirstViews). published := &publishedPackages{} - process.AwaitBucket(ctx, js, conn, delivery.PackagesBucket, "a Go check builds each mesh package at the version its go.mod names", - logger.Printf).Ready(func(kv jetstream.KeyValue) { - w, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) { published.Put(e.Key(), e.Value(), !opOf(e)) }) - if err != nil { - logger.Printf("%s cannot be watched, so a Go check builds each mesh package at the version its go.mod names: %v", delivery.PackagesBucket, err) - return - } - go func() { - _ = follow(ctx, w, func(e jetstream.KeyValueEntry) { published.Put(e.Key(), e.Value(), !opOf(e)) }) - }() - }) ch.Runs = repos // the node-engine a check reads beside at its running commit (ADR 0548 §6) // A group's tagged versions (design 51 *Tagged versions*): tag-wanted on the checker's stream asks, and the package // registry seats' published heard on the checker's durable consumer of the shared events stream. Where that consumer is @@ -239,9 +261,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, repos: repos, published: published, builds: builds, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf, announce: announce, compositions: compositions, tags: tags, - nodes: openNodeNames(ctx, js, logger.Printf), - predefined: keptPredefined{process.AwaitBucket(ctx, js, conn, delivery.IndexerModule+"_"+delivery.IndexerPredefinedState, - "a change's manifests are judged with no predefined keys, each fallback as its value declares it", logger.Printf)}, + nodes: openNodeNames(ctx, js, logger.Printf), + predefined: keptPredefined{predefined}, }) ch.Announce = announce tryer.Judge = ch.Judge @@ -252,10 +273,21 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // placement is settled first, so the toolchains settle through the gate already knowing it. gate := buildrun.NewToolchainGate([]string{buildrun.GoBuildModule, buildrun.NodeBuildModule}, tools.Put, logger.Printf) watch.put = gate.Toolchain - placementErrs, err := buildrun.FollowPlacement(ctx, js, gate, ch.NodesChanged, logger.Printf) - if err != nil { - return err - } + // Until the placement's first view is answered the gate passes every report on, as before the placement is settled. + placementErrs := make(chan error, 1) + go func() { + select { + case <-ctx.Done(): + return + case <-placement.Follower().Ready(): + } + pe, err := buildrun.FollowPlacement(ctx, reader, gate, ch.NodesChanged, logger.Printf) + if err != nil { + placementErrs <- err + return + } + placementErrs <- <-pe + }() if err := watch.start(); err != nil { return err } @@ -338,65 +370,89 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return nil } lazy.ensure() - // The modules say which repository holds a TypeScript module, so the Node build module judges it too. - mw, err := settle(ctx, buckets[delivery.ModulesBucket], func(e jetstream.KeyValueEntry) { repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) }) - if err != nil { - return err - } - go func() { - errs <- follow(ctx, mw, func(e jetstream.KeyValueEntry) { - before := repos.CommitOf(check.NodeEngineModule) - repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) - if after := repos.CommitOf(check.NodeEngineModule); after != before { - ch.TrunkMoved(ctx) // the node-engine the mesh runs moved: a verdict that read it beside is carried (ADR 0548 §6) - } - }) - }() - each := func(e jetstream.KeyValueEntry) { - if opOf(e) { - ch.Candidate(ctx, e.Key(), e.Value()) - } else { - ch.Forget(e.Key()) + afterFirstViews := func() error { + // The modules say which repository holds a TypeScript module, so the Node build module judges it too. + pw, err := settle(ctx, packages, func(e jetstream.KeyValueEntry) { published.Put(e.Key(), e.Value(), !opOf(e)) }) + if err != nil { + return err } - } - w, err := settle(ctx, buckets[delivery.CandidatesBucket], each) - if err != nil { - return err - } - // A key left running for heads no candidate holds is closed; the candidates are all known now. - if err := ch.Sweep(ctx); err != nil { - return err - } - // The tries: a queued one starts, one left running is taken over, an ended one past its keeping is removed. - tw, err := settle(ctx, buckets[check.TriesBucket], func(jetstream.KeyValueEntry) {}) - if err != nil { - return err - } - tryer.Reconsider(ctx) - go func() { - errs <- follow(ctx, tw, func(e jetstream.KeyValueEntry) { + go func() { + errs <- follow(ctx, pw, func(e jetstream.KeyValueEntry) { published.Put(e.Key(), e.Value(), !opOf(e)) }) + }() + mw, err := settle(ctx, modules, func(e jetstream.KeyValueEntry) { repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) }) + if err != nil { + return err + } + go func() { + errs <- follow(ctx, mw, func(e jetstream.KeyValueEntry) { + before := repos.CommitOf(check.NodeEngineModule) + repos.Put(ctx, e.Key(), e.Value(), !opOf(e)) + if after := repos.CommitOf(check.NodeEngineModule); after != before { + ch.TrunkMoved(ctx) // the node-engine the mesh runs moved: a verdict that read it beside is carried (ADR 0548 §6) + } + }) + }() + each := func(e jetstream.KeyValueEntry) { if opOf(e) { - tryer.Put(ctx, e.Key(), e.Value()) - } - }) - }() - // The checker's own verdicts, updates only, in one watch: one checker_ask_again wrote asks its check again - // (Checker.VerdictWritten); a deleted key of heads a candidate holds is judged again (Checker.VerdictDeleted, issue 1733). - vw, err := buckets[delivery.CheckerVerdictsBucket].WatchAll(ctx, jetstream.UpdatesOnly()) - if err != nil { - return fmt.Errorf("the checker's verdicts cannot be watched, so no check can be asked again by hand and a deleted verdict would never be written again: %w", err) - } - go func() { - errs <- follow(ctx, vw, func(e jetstream.KeyValueEntry) { - if opOf(e) { - ch.VerdictWritten(ctx, e.Key(), e.Value()) + ch.Candidate(ctx, e.Key(), e.Value()) } else { - ch.VerdictDeleted(ctx, e.Key()) + ch.Forget(e.Key()) } - }) + } + w, err := settle(ctx, candidates, each) + if err != nil { + return err + } + // A key left running for heads no candidate holds is closed; the candidates are all known now. + if err := ch.Sweep(ctx); err != nil { + return err + } + // The tries: a queued one starts, one left running is taken over, an ended one past its keeping is removed. + tw, err := settle(ctx, buckets[check.TriesBucket], func(jetstream.KeyValueEntry) {}) + if err != nil { + return err + } + tryer.Reconsider(ctx) + go func() { + errs <- follow(ctx, tw, func(e jetstream.KeyValueEntry) { + if opOf(e) { + tryer.Put(ctx, e.Key(), e.Value()) + } + }) + }() + // The checker's own verdicts, updates only, in one watch: one checker_ask_again wrote asks its check again + // (Checker.VerdictWritten); a deleted key of heads a candidate holds is judged again (Checker.VerdictDeleted, issue 1733). + vw, err := buckets[delivery.CheckerVerdictsBucket].WatchAll(ctx, jetstream.UpdatesOnly()) + if err != nil { + return fmt.Errorf("the checker's verdicts cannot be watched, so no check can be asked again by hand and a deleted verdict would never be written again: %w", err) + } + go func() { + errs <- follow(ctx, vw, func(e jetstream.KeyValueEntry) { + if opOf(e) { + ch.VerdictWritten(ctx, e.Key(), e.Value()) + } else { + ch.VerdictDeleted(ctx, e.Key()) + } + }) + }() + go func() { errs <- follow(ctx, w, each) }() + return nil + } + reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "checker_first_views reads nothing and checker_first_view_again is not heard", logger.Printf)) + // The checks, the tries and the hand asks, once every first view is answered (the lead's ruling for DLV-M2): a check + // judged without the candidates, the modules, the manifests, the published packages, the predefined keys or the + // placement would be another check. + go func() { + select { + case <-ctx.Done(): + return + case <-reader.Ready(): + } + if err := afterFirstViews(); err != nil { + errs <- err + } }() logger.Printf("started on %s", cred.Node) - go func() { errs <- follow(ctx, w, each) }() select { case <-ctx.Done(): ch.Wait() diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index c70b5bc6..41491b4e 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -102,19 +102,27 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 t.Errorf("module = %q", m.Module) } // delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets. - if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) { + if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains", - "mesh-delivery-assigner.placement", "mesh-delivery-builder.builds", "mesh-delivery-gate.source", "mesh-delivery-gate.turns", "mesh-delivery-indexer.candidates", - "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.packages", "mesh-delivery-indexer.predefined", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) { + "mesh-delivery-builder.builds", "mesh-delivery-gate.source", "mesh-delivery-gate.turns", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } + // DLV-M2 (ADR 0615 §1, ADR 0596): the indexer's candidates, modules, manifests, packages and predefined keys and the + // assigner's placement are asked by their read tools and followed by their views families, never read. + for _, r := range m.Reads { + if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") { + t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r) + } + } // 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no // build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4). // The forge's reads for a mesh-sdk pin (ADR 0484 clause 9), and of the node seat node-engine its one verb // compose-candidate, asked of every placed node (ADR 0593 §7), never the seat's other verbs. if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches", + "mesh-delivery-assigner.assigner_read_placement", "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_read_predefined", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes) } @@ -123,7 +131,9 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 } // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. // And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*). - if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", + "mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", + "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), 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" { @@ -141,7 +151,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 // The read tools (issue 1686) are served by the tools bundle, not by the checker's process. // The try's tools (ADR 0501 §5; the end by hand, issue 734) beside them: mesh-controller's try calls them. A lost check // is asked again by hand (checker_ask_again). - if want := []string{"checker_ask_again", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) { + if want := []string{"checker_ask_again", "checker_first_view_again", "checker_first_views", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) { t.Errorf("tools = %v, want %v", m.Tools, want) } // The try is the checker's own seat's verbs (ADR 0547 §10, issue 774), served beside the tools by the same code. diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index 8c054afd..df8cfd0c 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -17,6 +17,12 @@ "go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", + "mesh-delivery-assigner.views.placement", + "mesh-delivery-indexer.views.candidates", + "mesh-delivery-indexer.views.manifests", + "mesh-delivery-indexer.views.modules", + "mesh-delivery-indexer.views.packages", + "mesh-delivery-indexer.views.predefined", "npm-package-registry.published" ], "streams": [ @@ -30,22 +36,18 @@ { "name": "owed", "history": 5 }, { "name": "previews", "history": 5 }, { "name": "tries", "history": 1 }, - { "name": "verdicts", "history": 5 } + { "name": "verdicts", "history": 5 }, + { "name": "first-views", "history": 1 }, + { "name": "findings", "history": 1 } ], "reads": [ "mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains", - "mesh-delivery-assigner.placement", "mesh-delivery-builder.builds", "mesh-delivery-gate.source", "mesh-delivery-gate.turns", - "mesh-delivery-indexer.candidates", - "mesh-delivery-indexer.manifests", - "mesh-delivery-indexer.modules", - "mesh-delivery-indexer.packages", - "mesh-delivery-indexer.predefined", "mesh-delivery-watchman.presence", "node-engine.have" ], @@ -59,6 +61,8 @@ "checker_previews", "checker_pull", "checker_status", + "checker_first_views", + "checker_first_view_again", "checker_try", "checker_try_end", "checker_try_read", @@ -69,6 +73,12 @@ "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches", + "mesh-delivery-assigner.assigner_read_placement", + "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_read_predefined", "seat:node-engine.compose-candidate" ], diff --git a/sdk/firstview/firstview.go b/sdk/firstview/firstview.go index 57d82e26..3795dca0 100644 --- a/sdk/firstview/firstview.go +++ b/sdk/firstview/firstview.go @@ -105,6 +105,16 @@ func (r *Reader) Picture(bucket string) *Picture { return r.pics[bucket] } +// KeyValue opens a followed state's picture by its bus name, where a process opened the bucket (process.BucketOpener, +// as buildrun.FollowPlacement takes it); any other bucket is refused, for the reader reads its owners' states by their +// first views alone. +func (r *Reader) KeyValue(_ context.Context, bucket string) (jetstream.KeyValue, error) { + if p := r.Picture(bucket); p != nil { + return p, nil + } + return nil, fmt.Errorf("%s reads %s by its owner's first view alone, and does not follow it", r.Module, bucket) +} + // Followers are the followers, in the order they were added. func (r *Reader) Followers() []*views.Follower { r.mu.Lock()