Node-engine: the first read of the seat list is awaited on the reader's Ready, not on Start's return, and its end is told to the waiter and the composer (B9c, ADR 0636 §1; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:45:43 +01:00
parent f29b19aff2
commit c642471c16
10 changed files with 40 additions and 1385 deletions
@@ -1019,3 +1019,25 @@ func TestNatsANodeThatCannotReadTheSeatListActsOnNothingNew(t *testing.T) {
t.Errorf("web does not stay as it ran after a start with the seat gone: %q", b.file("web"))
}
}
// 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).
func TestNatsTheFirstReadOfTheSeatListEndsTheNodesWait(t *testing.T) {
clock := &fiveMinutes{}
was := seatWaitClock
seatWaitClock = clock
t.Cleanup(func() { seatWaitClock = was })
b := newItBedAs(t, nodegrant.For(itNode, "it2.mailer.domain"))
stop, _ := h8(t, b, false)
defer stop()
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 {
return false
}
<-st.reader.Ready()
waiting, _ := st.waiter.Waiting()
return st.reader.Status().Heard && !waiting
})
}
+18 -5
View File
@@ -138,12 +138,13 @@ func (w findingsOfTheNode) Delete(f views.Finding) error {
// it holds again, saying so to the composer and to the waiter that writes the five-minute finding.
func startSeatList(ctx context.Context, session *link.Session, node string, say func(string)) (*seatListState, error) {
st := &seatListState{changed: make(chan struct{}, 1)}
var announce func()
// Start returns at once and reads the seat on a goroutine, which may announce a change before the waiter exists.
var announce atomic.Pointer[func()]
reader, err := seatlist.Start(seatlist.Config{Bus: sessionBus{ctx, session}, Bootstrap: seatlist.Bootstrapped(),
OnChange: func(seats []catalogue.Seat) {
catalogue.UseSeats(seats)
if announce != nil { // the first read happens inside Start, before the waiter exists
announce()
if f := announce.Load(); f != nil { // the first read may end before the waiter exists
(*f)()
}
}})
if err != nil {
@@ -152,15 +153,27 @@ func startSeatList(ctx context.Context, session *link.Session, node string, say
st.reader = reader
st.waiter = &seatwait.Waiter{Node: node, Observer: "node-engine", Reader: reader, Findings: findingsOfTheNode{session},
Clock: seatWaitClock, Say: say}
announce = func() {
told := func() {
st.waiter.Changed()
select {
case st.changed <- struct{}{}:
default:
}
}
announce.Store(&told)
catalogue.UseSeats(reader.Seats())
announce()
told()
// The first read ends whether the seat answered or not, and a read that held what the reader held already says no
// change: its end is told to the waiter and the composer, so a status that moved from reading to heard starts or ends
// the bound (ADR 0636 §1). Start's return held nobody's start.
go func() {
select {
case <-reader.Ready():
catalogue.UseSeats(reader.Seats())
told()
case <-ctx.Done():
}
}()
seatListNow.Store(st)
return st, nil
}