From f917fc60c126515a209f0fa9032ef18d58fb1169 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:49:52 +0100 Subject: [PATCH] =?UTF-8?q?Tool=20runner:=20waits=20for=20the=20first=20re?= =?UTF-8?q?ad=20of=20the=20seat=20list=20on=20the=20reader's=20Ready,=20to?= =?UTF-8?q?=20a=20bound,=20and=20serves=20with=20the=20bootstrapped=20seat?= =?UTF-8?q?s=20past=20it=20(B9c,=20ADR=200636=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- node-tools/cmd/node-tools/seatlist.go | 28 +++++++-- node-tools/cmd/node-tools/seatlist_test.go | 69 ++++++++++++++++++++++ node-tools/go.mod | 1 - 3 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 node-tools/cmd/node-tools/seatlist_test.go diff --git a/node-tools/cmd/node-tools/seatlist.go b/node-tools/cmd/node-tools/seatlist.go index ad53c10..5d93ec3 100644 --- a/node-tools/cmd/node-tools/seatlist.go +++ b/node-tools/cmd/node-tools/seatlist.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seatdef" @@ -39,15 +40,34 @@ func (b busOfTheRunner) Subscribe(pattern string, handle func(string, json.RawMe return err } -// startSeatList starts the runner's reader and gives catalogue its bootstrapped seats at once. +// seatListWithin is how long the runner waits for the first read of the seat list before it serves; the read goes on +// after it, and the descriptions are the bootstrapped ones until it ends (ADR 0636 §1). +const seatListWithin = 5 * time.Second + +// startSeatList starts the runner's reader and gives catalogue its bootstrapped seats at once, then waits for the first +// read to end (the reader's Ready) for the bound, so the runner's first descriptions are of the list in the common case. func startSeatList(conn *bus.Conn) *seatlist.Reader { - reader, err := seatlist.Start(seatlist.Config{Bus: busOfTheRunner{conn}, Bootstrap: seatlist.Bootstrapped(), + timer := time.NewTimer(seatListWithin) + defer timer.Stop() + return startSeatListOn(busOfTheRunner{conn}, timer.C, log.Printf) +} + +// startSeatListOn is startSeatList over any bus, with the bound as a channel the caller gives. +func startSeatListOn(b seatlist.Bus, bound <-chan time.Time, logf func(string, ...any)) *seatlist.Reader { + reader, err := seatlist.Start(seatlist.Config{Bus: b, Bootstrap: seatlist.Bootstrapped(), OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }}) if err != nil { - log.Printf("node-tools: %v", fmt.Errorf("the tool runner cannot follow the seat list: %w", err)) + logf("node-tools: %v", fmt.Errorf("the tool runner cannot follow the seat list: %w", err)) } - if reader != nil { + if reader == nil { + return nil + } + catalogue.UseSeats(reader.Seats()) + select { + case <-reader.Ready(): catalogue.UseSeats(reader.Seats()) + case <-bound: + logf("node-tools: the seat list is not read yet; the runner serves with the bootstrapped seats and reads on") } return reader } diff --git a/node-tools/cmd/node-tools/seatlist_test.go b/node-tools/cmd/node-tools/seatlist_test.go new file mode 100644 index 0000000..fda2420 --- /dev/null +++ b/node-tools/cmd/node-tools/seatlist_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/json" + "errors" + "strings" + "sync/atomic" + "testing" + "time" +) + +// busOfAStandIn is a seat mesh-catalog that answers after release, or never (a refusal). +type busOfAStandIn struct { + release chan struct{} + asked atomic.Int32 +} + +func (b *busOfAStandIn) Ask(string, any) (json.RawMessage, error) { + b.asked.Add(1) + <-b.release + return nil, errors.New("the seat does not answer") +} + +func (b *busOfAStandIn) Subscribe(string, func(string, json.RawMessage) error) error { return nil } + +// ADR 0636 §1: the runner waits for the first read of the seat list on the reader's Ready, but only to a bound that the +// test gives as a channel; past it, it serves with the bootstrapped seats and says so, and nothing sleeps. +func TestTheRunnerWaitsForTheFirstReadOfTheSeatListOnlyToItsBound(t *testing.T) { + for _, row := range []struct { + name string + release bool // the seat ends its first read (here by refusing) before the bound + wantSaid string + wantReady bool + }{ + {"the first read ends before the bound", true, "", true}, + {"the bound passes with the seat silent", false, "the seat list is not read yet", false}, + } { + t.Run(row.name, func(t *testing.T) { + b := &busOfAStandIn{release: make(chan struct{})} + bound := make(chan time.Time, 1) + var said []string + if row.release { + close(b.release) + } else { + bound <- time.Time{} + } + r := startSeatListOn(b, bound, func(f string, a ...any) { said = append(said, f) }) + if r == nil { + t.Fatal("no reader") + } + ended := false + select { + case <-r.Ready(): + ended = true + default: + } + if ended != row.wantReady { + t.Errorf("first read ended = %v, want %v", ended, row.wantReady) + } + if got := strings.Join(said, ";"); !strings.Contains(got, row.wantSaid) || (row.wantSaid == "" && got != "") { + t.Errorf("said %q, want %q", got, row.wantSaid) + } + if !row.release { + close(b.release) + <-r.Ready() + } + }) + } +} diff --git a/node-tools/go.mod b/node-tools/go.mod index 3d1ed80..823bb94 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -9,7 +9,6 @@ require ( golang.org/x/sys v0.48.0 ) -require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect require ( git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f