diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index 7a3551e..f8da072 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -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") + } }