Tool runner: waits for the first read of the seat list on the reader's Ready, to a bound, and serves with the bootstrapped seats past it (B9c, ADR 0636 §1; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:49:52 +01:00
parent a918352ff1
commit f917fc60c1
3 changed files with 93 additions and 5 deletions
+24 -4
View File
@@ -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
}
@@ -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()
}
})
}
}
-1
View File
@@ -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