R-5: the tool runner follows the assigner's views of the module that claims the seat, named by the seat
The holder is read from the catalog half's entries serving a verb of mesh-module-assigner, on the branch follower's change callback; the manifest's consumes names the two views by the seat (seat:mesh-module-assigner.views.*). Builds: design 47 G §616, §428, §7 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -48,8 +48,8 @@ func TestTheManifestNamesTheFirstFamiliesOneByOne(t *testing.T) {
|
||||
// `registration-changed`, and the gate's `source-moved`; never the indexer's `views.modules` or `views.manifests`), the two
|
||||
// events of the seat that serves the seat list (ADR 0636 §4) and the keeper's `findings-asked`, and nothing else.
|
||||
wantConsumes := []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-assigner.views.seat-holders", "mesh-delivery.source-moved"}
|
||||
"mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "seat:mesh-module-assigner.views.assignments",
|
||||
"seat:mesh-module-assigner.views.seat-holders", "mesh-delivery.source-moved"}
|
||||
if !reflect.DeepEqual(m.Consumes, wantConsumes) {
|
||||
t.Errorf("the tool runner consumes %v, want %v", m.Consumes, wantConsumes)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
@@ -31,27 +32,27 @@ import (
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
)
|
||||
|
||||
// The owner and its first-view tools (the tools' addresses are `<owner>.<tool>`).
|
||||
const Assigner = "mesh-delivery-assigner"
|
||||
// AssignerSeat is the seat whose holder's views the runner follows: the holder is read from the catalog half's claims of the
|
||||
// seat, never named in code (design 47 G §616, §428), and the names the console reads the two states by are the seat's
|
||||
// (`<seat>.<state>`; the manifest's `consumes` names their views families by the seat, as `seat:<seat>.views.<state>`).
|
||||
const AssignerSeat = meshmoduleassigner.Seat
|
||||
|
||||
// The names the console reads the assigner's two states by (`<owner>.<state>`), as the manifest's `consumes` names their views
|
||||
// families (no seat's client names a holder, novox/hq ADR 0625 §4; TestTheFirstViewsAreTheManifestsViews).
|
||||
const (
|
||||
AssignmentsRead = Assigner + ".assignments"
|
||||
SeatHoldersRead = Assigner + ".seat-holders"
|
||||
AssignmentsRead = AssignerSeat + ".assignments"
|
||||
SeatHoldersRead = AssignerSeat + ".seat-holders"
|
||||
)
|
||||
|
||||
// Spec is one first view the runner holds.
|
||||
type Spec struct{ Owner, Tool, State string }
|
||||
// Spec is one first view the runner holds of the seat's holder: its first-view tool and the state.
|
||||
type Spec struct{ Tool, State string }
|
||||
|
||||
// Specs are the two owner states asked of an owner's tool; the catalog half's views are adopted by the branch follower.
|
||||
// Specs are the two states asked of the holder's tool; the catalog half's views are adopted by the branch follower.
|
||||
var Specs = []Spec{
|
||||
{Assigner, "assignments", "assignments"},
|
||||
{Assigner, "seat-holders", "seat-holders"},
|
||||
{"assignments", "assignments"},
|
||||
{"seat-holders", "seat-holders"},
|
||||
}
|
||||
|
||||
// Name is the name a state is read by, `<owner>.<state>`: AssignmentsRead and SeatHoldersRead.
|
||||
func (s Spec) Name() string { return s.Owner + "." + s.State }
|
||||
// Name is the name a state is read by, `<seat>.<state>`: AssignmentsRead and SeatHoldersRead.
|
||||
func (s Spec) Name() string { return AssignerSeat + "." + s.State }
|
||||
|
||||
// Views is the first views: the assigner's two, and the catalog half's per watched branch.
|
||||
type Views struct {
|
||||
@@ -62,6 +63,7 @@ type Views struct {
|
||||
adopted map[string]*adoption // by `<owner>.<state>`, the views the branch follower adopted
|
||||
hearers map[string][]*adoption // event key `<seat>.<event>` -> the adopted views that hear it
|
||||
branches *meshcatalogclient.Branches
|
||||
holder *holding // the module that claims the assigner seat, once the catalog half names it
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
@@ -163,22 +165,6 @@ func StartWith(parent context.Context, d Deps, logf func(string, ...any)) (*View
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
v := &Views{logf: logf, ctx: ctx, cancel: cancel, followers: map[string]*views.Follower{}, byState: map[string]*views.Follower{},
|
||||
adopted: map[string]*adoption{}, hearers: map[string][]*adoption{}, deps: d}
|
||||
for _, s := range Specs {
|
||||
s := s
|
||||
call := func(tool string, body any) (json.RawMessage, error) { return d.Ask(s.Owner+"."+tool, body) }
|
||||
f := &views.Follower{Owner: s.Owner, Tool: s.Tool, State: s.State, Ask: views.AskVia(call, s.Tool), Logf: logf,
|
||||
Bound: d.Bound, Wait: d.Wait,
|
||||
Raise: func(st views.Status) {
|
||||
v.finding(unansweredFinding(v.deps.Module, v.deps.Node, st, time.Now()), st.Tool, true)
|
||||
},
|
||||
Clear: func(st views.Status) {
|
||||
v.finding(unansweredFinding(v.deps.Module, v.deps.Node, st, time.Now()), st.Tool, false)
|
||||
}}
|
||||
v.followers[s.Name()] = f
|
||||
v.byState[s.State] = f
|
||||
v.wg.Add(1)
|
||||
go func() { defer v.wg.Done(); _ = f.Start(ctx) }()
|
||||
}
|
||||
stopFindings := func() {}
|
||||
if d.StartFindings != nil {
|
||||
if stop, err := d.StartFindings(ctx); err != nil {
|
||||
@@ -194,6 +180,9 @@ func StartWith(parent context.Context, d Deps, logf func(string, ...any)) (*View
|
||||
logf("[mesh-tools] the catalog half is not followed (%v): the console cannot say which modules exist", err)
|
||||
} else {
|
||||
branches.Logf = func(format string, args ...any) { logf("[mesh-tools] "+format, args...) }
|
||||
branches.Changed = func(_, module string, entry json.RawMessage, deleted bool, _ time.Time) {
|
||||
v.claimed(module, entry, deleted)
|
||||
}
|
||||
v.branches = branches
|
||||
}
|
||||
stop, err := d.Consume(v.hear)
|
||||
@@ -221,7 +210,9 @@ func (v *Views) hear(env bus.Envelope) error {
|
||||
if err != nil {
|
||||
return nil // not a views event: nothing to hold, nothing to retry
|
||||
}
|
||||
v.mu.Lock()
|
||||
f := v.byState[ev.State]
|
||||
v.mu.Unlock()
|
||||
if f == nil || f.Owner != ev.Owner {
|
||||
return nil
|
||||
}
|
||||
@@ -253,6 +244,13 @@ func (v *Views) finding(f condseat.Finding, tool string, raised bool) {
|
||||
// not answered yet, so nothing is claimed of it.
|
||||
func (v *Views) State(name string) (map[string][]byte, error) {
|
||||
v.mu.Lock()
|
||||
if state, ok := strings.CutPrefix(name, AssignerSeat+"."); ok { // the seat's name stands for its holder's
|
||||
if v.holder == nil {
|
||||
v.mu.Unlock()
|
||||
return nil, fmt.Errorf("the catalog half names no holder of the seat %s yet, so %s has no first view", AssignerSeat, name)
|
||||
}
|
||||
name = v.holder.module + "." + state
|
||||
}
|
||||
f := v.followers[name]
|
||||
v.mu.Unlock()
|
||||
if f == nil {
|
||||
@@ -462,3 +460,78 @@ func (v *Views) Modules() (map[string]catalogseat.CatalogEntry, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// holding is the views of the module that claims the assigner seat, and what stops them.
|
||||
type holding struct {
|
||||
module string
|
||||
cancel context.CancelFunc
|
||||
views []*views.Follower
|
||||
}
|
||||
|
||||
// claimed is the branch follower's word of one entry changed: an entry that serves a verb of the assigner seat makes its module
|
||||
// the holder whose views the runner follows (a holder before it is dropped), the deletion of the holder's entry drops them
|
||||
// (design 47 G §616, §428). It acts on the event alone; nothing waits on a timer for the claim.
|
||||
func (v *Views) claimed(module string, entry json.RawMessage, deleted bool) {
|
||||
claims := false
|
||||
if !deleted {
|
||||
var e catalogseat.CatalogEntry
|
||||
if json.Unmarshal(entry, &e) == nil {
|
||||
claims = slices.ContainsFunc(e.Serves, func(s string) bool { return strings.HasPrefix(s, AssignerSeat+".") })
|
||||
}
|
||||
}
|
||||
v.mu.Lock()
|
||||
cur := v.holder
|
||||
switch {
|
||||
case claims && cur != nil && cur.module == module, !claims && (cur == nil || cur.module != module), v.ctx.Err() != nil:
|
||||
v.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if cur != nil {
|
||||
v.dropHolderLocked()
|
||||
}
|
||||
if claims {
|
||||
v.startHolderLocked(module)
|
||||
}
|
||||
v.mu.Unlock()
|
||||
}
|
||||
|
||||
// startHolderLocked follows the first views of module, the holder of the seat, on the runner's own consumer.
|
||||
func (v *Views) startHolderLocked(module string) {
|
||||
ctx, cancel := context.WithCancel(v.ctx)
|
||||
h := &holding{module: module, cancel: cancel}
|
||||
d := v.deps
|
||||
for _, s := range Specs {
|
||||
s := s
|
||||
call := func(tool string, body any) (json.RawMessage, error) { return d.Ask(module+"."+tool, body) }
|
||||
f := &views.Follower{Owner: module, Tool: s.Tool, State: s.State, Ask: views.AskVia(call, s.Tool), Logf: v.logf,
|
||||
Bound: d.Bound, Wait: d.Wait,
|
||||
Raise: func(st views.Status) {
|
||||
v.finding(unansweredFinding(d.Module, d.Node, st, time.Now()), st.Tool, true)
|
||||
},
|
||||
Clear: func(st views.Status) {
|
||||
v.finding(unansweredFinding(d.Module, d.Node, st, time.Now()), st.Tool, false)
|
||||
}}
|
||||
v.followers[module+"."+s.State] = f
|
||||
v.byState[s.State] = f
|
||||
h.views = append(h.views, f)
|
||||
v.wg.Add(1)
|
||||
go func() { defer v.wg.Done(); _ = f.Start(ctx) }()
|
||||
}
|
||||
v.holder = h
|
||||
v.logf("[mesh-tools] the seat %s is held by %s: its views are followed", AssignerSeat, module)
|
||||
}
|
||||
|
||||
// dropHolderLocked stops the views of the holder and clears the findings they raised.
|
||||
func (v *Views) dropHolderLocked() {
|
||||
h := v.holder
|
||||
v.holder = nil
|
||||
h.cancel()
|
||||
for _, f := range h.views {
|
||||
delete(v.followers, f.Owner+"."+f.State)
|
||||
delete(v.byState, f.State)
|
||||
if st := f.Status(); st.Raised {
|
||||
go v.finding(unansweredFinding(v.deps.Module, v.deps.Node, st, time.Now()), st.Tool, false)
|
||||
}
|
||||
}
|
||||
v.logf("[mesh-tools] %s no longer holds the seat %s: its views are no longer followed", h.module, AssignerSeat)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
)
|
||||
@@ -41,7 +45,9 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
|
||||
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
streamOf := map[string]string{Assigner: catalogue.StreamBusName("assigner-views")}
|
||||
// the manifest names the assigner's views by the seat; the module the catalog half says claims it is the fixture's
|
||||
const holder = "assigner-fixture"
|
||||
streamOf := map[string]string{"seat:" + AssignerSeat: catalogue.StreamBusName("assigner-views")}
|
||||
filters := map[string][]string{}
|
||||
families := 0
|
||||
for _, c := range manifest.Consumes {
|
||||
@@ -53,16 +59,16 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
|
||||
if stream == "" {
|
||||
t.Fatalf("the manifest consumes %s, whose views stream the test does not know", c)
|
||||
}
|
||||
filters[stream] = append(filters[stream], process.ModuleStreamSubject(emitter, views.EventPrefix+event))
|
||||
filters[stream] = append(filters[stream], process.ModuleStreamSubject(holder, views.EventPrefix+event))
|
||||
families++
|
||||
}
|
||||
if families != len(Specs) {
|
||||
t.Fatalf("the manifest consumes %d families, the runner holds %d first views", families, len(Specs))
|
||||
}
|
||||
runner := mt.MembershipOf("node-tools", "anchor", false, nil)
|
||||
for _, owner := range []string{Assigner} {
|
||||
stream := streamOf[owner]
|
||||
mesh.ModuleStream(t, owner, stream)
|
||||
for _, seat := range []string{"seat:" + AssignerSeat} {
|
||||
stream := streamOf[seat]
|
||||
mesh.ModuleStream(t, holder, stream)
|
||||
mesh.ConsumerOn(t, stream, "anchor", "node-tools", filters[stream], 2*time.Second)
|
||||
runner.Hears = append(runner.Hears, bus.HeardIssued{Stream: stream, Consumer: "anchor_node-tools"})
|
||||
}
|
||||
@@ -93,13 +99,13 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
|
||||
t.Cleanup(stop)
|
||||
|
||||
// The owners are down: no tool answers, nothing is claimed, and past the bound each view is a finding.
|
||||
if _, err := v.State(Assigner + ".assignments"); err == nil {
|
||||
if _, err := v.State(AssignmentsRead); err == nil {
|
||||
t.Fatal("a state its owner has not answered is claimed")
|
||||
}
|
||||
mt.Until(t, func() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(findings) != len(Specs)+catalogViews {
|
||||
if len(findings) != catalogViews {
|
||||
return errFindings(len(findings))
|
||||
}
|
||||
return nil
|
||||
@@ -111,10 +117,25 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
respond := func(subject string, answer any) {
|
||||
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
|
||||
raw, _ := json.Marshal(answer)
|
||||
body, _ := json.Marshal(map[string]json.RawMessage{"result": raw})
|
||||
_ = m.Respond(body)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// the catalog half answers: the gate's source names main, whose entries say the fixture claims the assigner seat
|
||||
respond("mesh.seat.mesh-delivery.tool.source", delivery.SourceAnswer{Branches: []string{"main"}})
|
||||
respond("mesh.seat.mesh-catalog.tool.registrations", catalogseat.RegistrationsAnswer{Registrations: []catalogseat.Registration{}})
|
||||
respond("mesh.seat.mesh-catalog.tool.revision", catalogseat.RevisionAnswer{
|
||||
Revision: &catalogseat.CatalogRevision{Revision: 3, Kind: meshcatalogclient.KindTrunk, Branch: "main"},
|
||||
Entries: []catalogseat.CatalogEntry{{Module: holder, Serves: []string{AssignerSeat + ".assignments"}}}})
|
||||
for _, s := range Specs {
|
||||
s := s
|
||||
if _, err := nc.Subscribe("mesh.mod."+s.Owner+".tool."+s.Tool, func(m *nats.Msg) {
|
||||
answer, _ := json.Marshal(views.Answer{V: 1, Owner: s.Owner, State: s.State, Revision: 4, Created: time.Unix(1000, 0).UTC(),
|
||||
if _, err := nc.Subscribe("mesh.mod."+holder+".tool."+s.Tool, func(m *nats.Msg) {
|
||||
answer, _ := json.Marshal(views.Answer{V: 1, Owner: holder, State: s.State, Revision: 4, Created: time.Unix(1000, 0).UTC(),
|
||||
Values: map[string]json.RawMessage{"first": json.RawMessage(`{"n":1}`)}})
|
||||
body, _ := json.Marshal(map[string]json.RawMessage{"result": answer})
|
||||
_ = m.Respond(body)
|
||||
@@ -130,7 +151,7 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
|
||||
mt.Until(t, func() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(findings) != catalogViews { // the seats' verbs are not served in this test: the catalog half stays unanswered
|
||||
if len(findings) != 0 { // every view answered clears its finding
|
||||
return errFindings(len(findings))
|
||||
}
|
||||
return nil
|
||||
@@ -138,9 +159,9 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
|
||||
|
||||
// Each owner's next event arrives on the runner's consumer: all four families are bound.
|
||||
for _, s := range Specs {
|
||||
ev, _ := json.Marshal(views.Event{V: 1, Owner: s.Owner, State: s.State, Key: "next", Revision: 5, Created: time.Unix(1000, 0).UTC(),
|
||||
ev, _ := json.Marshal(views.Event{V: 1, Owner: holder, State: s.State, Key: "next", Revision: 5, Created: time.Unix(1000, 0).UTC(),
|
||||
Value: json.RawMessage(`{"n":2}`)})
|
||||
mesh.Emit(t, process.ModuleStreamSubject(s.Owner, views.EventPrefix+s.State), json.RawMessage(ev))
|
||||
mesh.Emit(t, process.ModuleStreamSubject(holder, views.EventPrefix+s.State), json.RawMessage(ev))
|
||||
}
|
||||
for _, s := range Specs {
|
||||
name := s.Name()
|
||||
|
||||
@@ -32,12 +32,14 @@ type owners struct {
|
||||
// catalog is the revision of the watched branch main the seat mesh-catalog answers, and modules its entries
|
||||
catalog int64
|
||||
modules []string
|
||||
// holder is the module among them that claims the assigner seat
|
||||
holder string
|
||||
}
|
||||
|
||||
func newOwners() *owners {
|
||||
return &owners{state: map[string]map[string]string{"assignments": {"anchor": `{"p":1}`}, "seat-holders": {"seat": `{"h":1}`}},
|
||||
rev: map[string]uint64{"assignments": 4, "seat-holders": 2}, asked: map[string]int{}, finds: map[string]bool{},
|
||||
catalog: 3, modules: []string{"web"}}
|
||||
catalog: 3, modules: []string{"web", fixtureHolder}, holder: fixtureHolder}
|
||||
}
|
||||
|
||||
// Write and Clear are the runner's finding writer in a test: the findings written and not cleared, by their key.
|
||||
@@ -71,7 +73,11 @@ func (o *owners) ask(address string, _ any) (json.RawMessage, error) {
|
||||
case "seat:mesh-catalog.revision":
|
||||
a := catalogseat.RevisionAnswer{Revision: &catalogseat.CatalogRevision{Revision: o.catalog, Kind: meshcatalogclient.KindTrunk, Branch: "main"}}
|
||||
for _, m := range o.modules {
|
||||
a.Entries = append(a.Entries, catalogseat.CatalogEntry{Module: m, Manifest: map[string]any{"module": m, "tools": []string{m + "_check"}}})
|
||||
e := catalogseat.CatalogEntry{Module: m, Manifest: map[string]any{"module": m, "tools": []string{m + "_check"}}}
|
||||
if m == o.holder {
|
||||
e.Serves = []string{AssignerSeat + ".assignments", AssignerSeat + ".seat-holders"}
|
||||
}
|
||||
a.Entries = append(a.Entries, e)
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
@@ -94,14 +100,10 @@ func (o *owners) say(t *testing.T, state, key, value string, revision uint64, ga
|
||||
}
|
||||
}
|
||||
|
||||
func owner(state string) string {
|
||||
for _, s := range Specs {
|
||||
if s.State == state {
|
||||
return s.Owner
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// fixtureHolder is the module the fixture catalog half says claims the assigner seat: no name is in the code.
|
||||
const fixtureHolder = "assigner-fixture"
|
||||
|
||||
func owner(state string) string { return fixtureHolder }
|
||||
|
||||
func eventually(t *testing.T, what string, ok func() bool) {
|
||||
t.Helper()
|
||||
@@ -127,37 +129,37 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
|
||||
defer stop()
|
||||
|
||||
// Down: nothing is claimed, and past the bound each unanswered view is a finding.
|
||||
if _, err := v.State("mesh-delivery-assigner.assignments"); err == nil || !strings.Contains(err.Error(), "not been answered") {
|
||||
t.Fatalf("a state its owner has not answered is claimed: %v", err)
|
||||
if _, err := v.State(AssignmentsRead); err == nil || !strings.Contains(err.Error(), "names no holder") {
|
||||
t.Fatalf("a state whose holder is not known is claimed: %v", err)
|
||||
}
|
||||
eventually(t, "no finding past the bound", func() bool { o.mu.Lock(); defer o.mu.Unlock(); return len(o.finds) == 4 })
|
||||
eventually(t, "no finding past the bound", func() bool { o.mu.Lock(); defer o.mu.Unlock(); return len(o.finds) == 2 })
|
||||
|
||||
// Up: answered, the findings are cleared, and the pictures hold the owners' values.
|
||||
o.mu.Lock()
|
||||
o.down = false
|
||||
o.mu.Unlock()
|
||||
eventually(t, "not answered", func() bool { _, err := v.State("mesh-delivery-assigner.seat-holders"); return err == nil })
|
||||
eventually(t, "not answered", func() bool { _, err := v.State(SeatHoldersRead); return err == nil })
|
||||
eventually(t, "findings not cleared", func() bool { o.mu.Lock(); defer o.mu.Unlock(); return len(o.finds) == 0 })
|
||||
if got, _ := v.Entry("mesh-delivery-assigner.seat-holders", "none"); got != nil {
|
||||
if got, _ := v.Entry(AssignerSeat+".seat-holders", "none"); got != nil {
|
||||
t.Errorf("an absent key is %q", got)
|
||||
}
|
||||
|
||||
// The catalog half is followed by the shared branch follower on the runner's own consumer: the gate's source names main,
|
||||
// main's view is adopted, and its entries are the modules, each with its manifest (design 47 G §196, §614).
|
||||
eventually(t, "the catalog half holds no modules", func() bool { m, err := v.Modules(); return err == nil && len(m) == 1 })
|
||||
eventually(t, "the catalog half holds no modules", func() bool { m, err := v.Modules(); return err == nil && len(m) == 2 })
|
||||
m, _ := v.Modules()
|
||||
if m["web"].Manifest["module"] != "web" {
|
||||
t.Errorf("the module web is %+v", m["web"])
|
||||
}
|
||||
// A catalog-revision of the branch is heard on the same consumer and asks the view once more.
|
||||
o.mu.Lock()
|
||||
o.catalog, o.modules = 4, []string{"web", "db"}
|
||||
o.catalog, o.modules = 4, []string{"web", "db", fixtureHolder}
|
||||
o.mu.Unlock()
|
||||
ev, _ := json.Marshal(catalogseat.CatalogRevisionEvent{V: 1, Branch: "main", Kind: meshcatalogclient.KindTrunk, Revision: 4})
|
||||
if err := o.deliver(bus.Envelope{Key: catalogseat.Seat + "." + catalogseat.EventCatalogRevision, Body: ev}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
eventually(t, "the catalog-revision was not taken", func() bool { m, err := v.Modules(); return err == nil && len(m) == 2 })
|
||||
eventually(t, "the catalog-revision was not taken", func() bool { m, err := v.Modules(); return err == nil && len(m) == 3 })
|
||||
|
||||
// Events: the next revision is applied; an old one passed over; a deletion removes; none of it asks again.
|
||||
asked := func(tool string) int { o.mu.Lock(); defer o.mu.Unlock(); return o.asked[tool] }
|
||||
@@ -166,7 +168,7 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
|
||||
o.say(t, "assignments", "laptop", `{"p":0}`, 5, false)
|
||||
o.say(t, "assignments", "anchor", "", 6, false)
|
||||
// (a deleted mark carries no value: Deleted is the encoding's, set below)
|
||||
if got, _ := v.Entry("mesh-delivery-assigner.assignments", "laptop"); string(got) != `{"p":2}` {
|
||||
if got, _ := v.Entry(AssignerSeat+".assignments", "laptop"); string(got) != `{"p":2}` {
|
||||
t.Errorf("the next revision is not applied: %q", got)
|
||||
}
|
||||
if n := asked("assignments"); n != before {
|
||||
@@ -184,7 +186,7 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
|
||||
assignments = s
|
||||
}
|
||||
}
|
||||
if assignments.Owner != "mesh-delivery-assigner" || assignments.Tool != "assignments" || assignments.Asks < 2 || assignments.Answered.IsZero() {
|
||||
if assignments.Owner != fixtureHolder || assignments.Tool != "assignments" || assignments.Asks < 2 || assignments.Answered.IsZero() {
|
||||
t.Errorf("the status of assignments is %+v", assignments)
|
||||
}
|
||||
manifests := asked("seat-holders")
|
||||
@@ -224,13 +226,19 @@ func TestTheFirstViewsAreTheManifestsViews(t *testing.T) {
|
||||
t.Errorf("the first view %s is read by no name here", s.Name())
|
||||
}
|
||||
delete(want, s.Name())
|
||||
if view := s.Owner + ".views." + s.Tool; !slices.Contains(m.Consumes, view) {
|
||||
// the family is named by the seat, never by its holder (design 47 G §616)
|
||||
if view := "seat:" + AssignerSeat + ".views." + s.Tool; !slices.Contains(m.Consumes, view) {
|
||||
t.Errorf("the first view %s has no family in the manifest's consumes %v", view, m.Consumes)
|
||||
}
|
||||
}
|
||||
if len(want) > 0 {
|
||||
t.Errorf("no first view reads %v", want)
|
||||
}
|
||||
for _, c := range m.Consumes {
|
||||
if strings.Contains(c, "assigner") && !strings.HasPrefix(c, "seat:") {
|
||||
t.Errorf("the manifest names the assigner by a module: %s", c)
|
||||
}
|
||||
}
|
||||
// the catalog half's follower hears these events and asks these verbs, and the manifest says so (design 47 G §614)
|
||||
for _, ev := range []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved"} {
|
||||
if !slices.Contains(m.Consumes, ev) {
|
||||
@@ -243,3 +251,42 @@ func TestTheFirstViewsAreTheManifestsViews(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// design 47 G §616: the holder of the assigner seat is whichever module the catalog half says serves it; when another one
|
||||
// claims it the views are of the new holder, and when none does the seat's names answer that the catalog half names no holder.
|
||||
func TestTheAssignersViewsFollowTheModuleThatClaimsTheSeat(t *testing.T) {
|
||||
o := newOwners()
|
||||
deps := Deps{Module: "node-tools", Ask: o.ask, Bound: time.Second, Wait: 10 * time.Millisecond,
|
||||
Consume: func(deliver func(bus.Envelope) error) (func(), error) { o.deliver = deliver; return func() {}, nil }, Findings: o}
|
||||
v, stop := StartWith(context.Background(), deps, func(string, ...any) {})
|
||||
defer stop()
|
||||
eventually(t, "not answered", func() bool { _, err := v.State(AssignmentsRead); return err == nil })
|
||||
ownerOf := func() string {
|
||||
for _, s := range v.Status() {
|
||||
if s.State == "assignments" {
|
||||
return s.Owner
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if ownerOf() != fixtureHolder {
|
||||
t.Fatalf("the views are of %q", ownerOf())
|
||||
}
|
||||
revise := func(rev int64, holder string, modules ...string) {
|
||||
o.mu.Lock()
|
||||
o.catalog, o.modules, o.holder = rev, modules, holder
|
||||
o.mu.Unlock()
|
||||
ev, _ := json.Marshal(catalogseat.CatalogRevisionEvent{V: 1, Branch: "main", Kind: meshcatalogclient.KindTrunk, Revision: rev})
|
||||
if err := o.deliver(bus.Envelope{Key: catalogseat.Seat + "." + catalogseat.EventCatalogRevision, Body: ev}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
revise(4, "other-holder", "web", "other-holder")
|
||||
eventually(t, "the views did not move to the new holder", func() bool { return ownerOf() == "other-holder" })
|
||||
eventually(t, "not answered", func() bool { _, err := v.State(SeatHoldersRead); return err == nil })
|
||||
revise(5, "", "web")
|
||||
eventually(t, "the views stayed after the claim went", func() bool {
|
||||
_, err := v.State(AssignmentsRead)
|
||||
return err != nil && strings.Contains(err.Error(), "names no holder")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func sdkMembership(t *testing.T, mem bus.Membership) membership.Membership {
|
||||
// no holder).
|
||||
const (
|
||||
catalogHolder = "mesh-catalog"
|
||||
assignerHolder = "mesh-delivery-assigner"
|
||||
assignerHolder = "mesh-module-assigner" // the seat's name stands for its holder in the console's state names (firstview.AssignmentsRead)
|
||||
// AssignmentsBucket and SeatHoldersBucket are the assigner's states assignments and seat-holders.
|
||||
AssignmentsBucket = assignerHolder + "_assignments"
|
||||
SeatHoldersBucket = assignerHolder + "_seat-holders"
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-conditions.findings-asked",
|
||||
"mesh-delivery-assigner.views.assignments",
|
||||
"mesh-delivery-assigner.views.seat-holders",
|
||||
"seat:mesh-module-assigner.views.assignments",
|
||||
"seat:mesh-module-assigner.views.seat-holders",
|
||||
"mesh-delivery.source-moved"
|
||||
],
|
||||
"streams": [
|
||||
|
||||
Reference in New Issue
Block a user