COND6: the keeper follows one catalog view per watched branch and adopts a branch registered while it runs

Builds: design 47 G §365, §479, §521, §532

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 20:26:40 +01:00
parent ef60e5d09e
commit 8f4f70f088
7 changed files with 213 additions and 3 deletions
@@ -0,0 +1,128 @@
package main
// The keeper follows one view of the catalog half per watched branch, the branches the registrations name, and adopts a
// branch registered while it runs with no restart (design 47 G §532, §365, §479). No branch is named here (§7).
import (
"encoding/json"
"slices"
"sort"
"sync"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
// Branches keeps the keeper's views of the catalog half, one per watched branch.
type Branches struct {
// Open adopts the view of the branch's catalog half and answers its name in the reader; Drop ends it, and Dropped
// forgets what the keeper held of the branch.
Open func(branch string) (string, error)
Drop func(name string) error
Dropped func(branch string) error
Logf func(string, ...any)
mu sync.Mutex
names map[string]string
}
// Watched sets the watched branches to the ones the registrations name: a branch new to them is adopted, a branch no
// registration names any more is dropped.
func (b *Branches) Watched(regs []catseat.Registration) {
want := map[string]bool{}
for _, r := range regs {
for _, branch := range r.Branches {
want[keys.EscapeName(branch)] = true
}
}
b.mu.Lock()
if b.names == nil {
b.names = map[string]string{}
}
var adopt, drop []string
for branch := range want {
if _, ok := b.names[branch]; !ok {
adopt = append(adopt, branch)
}
}
for branch := range b.names {
if !want[branch] {
drop = append(drop, branch)
}
}
b.mu.Unlock()
sort.Strings(adopt)
sort.Strings(drop)
for _, branch := range adopt {
name, err := b.Open(branch)
if err != nil {
b.Logf("the catalog half of %s is not followed: %v", branch, err)
continue
}
b.mu.Lock()
b.names[branch] = name
b.mu.Unlock()
}
for _, branch := range drop {
b.mu.Lock()
name := b.names[branch]
delete(b.names, branch)
b.mu.Unlock()
if err := b.Drop(name); err != nil {
b.Logf("the view %s is not dropped: %v", name, err)
}
if err := b.Dropped(branch); err != nil {
b.Logf("the conditions of the dropped branch %s are not cleared: %v", branch, err)
}
}
}
// Followed is the branches followed, in name order.
func (b *Branches) Followed() []string {
b.mu.Lock()
defer b.mu.Unlock()
out := make([]string, 0, len(b.names))
for branch := range b.names {
out = append(out, branch)
}
slices.Sort(out)
return out
}
// followCatalog is Branches over the keeper's reader: each branch's view is the catalog client's, whose changes hand the
// branch's whole half to the keeper.
func followCatalog(k *Keeper, v *views.Reader, call meshcatalogclient.Call) *Branches {
return &Branches{
Logf: logf,
Open: func(branch string) (string, error) {
var mu sync.Mutex
var c *meshcatalogclient.Catalog
changed := func(string, json.RawMessage, bool, time.Time) {
mu.Lock()
view := c
mu.Unlock()
if view == nil {
return
}
if half, ok := view.View(); ok {
if err := k.CatalogHeld(branch, half.Entries); err != nil {
logf("%v", err)
}
}
}
mu.Lock()
defer mu.Unlock()
made, err := meshcatalogclient.WireCatalog(v, call, branch, changed)
if err != nil {
return "", err
}
c = made
return made.Follower.Owner + "." + made.Follower.State, nil
},
Drop: v.Drop,
Dropped: k.CatalogDropped,
}
}
@@ -0,0 +1,43 @@
package main
import (
"slices"
"testing"
catseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// Design 47 G §532: the keeper adopts a branch a registration adds while it runs, and drops one no registration names.
func TestTheKeeperFollowsTheBranchesTheRegistrationsName(t *testing.T) {
var opened, dropped, forgotten []string
b := &Branches{
Logf: func(string, ...any) {},
Open: func(branch string) (string, error) {
opened = append(opened, branch)
return "mesh-catalog." + branch, nil
},
Drop: func(name string) error { dropped = append(dropped, name); return nil },
Dropped: func(branch string) error { forgotten = append(forgotten, branch); return nil },
}
reg := func(module string, branches ...string) catseat.Registration {
return catseat.Registration{Module: module, Branches: branches}
}
steps := []struct {
name string
regs []catseat.Registration
followed, opened, dropped, forgotten []string
}{
{"the first registrations", []catseat.Registration{reg("a", "trunk")}, []string{"trunk"}, []string{"trunk"}, nil, nil},
{"a branch registered while it runs", []catseat.Registration{reg("a", "trunk"), reg("b", "feature/x")}, []string{"feature/x", "trunk"}, []string{"trunk", "feature/x"}, nil, nil},
{"a branch nobody names is dropped", []catseat.Registration{reg("a", "trunk")}, []string{"trunk"}, []string{"trunk", "feature/x"}, []string{"mesh-catalog.feature/x"}, []string{"feature/x"}},
}
for _, s := range steps {
b.Watched(s.regs)
if got := b.Followed(); !slices.Equal(got, s.followed) {
t.Errorf("%s: followed %v, want %v", s.name, got, s.followed)
}
if !slices.Equal(opened, s.opened) || !slices.Equal(dropped, s.dropped) || !slices.Equal(forgotten, s.forgotten) {
t.Errorf("%s: opened %v dropped %v forgotten %v", s.name, opened, dropped, forgotten)
}
}
}
@@ -52,6 +52,20 @@ func (k *Keeper) CatalogHeld(branch string, entries map[string]catseat.CatalogEn
k.mu.Lock()
k.init()
k.halves[branch] = entries
return k.sweepCatalog()
}
// CatalogDropped forgets a branch no registration watches any more (design 47 G §532): the modules only it listed leave
// the sources with their conditions.
func (k *Keeper) CatalogDropped(branch string) error {
k.mu.Lock()
k.init()
delete(k.halves, branch)
return k.sweepCatalog()
}
// sweepCatalog clears the conditions of every observer no held revision lists. Called with mu held; it releases it.
func (k *Keeper) sweepCatalog() error {
var gone []string
seen := map[string]bool{}
consider := func(observer string) {
@@ -607,6 +607,16 @@ func TestTheKeeperSetsItsViewFromWhatAnObserverStates(t *testing.T) {
}
return k2.CatalogHeld("main", map[string]catseat.CatalogEntry{selfCheckModule: {}})
}, []string{"machine.d.silent", "machine.g.silent"}, whyLeftCatalog},
// Design 47 G §532: a module only a second watched branch lists is a source, and the branch dropped takes it away.
{"a module listed only by a second branch is a source", func() error {
if err := r.k.CatalogHeld("feature", map[string]catseat.CatalogEntry{"fixture": {Streams: []string{EventFindingWritten}}}); err != nil {
return err
}
return r.k.FindingWritten("fixture", "ace", mustJSON(t, f("fixture", "z")))
}, []string{"machine.d.silent", "machine.g.silent", "machine.z.silent"}, ""},
{"the second branch dropped takes its source away", func() error {
return r.k.CatalogDropped("feature")
}, []string{"machine.d.silent", "machine.g.silent"}, whyLeftCatalog},
}
for _, s := range steps {
if err := s.do(); err != nil {
@@ -18,7 +18,9 @@ import (
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"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/clients/meshwantsclient"
catseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -156,6 +158,14 @@ func NewViews(k *Keeper) *views.Reader {
if _, err := meshwantsclient.WireAssignment(v, stdio.Ask, func(rev meshwants.AssignmentRevision) { _ = k.AssignmentHeld(rev) }); err != nil {
logf("the assignment half is not followed: %v", err)
}
// One view of the catalog half per watched branch, the branches the registrations name, adopted and dropped while the
// keeper runs (design 47 G §365, §532).
branches := followCatalog(k, v, stdio.Ask)
var regs *meshcatalogclient.Registrations
regs, err := meshcatalogclient.WireRegistrations(v, stdio.Ask, func(catseat.Registration, bool) { branches.Watched(regs.All()) })
if err != nil {
logf("the registrations are not followed: %v", err)
}
return v
}
@@ -36,9 +36,10 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
// The assigner's wants and assignments are asked once of their read tools and followed by their events (ADR 0615 §1);
// the observers' findings are heard as events and no `findings` is read (design 47 G §383).
reads := []string{HavesRead, AttemptsRead}
if !slices.Equal(m.Invokes, []string{WantsTool, AssignmentsTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-wants.revision"}) ||
if !slices.Equal(m.Invokes, []string{WantsTool, AssignmentsTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-wants.revision",
"seat:mesh-catalog.registrations", "seat:mesh-catalog.revision"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-assigner.views.assignments",
"*.finding-written", "*.finding-cleared", "*.findings-stated", "mesh-watchdogs.machine-away", "mesh-watchdogs.machine-back", "mesh-wants.assignment-revision", "node-engine.running"}) {
"*.finding-written", "*.finding-cleared", "*.findings-stated", "mesh-watchdogs.machine-away", "mesh-watchdogs.machine-back", "mesh-wants.assignment-revision", "mesh-catalog.registration-changed", "mesh-catalog.catalog-revision", "node-engine.running"}) {
t.Errorf("invokes %v, consumes %v", m.Invokes, m.Consumes)
}
for _, r := range m.Reads {
+5 -1
View File
@@ -21,7 +21,9 @@
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-assigner.assigner_read_assignments",
"seat:mesh-hand-acts.hand-act",
"seat:mesh-wants.revision"
"seat:mesh-wants.revision",
"seat:mesh-catalog.registrations",
"seat:mesh-catalog.revision"
],
"consumes": [
"mesh-delivery-assigner.views.wants",
@@ -32,6 +34,8 @@
"mesh-watchdogs.machine-away",
"mesh-watchdogs.machine-back",
"mesh-wants.assignment-revision",
"mesh-catalog.registration-changed",
"mesh-catalog.catalog-revision",
"node-engine.running"
],
"state": [