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:
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user