From 9f9e34301ade19e164e412fd5e35db22490d275f Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 10 Nov 2026 02:52:19 +0100 Subject: [PATCH] S3: the seat-list reader asks again on a timeout (issue 1966, ADR 0668) Closes the gap #184 left: the read is safe to repeat, so a timed-out ask is asked again. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/seatlist/seatlist.go | 12 +++++++----- go/seatlist/seatlist_test.go | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/go/seatlist/seatlist.go b/go/seatlist/seatlist.go index a7f156f..db1048b 100644 --- a/go/seatlist/seatlist.go +++ b/go/seatlist/seatlist.go @@ -8,7 +8,8 @@ // one seat (that definition, Answer); the seat's two events, whose bodies name the seat (Changed). It calls the seat, // never the module that holds it, and only trunk definitions are held: a pull request's are the indexer's to judge. // An ask that reached no one (busretry.Undelivered: the bus's "no responders", as while the seat's holder re-serves) is -// asked again within the bus's retry bound (busretry.Bound, about 30 s, backing off; issue 774's form, issue 1966). +// asked again within the bus's retry bound (busretry.Bound, about 30 s, backing off; issue 774's form, issue 1966), and so is one +// that timed out: the ask is a read, safe to make again (busretry.DoRepeatable; novox/hq ADR 0668). // Nothing else polls or waits on a timer: a reader that could not ask says so in its Status and reads again on the // next event or on Reload, which the host offers as a tool (Tools). // @@ -361,11 +362,12 @@ func (r *Reader) ReloadIfUnread() <-chan struct{} { return done } -// ask asks the seat's verb, and again while the ask reached no one, within the bus's retry bound: a holder that re-serves -// for a few seconds while the reader starts leaves no list unread (issue 1966). An answer, a refusal or a timeout is never -// asked again. +// ask asks the seat's verb, and again while the ask reached no one or timed out, within the bus's retry bound: a holder +// that re-serves for a few seconds while the reader starts leaves no list unread (issue 1966). The ask is a read, so it +// is safe to make again on a timeout (busretry.DoRepeatable; novox/hq ADR 0668). An answer or a refusal is never asked +// again. func (r *Reader) ask(body map[string]any, what string) (json.RawMessage, error) { - return busretry.Do(what, func() (json.RawMessage, error) { + return busretry.DoRepeatable(what, func() (json.RawMessage, error) { return r.cfg.Bus.Ask(Key(), body) }, nil) } diff --git a/go/seatlist/seatlist_test.go b/go/seatlist/seatlist_test.go index 7a3548f..93a4b91 100644 --- a/go/seatlist/seatlist_test.go +++ b/go/seatlist/seatlist_test.go @@ -1,6 +1,7 @@ package seatlist import ( + "context" "encoding/json" "errors" "fmt" @@ -22,6 +23,7 @@ type fakeOwner struct { table map[string]string // seat -> definition JSON, trunk down bool silent int // when set, so many asks first reach no one (the bus's no responders), as while the holder re-serves + timeouts int // when set, so many asks first time out (a deadline passed, as a callee that restarts mid-ask) gate chan struct{} // when set, an ask waits for it to be closed asked []string // "key seat" handlers map[string]func(string, json.RawMessage) error @@ -34,6 +36,10 @@ func (f *fakeOwner) Ask(key string, body any) (json.RawMessage, error) { args, _ := body.(map[string]any) seat, _ := args["seat"].(string) f.asked = append(f.asked, key+" "+seat) + if f.timeouts > 0 { + f.timeouts-- + return nil, fmt.Errorf("asking %s: %w", key, context.DeadlineExceeded) + } if f.silent > 0 { f.silent-- return nil, fmt.Errorf("not asked: nothing on this machine answers it (%w)", nats.ErrNoResponders) @@ -287,6 +293,17 @@ func TestStartReturnsAtOnceAndTheReaderSaysItIsReadingUntilTheSeatAnswers(t *tes if st := r.Status(); !st.Heard || st.LastError != "" || !r.Has("node-b") || len(owner.asked) != 4 { t.Fatalf("after a holder that re-served: %+v, asked %q: want heard on the third ask of the list", st, owner.asked) } + + // Issue 1966, ADR 0668: a read of the list is safe to make again, so an ask that timed out is asked again too. + owner = &fakeOwner{timeouts: 1, table: map[string]string{"node-b": def("node-b", "node", "put")}} + r, err = Start(Config{Bus: owner, Bootstrap: Bootstrapped()}) + if err != nil { + t.Fatal(err) + } + <-r.Ready() + if st := r.Status(); !st.Heard || st.LastError != "" || !r.Has("node-b") || len(owner.asked) != 3 { + t.Fatalf("after an ask that timed out: %+v, asked %q: want heard on the second ask of the list", st, owner.asked) + } } // The bootstrap and the tests' seat list are copies of the same contracts files: where a seat is in both, they are equal,