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