Grants part: reads the seat list again when its own consumer is heard, since the seat's event before it is lost to it (whole case for the unread phase; ADR 0636 §1; issue 1966)

This commit is contained in:
2026-11-05 16:10:11 +01:00
parent f938a53e09
commit e2deb78caf
3 changed files with 22 additions and 1 deletions
@@ -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")
@@ -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
@@ -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}