S5: the whole node reads its seat list after a holder that re-served

The live case of ace, in process against a real bus: the stand-in of the seat
mesh-catalog answers as nothing serves it (no responders) while the node's
reader first asks, then serves; the same reader reads the list within the
bus's retry bound, with no event of the seat and no hand act (issue 1966,
ADR 0636 §8). The test of an unread list now reads the reason that names the
bootstrapped seats.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-10 00:48:14 +01:00
parent 4af6c274b8
commit a484d5f960
+34 -2
View File
@@ -7,6 +7,7 @@ import (
"crypto/ed25519"
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
@@ -20,6 +21,7 @@ import (
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -72,7 +74,10 @@ type itBed struct {
seatSilent atomic.Bool
// seatAsked counts the node's asks of the stand-in seat, answered or refused: the node reached the bus and asked.
seatAsked atomic.Int32
findings nats.KeyValue
// seatAway makes the stand-in answer as the bus answers a subject nothing serves (no responders), as while the seat's
// holder re-serves (issue 1966).
seatAway atomic.Bool
findings nats.KeyValue
}
// aServer is an embedded bus with JetStream and two users: the test's, unrestricted, and the node's, granted exactly what
@@ -918,6 +923,10 @@ func (b *itBed) standInSeat() {
}
_ = json.Unmarshal(m.Data, &q)
b.seatAsked.Add(1)
if b.seatAway.Load() {
_ = m.RespondMsg(&nats.Msg{Header: nats.Header{"Status": []string{"503"}}}) // the bus's no responders
return
}
var result any
switch {
case b.seatSilent.Load():
@@ -1002,7 +1011,14 @@ func TestNatsANodeThatCannotReadTheSeatListActsOnNothingNew(t *testing.T) {
b.mu.Lock()
defer b.mu.Unlock()
said := strings.Join(b.said, "\n")
return strings.Contains(said, selfcompose.WaitsForTheSeatListOwn) && strings.Contains(said, selfcompose.WaitsForTheSeatList)
// web's reason names the bootstrapped seats the node holds (issue 1966).
var boot []string
for _, d := range seatlist.Bootstrapped() {
boot = append(boot, d.Seat)
}
sort.Strings(boot)
return strings.Contains(said, selfcompose.WaitsForTheSeatListOwn) &&
strings.Contains(said, "web: "+fmt.Sprintf(selfcompose.WaitsForTheSeatListHeld, len(boot), strings.Join(boot, ", ")))
})
if b.file("web") != "" {
t.Fatalf("the node applied web before it read the seat list: %q", b.file("web"))
@@ -1047,6 +1063,10 @@ func TestNatsANodeThatCannotReadTheSeatListActsOnNothingNew(t *testing.T) {
// The first read of the seat list ends the wait whether or not it changed what the reader held: Start returns at once, and
// the node waits on the reader's Ready before it says the read is over (ADR 0636 §1).
//
// Issue 1966, the live case of ace: the seat's holder re-serves while the node starts, so the node's reader is answered as
// nothing serves the seat (no responders); once the holder serves again the reader reads the list within the bus's retry
// bound, with no event of the seat and no hand act.
func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
clock := &fiveMinutes{}
was := seatWaitClock
@@ -1054,8 +1074,17 @@ func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
t.Cleanup(func() { seatWaitClock = was })
b := newItBedAs(t, nodegrant.For(itNode, "it2.mailer.domain"))
b.seatAway.Store(true)
stop, _ := h8(t, b, false)
defer stop()
var st *seatListState
within(t, 5*time.Second, "the node started no reader of the seat list", func() bool { st = seatListNow.Load(); return st != nil })
asked := b.seatAsked.Load()
within(t, 10*time.Second, "the node's reader did not ask the seat while nothing served it", func() bool {
now := st.reader.Status()
return b.seatAsked.Load() > asked || (!now.Reading && !now.Heard)
})
b.seatAway.Store(false)
within(t, 30*time.Second, "the node did not end its wait at the first read of the seat list", func() bool {
st := seatListNow.Load()
if st == nil {
@@ -1065,4 +1094,7 @@ func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
waiting, _ := st.waiter.Waiting()
return st.reader.Status().Heard && !waiting
})
if seatListNow.Load() != st {
t.Error("the list was read by the reader of a run begun since, not by the one that met no responders")
}
}