diff --git a/modules/mesh-grants/cmd/mesh-grants/integration_test.go b/modules/mesh-grants/cmd/mesh-grants/integration_test.go index 6d72de3c..72ae65bf 100644 --- a/modules/mesh-grants/cmd/mesh-grants/integration_test.go +++ b/modules/mesh-grants/cmd/mesh-grants/integration_test.go @@ -193,7 +193,7 @@ func TestTheGrantsPartWhole(t *testing.T) { // has heard the seat and holds the bootstrapped definitions, and every module is composed. (The unit case holds a module // while the list is unread.) var seatAnswering atomic.Bool - seatAnswering.Store(true) + seatAnswering.Store(false) for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} { if _, err := nc.Subscribe(subject, func(m *nats.Msg) { if !seatAnswering.Load() { @@ -239,6 +239,14 @@ func TestTheGrantsPartWhole(t *testing.T) { if _, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c"); err == nil { t.Error("a membership composed before the owners answered its first views") } + // ADR 0636 §1, the unread phase: the seat was silent when the part started and speaks now, and says so on its event + // before the part's own consumer exists (the part makes it from its first composition, which waits on its owners' + // first views). The event is lost to that consumer, which starts at the stream's end, so the part reads the list again + // when its consumer is heard; mod-c, which the unread list held, is composed after that and not before. + seatAnswering.Store(true) + if _, err := js.Publish(ctx, "mesh.seat.mesh-catalog.event.seat-definition-changed", []byte(`{"seat":"mesh-broker","source":"trunk"}`)); err != nil { + t.Fatal(err) + } answering.Store(true) until("a.mod-c's membership in the part's own state memberships", func() bool { e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c") diff --git a/modules/mesh-grants/cmd/mesh-grants/main.go b/modules/mesh-grants/cmd/mesh-grants/main.go index e1cd993f..b52f0518 100644 --- a/modules/mesh-grants/cmd/mesh-grants/main.go +++ b/modules/mesh-grants/cmd/mesh-grants/main.go @@ -183,6 +183,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := hub.Start(ctx, js, account); err != nil { return err } + gate.eventsLive() hears, err := grants.OwnHears(ownStore{conn}, account.Node) if err != nil { return err diff --git a/modules/mesh-grants/cmd/mesh-grants/seatlist.go b/modules/mesh-grants/cmd/mesh-grants/seatlist.go index 3c477fb2..8d0c50a7 100644 --- a/modules/mesh-grants/cmd/mesh-grants/seatlist.go +++ b/modules/mesh-grants/cmd/mesh-grants/seatlist.go @@ -49,6 +49,18 @@ func (g *seatGate) follow(bus seatlist.Bus, changed func(), logf func(string, .. return reader } +// eventsLive is called when the part's own consumer on EVENTS is heard. A seat that was silent at the part's start may have said it +// speaks before that consumer existed, which starts at the stream's end, so its event is never heard: while the list is unread the +// part reads it now (ADR 0636 §1), on a goroutine of its own so the pass is not held; the reader changes the catalogue and wakes +// the part when the seat answers, and stays unread, to be read at the seat's next event, when it does not. +func (g *seatGate) eventsLive() { + r := g.reader.Load() + if r == nil || r.Status().Heard { + return + } + go func() { _ = r.Reload() }() +} + // tools are the two tools over whatever reader the gate holds at the call. func (g *seatGate) tools() []stdio.Tool { names := []string{grants.ToolSeatList, grants.ToolSeatListAgain}