E-2: a heard event keeps its own time, and a refused wait is never a hot loop (issue 1966)

HearFirst reads the event's x-time as Published, notes a subscription the bus
refuses while the others go on waiting, and answers a wait refused whole only
after its timeout, in words naming the grants part's grant of every event
subject; mesh_wait lists the refused subjects in its answer.

Builds: design 34 every-event §12, §16, §18, §19 (bus/wait_test.go)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:03:13 +01:00
parent 3f89c96d20
commit cb47de8517
3 changed files with 119 additions and 58 deletions
+47 -27
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"
"time"
@@ -23,31 +24,42 @@ type WaitIssued struct {
Subject string `json:"subject"`
}
// Heard is one event a wait heard: the subject it arrived on, its body and when it arrived.
// Heard is one event a wait heard: the subject it arrived on, its body, when it arrived, and when it was published, the
// event's own header `x-time` as its emitter wrote it, empty for an event that carries none (design 34 every-event §12).
type Heard struct {
Subject string
Body json.RawMessage
At time.Time
Subject string
Body json.RawMessage
At time.Time
Published string
}
// HearFirst subscribes the subjects for this call alone and answers the first message whose body match
// accepts (every one, when match is nil), or ok false once `within` has passed. Every subscription is
// closed before it returns. A subscription the bus refuses is said as an error naming the subject, at once;
// a caller gone — ctx done — ends the wait at once with ctx's error, so nothing waits on for nobody.
func (c *Conn) HearFirst(ctx context.Context, subjects []string, within time.Duration, match func(json.RawMessage) bool) (heard Heard, ok bool, err error) {
// closed before it returns. A subscription the bus refuses is noted and the others go on waiting; refused lists
// those noted by the time it answers (design 34 every-event §16). When the bus refused every subscription of the
// call, it still waits out `within` and then answers the refusal, so a caller looping on it asks once a timeout and
// never at once again. A caller gone — ctx done — ends the wait at once with ctx's error, so nothing waits on for
// nobody.
func (c *Conn) HearFirst(ctx context.Context, subjects []string, within time.Duration, match func(json.RawMessage) bool) (heard Heard, ok bool, refused []string, err error) {
if len(subjects) == 0 {
return Heard{}, false, fmt.Errorf("a wait names at least one subject")
return Heard{}, false, nil, fmt.Errorf("a wait names at least one subject")
}
var unique []string
for _, subject := range subjects {
if !slices.Contains(unique, subject) {
unique = append(unique, subject)
}
}
got := make(chan Heard, 1)
refused := make(chan string, len(subjects))
refusals := make(chan string, len(unique))
var subs []*nats.Subscription
defer func() {
for _, s := range subs {
_ = s.Unsubscribe()
}
c.mu.Lock()
for _, subject := range subjects {
delete(c.waits[subject], refused)
for _, subject := range unique {
delete(c.waits[subject], refusals)
if len(c.waits[subject]) == 0 {
delete(c.waits, subject)
}
@@ -58,14 +70,14 @@ func (c *Conn) HearFirst(ctx context.Context, subjects []string, within time.Dur
if c.waits == nil {
c.waits = map[string]map[chan string]bool{}
}
for _, subject := range subjects {
for _, subject := range unique {
if c.waits[subject] == nil {
c.waits[subject] = map[chan string]bool{}
}
c.waits[subject][refused] = true
c.waits[subject][refusals] = true
}
c.mu.Unlock()
for _, subject := range subjects {
for _, subject := range unique {
sub, err := c.nc.Subscribe(subject, func(msg *nats.Msg) {
body := json.RawMessage(msg.Data)
if !json.Valid(body) {
@@ -76,31 +88,39 @@ func (c *Conn) HearFirst(ctx context.Context, subjects []string, within time.Dur
return
}
select {
case got <- Heard{Subject: msg.Subject, Body: body, At: time.Now().UTC()}:
case got <- Heard{Subject: msg.Subject, Body: body, At: time.Now().UTC(), Published: msg.Header.Get("x-time")}:
default:
}
})
if err != nil {
return Heard{}, false, fmt.Errorf("could not wait on %s: %w", subject, err)
return Heard{}, false, nil, fmt.Errorf("could not wait on %s: %w", subject, err)
}
subs = append(subs, sub)
}
// The bus has every subscription — or has said which it refused — once this returns.
if err := c.nc.FlushTimeout(5 * time.Second); err != nil {
return Heard{}, false, fmt.Errorf("the bus did not take the wait: %w", err)
return Heard{}, false, nil, fmt.Errorf("the bus did not take the wait: %w", err)
}
timer := time.NewTimer(within)
defer timer.Stop()
select {
case h := <-got:
return h, true, nil
case subject := <-refused:
return Heard{}, false, fmt.Errorf("the bus refused this account a subscription on %s: it is not in the "+
"account's wait grant (novox/hq ADR 0367 §2)", subject)
case <-timer.C:
return Heard{}, false, nil
case <-ctx.Done():
return Heard{}, false, ctx.Err()
for {
select {
case h := <-got:
return h, true, refused, nil
case subject := <-refusals:
if !slices.Contains(refused, subject) {
refused = append(refused, subject)
}
case <-timer.C:
if len(refused) == len(unique) {
return Heard{}, false, refused, fmt.Errorf("the bus refused this account a subscription on %s: the "+
"grants part grants this machine's tool runner every event subject (novox/hq ADR 0670 §1), so its "+
"grant is not composed yet; read the state from its owner meanwhile", strings.Join(refused, ", "))
}
return Heard{}, false, refused, nil
case <-ctx.Done():
return Heard{}, false, refused, ctx.Err()
}
}
}