W16.4: mesh-self-check reads which modules exist and their manifests from the catalog half through meshcatalogclient's branch follower (milestone 47/G)

Builds: design 47 G §196, §594, §614; lead answers A-5, A-15

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 23:21:31 +01:00
parent 469763547c
commit 095e23ecda
9 changed files with 211 additions and 42 deletions
@@ -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
}
@@ -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)
}
}
@@ -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
}
@@ -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
@@ -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
}
}
@@ -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"},
@@ -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
@@ -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){}}
+6 -4
View File
@@ -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",