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:
@@ -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){}}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user