From 095e23ecdac122f20af5b16e979fc29fff147c97 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:21:31 +0100 Subject: [PATCH] W16.4: mesh-self-check reads which modules exist and their manifests from the catalog half through meshcatalogclient's branch follower (milestone 47/G) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §196, §594, §614; lead answers A-5, A-15 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-self-check/catalogfeed.go | 53 ++++++++++ .../cmd/mesh-self-check/catalogfeed_test.go | 98 +++++++++++++++++++ .../cmd/mesh-self-check/main.go | 14 ++- .../cmd/mesh-self-check/probes.go | 17 +++- .../cmd/mesh-self-check/probes_data.go | 36 +++---- .../cmd/mesh-self-check/probes_test.go | 16 +-- .../cmd/mesh-self-check/selfcheck.go | 2 + .../cmd/mesh-self-check/selfcheck_test.go | 7 ++ modules/mesh-self-check/module.json | 10 +- 9 files changed, 211 insertions(+), 42 deletions(-) create mode 100644 modules/mesh-self-check/cmd/mesh-self-check/catalogfeed.go create mode 100644 modules/mesh-self-check/cmd/mesh-self-check/catalogfeed_test.go diff --git a/modules/mesh-self-check/cmd/mesh-self-check/catalogfeed.go b/modules/mesh-self-check/cmd/mesh-self-check/catalogfeed.go new file mode 100644 index 000000000..43f4777cb --- /dev/null +++ b/modules/mesh-self-check/cmd/mesh-self-check/catalogfeed.go @@ -0,0 +1,53 @@ +package main + +// The self-check's reading of which modules exist (design 47 G §196, §594, §614): the catalog half, taken through +// meshcatalogclient's follower of the watched branches on the part's one views reader. A module's manifest is the one its +// catalog graph entry carries whole (§443), never the indexer's state `modules` or `manifests`. + +import ( + "fmt" + "strings" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// CatalogHalf is every module's entry in the catalog half, as the self-check holds it. +type CatalogHalf interface { + // Modules is each module's entry; with a branch that has no first view yet it is an error naming it, never no module. + Modules() (map[string]catalog.CatalogEntry, error) +} + +// branchesHalf is the CatalogHalf over the follower of the watched branches. +type branchesHalf struct{ b *meshcatalogclient.Branches } + +func (h branchesHalf) Modules() (map[string]catalog.CatalogEntry, error) { + followed := h.b.Followed() + if len(followed) == 0 { + return nil, fmt.Errorf("the catalog half names no watched branch yet (the seat mesh-catalog's registrations are asked until they answer)") + } + out := map[string]catalog.CatalogEntry{} + var waiting []string + for _, branch := range followed { + c, ok := h.b.Catalog(branch) + if !ok { + continue + } + select { + case <-c.Ready(): + default: + waiting = append(waiting, branch) + continue + } + half, _ := c.View() // a branch at none-yet is ready and empty (§614) + for m, e := range half.Entries { + if _, ok := out[m]; !ok { // the branch followed first names a module both hold + out[m] = e + } + } + } + if len(waiting) > 0 { + return nil, fmt.Errorf("the catalog half has no first view of %s yet", strings.Join(waiting, ", ")) + } + return out, nil +} diff --git a/modules/mesh-self-check/cmd/mesh-self-check/catalogfeed_test.go b/modules/mesh-self-check/cmd/mesh-self-check/catalogfeed_test.go new file mode 100644 index 000000000..d045883d8 --- /dev/null +++ b/modules/mesh-self-check/cmd/mesh-self-check/catalogfeed_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + "testing" + "time" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" + "git.novox.be/novox/mesh-sdk/go/views" +) + +// wiringOf is a part's reader in miniature: it keeps the followers adopted, so a test starts them as the reader's Start does. +type wiringOf struct { + mu sync.Mutex + fs []*views.Follower +} + +func (w *wiringOf) Adopt(f *views.Follower, _, _ string, _ func(context.Context, []byte) error) error { + w.mu.Lock() + defer w.mu.Unlock() + w.fs = append(w.fs, f) + return nil +} +func (w *wiringOf) Drop(string, string) error { return nil } + +// Design 47 G §196, §594: which modules exist, and the manifest of each, are the catalog half's; a branch with no first +// view yet is an error naming it, never no module, and an entry's manifest is the one it carries whole. +func TestCatalogHalfIsTheWatchedBranchesNewestRevision(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + answered := false + var mu sync.Mutex + call := func(key string, _ any) (json.RawMessage, error) { + mu.Lock() + defer mu.Unlock() + switch key { + case meshcatalogclient.RegistrationsVerb: + return json.Marshal(catalog.RegistrationsAnswer{Registrations: []catalog.Registration{{Module: "m", Version: 1, Branches: []string{"main"}}}}) + case meshcatalogclient.SourceVerb: + return json.Marshal(delivery.SourceAnswer{}) + case meshcatalogclient.RevisionVerb: + if !answered { + return nil, fmt.Errorf("the holder is away") + } + return json.Marshal(catalog.RevisionAnswer{Revision: &catalog.CatalogRevision{Revision: 1, Kind: meshcatalogclient.KindTrunk}, + Entries: []catalog.CatalogEntry{{Module: "pg", Manifest: map[string]any{"module": "pg"}}}}) + } + return nil, fmt.Errorf("asked %s", key) + } + w := &wiringOf{} + b, err := meshcatalogclient.NewBranches(w, call) + if err != nil { + t.Fatal(err) + } + h := branchesHalf{b} + if _, err := h.Modules(); err == nil || !strings.Contains(err.Error(), "no watched branch") { + t.Fatalf("before the registrations: %v, want an error naming the missing branch", err) + } + go func() { _ = w.fs[0].Start(ctx) }() // the registrations + for len(b.Followed()) == 0 { + select { + case <-ctx.Done(): + t.Fatal("the registrations' branch is never followed") + case <-time.After(10 * time.Millisecond): + } + } + if _, err := h.Modules(); err == nil || !strings.Contains(err.Error(), "no first view of main") { + t.Fatalf("with no first view: %v, want an error naming the branch, never no module", err) + } + mu.Lock() + answered = true + mu.Unlock() + c, _ := b.Catalog("main") + if err := c.Start(ctx); err != nil { + t.Fatal(err) + } + got, err := h.Modules() + if err != nil || len(got) != 1 || got["pg"].Manifest["module"] != "pg" { + t.Fatalf("with the first view: %v, %v", got, err) + } +} + +// D13 reads each manifest from the catalog entry that carries it, and a catalog half without a view is an error, never a +// mesh with no module declaring data. +func TestD13ReadsTheCatalogHalf(t *testing.T) { + mesh := dataMesh(func(*dataRig) {}) + mesh.modulesErr = fmt.Errorf("the catalog half has no first view of main yet") + c := &Checker{Store: mesh, Catalog: mesh, Now: func() time.Time { return dataNow }} + if _, err := probeData(context.Background(), c); err == nil || !strings.Contains(err.Error(), "no first view") { + t.Fatalf("D13 with no catalog view: %v", err) + } +} diff --git a/modules/mesh-self-check/cmd/mesh-self-check/main.go b/modules/mesh-self-check/cmd/mesh-self-check/main.go index 75ef37b47..fb0ba73f1 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/main.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/main.go @@ -18,6 +18,7 @@ import ( "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -76,7 +77,16 @@ func NewChecker() *Checker { os.Exit(1) } c.Writer = w - c.Store = runtimeStore{views: newViews(w)} + v := newViews(w) + // Which modules exist is the catalog half's, followed per watched branch by the seat's client (design 47 G §196, §614). + b, err := meshcatalogclient.FollowBranches(v, func(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) }) + if err != nil { + logf("%v", err) + os.Exit(1) + } + b.Logf = logf + c.Store = runtimeStore{views: v} + c.Catalog = branchesHalf{b} return c } @@ -87,8 +97,6 @@ func newViews(w *FindingsWriter) *views.Reader { r := &views.Reader{Module: Module, Logf: logf, Findings: w, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}} r.Follow(assignerModule, placing.StateAssignments, AssignmentsTool) r.Follow(assignerModule, placing.StateSeatHolders, SeatHoldersTool) - r.Follow(indexerModule, "modules", ModulesTool) - r.Follow(indexerModule, "manifests", ManifestsTool) return r } diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes.go b/modules/mesh-self-check/cmd/mesh-self-check/probes.go index 39a5614bc..6e944eea3 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes.go @@ -34,12 +34,13 @@ const ( // Invokes are the tools and seat verbs the probes ask, and Reads the states they read: the manifest's `invokes` and // `reads`, which its test holds to these. var ( - Invokes = []string{StreamsTool, ClosedTool, SignalsVerb, BannedVerb, AssignmentsTool, SeatHoldersTool, ModulesTool, ManifestsTool, HandActVerb} - Reads = []string{PresenceRead, HealthRead, ReadingsRead, GoodRead} + Invokes = []string{StreamsTool, ClosedTool, SignalsVerb, BannedVerb, AssignmentsTool, SeatHoldersTool, + RevisionVerb, RegistrationsVerb, SourceVerb, HandActVerb} + Reads = []string{PresenceRead, HealthRead, ReadingsRead, GoodRead} // Consumes are the views families the probes follow (ADR 0615): the indexer's and the assigner's states are asked // once, by their owners' read tools, and then followed by their events, never read. Consumes = []string{"mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.seat-holders", - "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests"} + "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved"} ) // HandActVerb records a re-read of a first view by hand (ADR 0615 §6). @@ -49,8 +50,14 @@ const HandActVerb = "seat:mesh-hand-acts.hand-act" const ( AssignmentsTool = "mesh-delivery-assigner.assigner_read_assignments" SeatHoldersTool = "mesh-delivery-assigner.assigner_read_seat_holders" - ModulesTool = "mesh-delivery-indexer.indexer_read_modules" - ManifestsTool = "mesh-delivery-indexer.indexer_read_manifests" +) + +// The seat verbs the follower of the watched branches asks (design 47 G §614): the catalog half's first view, the +// registrations' watched branches and, before the first registrations, the branches the gate's source answers. +const ( + RevisionVerb = "seat:mesh-catalog.revision" + RegistrationsVerb = "seat:mesh-catalog.registrations" + SourceVerb = "seat:mesh-delivery.source" ) // Registry is the self-check's registry, in to-be 45's order. **The registry is the design's live form**: a probe diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go b/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go index 42ce1fd4a..240b2ebe8 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go @@ -4,7 +4,7 @@ package main // declares is measured, is there, holds what it held, and is backed up within its bound. The controller composed what // every machine declares and asked each machine's backup holder what it measured; the facts now live on the bus: the // readings disk-load keeps (its state `readings`, with the item's seven-day peak), the last good night per item restic -// keeps (its state `good`), and the manifests the indexer holds (for the bounds an item declares). Each kind, key and +// keeps (its state `good`), and the manifests the catalog half's entries carry (for the bounds an item declares). Each kind, key and // word is the controller's (mesh-controller cmd/mesh-controller/data.go), so the condition the keeper raises from it is // the one the controller raised. @@ -27,10 +27,8 @@ import ( // The states D13 reads (declared under `reads`). const ( - ReadingsRead = diskreadings.ReadingsRead - GoodRead = backupgood.GoodRead - ModulesRead = indexerModule + ".modules" - ManifestsRead = indexerModule + ".manifests" + ReadingsRead = diskreadings.ReadingsRead + GoodRead = backupgood.GoodRead ) // The kinds D13 raises. @@ -63,7 +61,7 @@ type dataFacts struct { // keptStates are the states D13 judges from memory (issue 1952): read one key at a time through the runtime, each // read reopening its bucket, a run of D13 never finished within its bound. Each is watched once and kept as it changes. -var keptStates = []string{ReadingsRead, GoodRead, ModulesRead, ManifestsRead} +var keptStates = []string{ReadingsRead, GoodRead} // view is one state kept in memory from its watch: ready once the watch handed over every current value, err when it // was refused (then the next read asks again). @@ -152,26 +150,18 @@ func (c *Checker) dataFacts(ctx context.Context) (dataFacts, error) { f.good[backupgood.GoodKey(g.Node, g.Module, g.Item)] = g.LastGood } } - for name, raw := range states[ModulesRead] { - // Only the manifest's digest of the indexer's module, and the manifest's text: what the bounds are read from. - var m struct { - Manifest string `json:"manifest"` - } - if json.Unmarshal(raw, &m) != nil || m.Manifest == "" { - continue - } - mraw, ok := states[ManifestsRead][m.Manifest] - if !ok { - continue - } - var doc struct { - JSON string `json:"manifest"` - } - if json.Unmarshal(mraw, &doc) != nil { + // Each module's manifest is the one its catalog graph entry carries whole (design 47 G §594). + modules, err := c.Catalog.Modules() + if err != nil { + return f, err + } + for name, e := range modules { + raw, err := json.Marshal(e.Manifest) + if err != nil || len(e.Manifest) == 0 { continue } var man catalogue.Manifest - if json.Unmarshal([]byte(doc.JSON), &man) == nil { + if json.Unmarshal(raw, &man) == nil { f.manifests[name] = man } } diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go b/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go index b013e3478..8fd8d6906 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go @@ -15,7 +15,6 @@ import ( nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" watchdogsclient "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient" - catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings" @@ -214,7 +213,7 @@ func TestTheBusProbes(t *testing.T) { agents(map[string]string{"novox": "old"})}, } for _, tc := range cases { - c := &Checker{Ask: tc.bus.ask, Store: tc.mesh, Now: func() time.Time { return dataNow }} + c := &Checker{Ask: tc.bus.ask, Store: tc.mesh, Catalog: tc.mesh, Now: func() time.Time { return dataNow }} if tc.mesh == nil { c.Store = newStore() } @@ -222,7 +221,7 @@ func TestTheBusProbes(t *testing.T) { // D13 judges the states it reads from memory, kept by watching them, never one key at a time through the runtime, // which did not finish within its bound (issue 1952). if strings.HasPrefix(tc.name, "D13") { - for _, s := range []string{ReadingsRead, GoodRead, ModulesRead, ManifestsRead} { + for _, s := range []string{ReadingsRead, GoodRead} { if n := tc.mesh.gets[s]; n > 0 { t.Errorf("%s: D13 read %s one key at a time %d times", tc.name, s, n) } @@ -244,7 +243,7 @@ func TestTheBusProbes(t *testing.T) { } // What changes after the watch began is judged at the next run, from memory: every reading gone is seen. mesh := dataMesh(func(*dataRig) {}) - c := &Checker{Store: mesh, Now: func() time.Time { return dataNow }} + c := &Checker{Store: mesh, Catalog: mesh, Now: func() time.Time { return dataNow }} if found, err := probeData(context.Background(), c); err != nil || len(found) != 0 { t.Fatalf("D13 on a held mesh: %v, %v", keysOf(found), err) } @@ -452,9 +451,12 @@ func dataMesh(change func(*dataRig)) *memStore { man.Data.Own = append(man.Data.Own, it) } raw, _ := json.Marshal(man) - digest := catalogclient.ManifestDigest(raw) - _ = r.s.Put(ManifestsRead, digest, catalog.ManifestsByDigestValue{V: 1, Module: module, Manifest: string(raw)}) - _ = r.s.Put(ModulesRead, module, catalog.ModulesByModuleValue{V: 1, Manifest: digest}) + var whole map[string]any + _ = json.Unmarshal(raw, &whole) + if r.s.modules == nil { + r.s.modules = map[string]catalog.CatalogEntry{} + } + r.s.modules[module] = catalog.CatalogEntry{Module: module, Manifest: whole} } declare("pg", map[string]catalogue.DataItem{ "data": {Class: catalogue.ClassValuable, Within: "48h", Active: "24h"}, diff --git a/modules/mesh-self-check/cmd/mesh-self-check/selfcheck.go b/modules/mesh-self-check/cmd/mesh-self-check/selfcheck.go index 3ec8cca8d..d24dc9381 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/selfcheck.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/selfcheck.go @@ -110,6 +110,8 @@ type ( // Checker is the self-check: the registry and what its probes reach. type Checker struct { Store Store + // Catalog is the catalog half: which modules exist and the manifest each entry carries (design 47 G §594). + Catalog CatalogHalf // Enabled is the setting `probes`: the ids of the probes that run. Nil runs every probe in the registry; one not // listed runs nothing and raises nothing (the operator, 2026-10-30). Enabled func() []string diff --git a/modules/mesh-self-check/cmd/mesh-self-check/selfcheck_test.go b/modules/mesh-self-check/cmd/mesh-self-check/selfcheck_test.go index 74b4984d3..4f53afb12 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/selfcheck_test.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/selfcheck_test.go @@ -13,6 +13,7 @@ import ( "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -27,8 +28,14 @@ type memStore struct { watches map[string][]func(key string, value []byte, gone bool) keysErr error events []string // the subjects the finding writer said its events on + // modules are the catalog half's entries (the CatalogHalf this store answers as the tests' own). + modules map[string]catalog.CatalogEntry + modulesErr error } +// Modules is the catalog half, as the tests give it. +func (s *memStore) Modules() (map[string]catalog.CatalogEntry, error) { return s.modules, s.modulesErr } + func newStore() *memStore { return &memStore{kv: map[string]map[string][]byte{}, puts: map[string]int{}, gets: map[string]int{}, watches: map[string][]func(string, []byte, bool){}} diff --git a/modules/mesh-self-check/module.json b/modules/mesh-self-check/module.json index 3c16d3e69..fdfd77712 100644 --- a/modules/mesh-self-check/module.json +++ b/modules/mesh-self-check/module.json @@ -23,8 +23,9 @@ "seat:node-intrusion-prevention.banned", "mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-assigner.assigner_read_seat_holders", - "mesh-delivery-indexer.indexer_read_modules", - "mesh-delivery-indexer.indexer_read_manifests", + "seat:mesh-catalog.revision", + "seat:mesh-catalog.registrations", + "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act" ], "reads": [ @@ -36,8 +37,9 @@ "consumes": [ "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.seat-holders", - "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.manifests" + "mesh-catalog.catalog-revision", + "mesh-catalog.registration-changed", + "mesh-delivery.source-moved" ], "state": [ "findings",