Round 2: grants says the absent stream in its status and pass result; tests that fail without A-372's settle call and without Ready's reading of the first views
mesh/delivery ready: it is merged on the Approve
mesh/delivery-group group fix/47-G-start-2 checking: 2 of 2 member(s) ready

Builds: design 47 G A-371 health clause, A-372, A-368 (G-22-lead-answers-2.md)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-13 21:03:59 +01:00
parent 3c929d518c
commit fc51bf4832
6 changed files with 76 additions and 18 deletions
@@ -11,18 +11,10 @@ import (
"git.novox.be/novox/mesh-sdk/go/views"
)
type noneWiring struct{ followers []*views.Follower }
func (w *noneWiring) Adopt(f *views.Follower, _, _ string, _ func(context.Context, []byte) error) error {
w.followers = append(w.followers, f)
return nil
}
func (w *noneWiring) Drop(string, string) error { return nil }
// Design 47 G A-372: the assigner's wait for its catalog half asks the catalog client's Ready as well: a mesh whose registrations and
// gate's source are answered and name no branch is none-yet, and the first pass is told so once; before they answer it is not told.
func TestAMeshThatNamesNoBranchSettlesTheAssignersFirstPass(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
call := func(key string, _ any) (json.RawMessage, error) {
if key == mcc.RegistrationsVerb {
@@ -31,16 +23,14 @@ func TestAMeshThatNamesNoBranchSettlesTheAssignersFirstPass(t *testing.T) {
return json.Marshal(map[string][]string{"branches": {}})
}
for _, answered := range []bool{false, true} {
w := &noneWiring{}
b, err := mcc.NewBranches(w, call)
if err != nil {
settled := make(chan struct{}, 2)
reader := &views.ProcessReader{Module: "mesh-delivery-assigner", Call: call, Logf: t.Logf}
h := &halves{w: newWorld(), call: call, reader: reader, wake: func() {}, logf: t.Logf, ctx: ctx, settled: settled}
if err := h.follow(); err != nil {
t.Fatal(err)
}
settled := make(chan struct{}, 2)
h := &halves{ctx: ctx, settled: settled, branches: b}
h.settleWhenNoBranch()
if answered {
for _, f := range w.followers {
for _, f := range reader.Followers() {
if err := f.Start(ctx); err != nil {
t.Fatal(err)
}
@@ -135,3 +135,40 @@ func TestAMeshThatNamesNoBranchIsReadyWithAnEmptyView(t *testing.T) {
})
}
}
// A-368: a first view closes before the follower's change reaches the branches followed, so a registration or a gate's source that
// names a branch is no mesh that names none, though Followed holds nothing yet.
func TestAFirstViewThatNamesABranchIsNotReadyBeforeItIsFollowed(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for _, c := range []struct {
name string
source []string
regs map[string][]string
}{
{"a registration names main", nil, map[string][]string{"nats": {"main"}}},
{"the gate's source names main", []string{"main"}, map[string][]string{}},
} {
t.Run(c.name, func(t *testing.T) {
seats := &fakeSeats{source: c.source, regs: c.regs, version: 1}
w := &fakeWiring{adopted: map[string]*views.Follower{}, hears: map[string]func(context.Context, []byte) error{}}
b, err := NewBranches(w, seats.call)
if err != nil {
t.Fatal(err)
}
for _, name := range []string{"mesh-catalog.registrations", "mesh-delivery.source"} {
f, _ := w.get(name)
f.Changed = nil // the change is not yet taken into the branches followed
if err := f.Start(ctx); err != nil {
t.Fatal(err)
}
}
if len(b.Followed()) != 0 {
t.Fatalf("followed %v: the test means no branch is followed yet", b.Followed())
}
if err := b.Ready(); err == nil {
t.Error("Ready said ready with a branch named and not yet followed")
}
})
}
}
+4
View File
@@ -5317,6 +5317,10 @@ func TestAStreamThePartCreatesAndTheBusLacksIsAnEmptyFirstView(t *testing.T) {
if res.Sealed == nil {
t.Fatal("the pass says nothing of the sealed list")
}
reads := p.StreamReads()
if absent := len(res.StreamsAbsent) == 1 && len(reads) == 1 && reads[0].Absent != "" && reads[0].State == "waiting"; absent == row.held {
t.Errorf("the stream said absent = %v (pass %v, status %+v), want %v", absent, res.StreamsAbsent, reads, !row.held)
}
if held := strings.Contains(res.Sealed.Why, "a read of this pass failed"); held != row.held {
t.Errorf("held for a failed read = %v, want %v: %q", held, row.held, res.Sealed.Why)
}
@@ -215,6 +215,8 @@ type Inputs struct {
// CatalogUnread says why this pass composed before the first views of the catalog half were in (design 47 G §684); empty once they are.
// As WantsUnread, such a pass retires nothing (ReadFailures) and only adds: the list in force is kept.
CatalogUnread string
// StreamsAbsent says each stream the part reads whole that the bus lacks and the part creates, taken as empty this pass (A-371).
StreamsAbsent []string
// AssignmentUnread says why this pass composed before the first view of the assignment half was in (design 46 approval
// mark §6, design 47 G §195): no node's capability reading is known, so the list in force is kept; empty once it is.
AssignmentUnread string
+4 -1
View File
@@ -149,7 +149,9 @@ type Result struct {
// of the list in force, or stays out where it holds none (refused.go); the condition mesh-grants.principal-refused says it.
PrincipalsRefused []string `json:"principals_refused,omitempty"`
// CatalogWaiting is why this pass composed before the first views of the catalog half were in (design 47 G §684); empty once they are.
CatalogWaiting string `json:"catalog_waiting,omitempty"`
CatalogWaiting string `json:"catalog_waiting,omitempty"`
// StreamsAbsent names each stream read whole that the bus lacked, taken as empty and created after this pass (design 47 G, A-371).
StreamsAbsent []string `json:"streams_absent,omitempty"`
Errors []string `json:"errors,omitempty"`
PublishedOrAsserted string `json:"published_or_asserted"`
}
@@ -327,6 +329,7 @@ func (p *Part) Compose() Result {
in := p.inputs()
p.keepBrokers(in)
res.CatalogWaiting = in.CatalogUnread
res.StreamsAbsent = in.StreamsAbsent
if err := p.withBound(&in); err != nil {
res.Errors = append(res.Errors, err.Error())
}
+23 -1
View File
@@ -70,6 +70,9 @@ type StreamRead struct {
// State is `waiting` until the read ends, then `read`.
State string `json:"state"`
Since time.Time `json:"since"`
// Absent says why a waiting read is taken as empty: the stream is one the part creates and the bus does not hold yet, so its first
// view is empty and the stream is created after the pass (design 47 G, A-371); empty when the read waits for its answer.
Absent string `json:"absent,omitempty"`
}
// ReadingRunning begins the whole read of the node-engine's stream running at the part's start; the part acts on none of
@@ -101,6 +104,19 @@ func (p *Part) StreamReads() []StreamRead {
return []StreamRead{r}
}
// noteAbsent keeps in the stream's status whether its waiting read is taken as empty for the stream's absence.
func (p *Part) noteAbsent(absent bool) {
p.mu.Lock()
defer p.mu.Unlock()
if p.runningRead == nil {
return
}
p.runningRead.Absent = ""
if absent {
p.runningRead.Absent = "the stream is absent on the bus: its first view is taken as empty, and the part creates it after this pass"
}
}
// createsAndLacks says a stream the part reads whole at its start is one of the kept event streams it creates itself and the bus
// does not hold yet: its first view is empty, no wait (design 47 G, A-371). The pass composes, writes the part's own create right,
// and the part creates the stream and hears it from its start. A bus whose streams cannot be read leaves the read a wait.
@@ -119,7 +135,13 @@ func (p *Part) createsAndLacks(in Inputs, stream string) bool {
// readRan reads each key of StateRunning and the manifest its digest names (the indexer's, by digest), into in.Ran. A key
// whose manifest cannot be read is a gap, and that node's module is read as it was before (the trunk's).
func (p *Part) readRan(in *Inputs) {
if reads := p.StreamReads(); len(reads) > 0 && reads[0].State == "waiting" && !p.createsAndLacks(*in, reads[0].Stream) {
reads := p.StreamReads()
absent := len(reads) > 0 && reads[0].State == "waiting" && p.createsAndLacks(*in, reads[0].Stream)
p.noteAbsent(absent)
if absent {
in.StreamsAbsent = append(in.StreamsAbsent, reads[0].Stream+": absent on the bus, taken as empty; the part creates it after this pass")
}
if len(reads) > 0 && reads[0].State == "waiting" && !absent {
// The whole read of `running` has not ended: the part acts on none of it, and keeps the list it has (design 47 G §556).
in.Gaps = append(in.Gaps, Gap{Input: StateRunning, Failed: true,
Why: "the stream " + reads[0].Stream + " is waiting: its whole read at the part's start has not ended"})