node-tools: only a call pattern names a caller, and a fall-back for want of a responder is said (hq issue 365, review)
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered

callerOn reads a caller only under the call kind, so a tool subscription ending
in a wildcard never yields one. A caller-named call that reached nobody is said
once per subject before it is asked on the old subject, so a runtime or grant
that drifted behind is seen rather than a quiet unproven. The caller tests wait
for their bundle's exit, which reads the launcher's restart pauses another test
sets.
This commit is contained in:
2026-10-11 11:22:29 +02:00
parent ab0f888d67
commit 541de48e04
4 changed files with 89 additions and 6 deletions
+29 -4
View File
@@ -91,7 +91,11 @@ func calledPattern(subject string) string {
// callerOn is the bus user a call names, from the subject it arrived on: its last token, under a pattern
// calledPattern made. "" — said by the module as Unproven — for a call on the subject that names no caller.
func callerOn(pattern, subject string) string {
if !strings.HasSuffix(pattern, ".*") {
// Only the `call` kind names a caller: a `tool` pattern ending in a wildcard (a node seat's verb on any
// machine) is never read as one.
parts := strings.SplitN(pattern, ".", 5)
if len(parts) != 5 || parts[0] != "mesh" || (parts[1] != "mod" && parts[1] != "seat") || parts[3] != CallKind ||
!strings.HasSuffix(pattern, ".*") {
return ""
}
dot := strings.LastIndex(subject, ".")
@@ -107,9 +111,11 @@ type CalledHandler func(caller string, body json.RawMessage) (any, error)
// callers is what a connection keeps of its caller-named calls: those in flight, by subject, so a refused
// publish ends its call at once instead of after RequestTimeout.
type callers struct {
mu sync.Mutex
next uint64
pending map[string]map[uint64]context.CancelCauseFunc
mu sync.Mutex
// fellBack are the caller-named subjects already said to have reached nobody, so each is said once.
fellBack map[string]bool
next uint64
pending map[string]map[uint64]context.CancelCauseFunc
}
// errCallRefused is the bus refusing a caller-named publish: this user is not granted it yet (the controller's
@@ -176,6 +182,7 @@ func (c *Conn) askCalled(subject string, data []byte) (answered Answered, handin
case errors.Is(context.Cause(ctx), errCallRefused):
return Answered{}, false, true, nil
case errors.Is(err, nats.ErrNoResponders):
c.saidFallBack(subject)
return Answered{}, false, true, nil
case errors.Is(err, context.DeadlineExceeded) || errors.Is(err, nats.ErrTimeout):
return Answered{}, false, false, errors.New("timeout")
@@ -185,3 +192,21 @@ func (c *Conn) askCalled(subject string, data []byte) (answered Answered, handin
answered, handingOver, err = c.answerOf(subject, msg.Data)
return answered, handingOver, false, err
}
// saidFallBack says, once per subject, that a caller-named call reached nobody and is asked on the subject that
// names no caller: a runtime or holder not yet serving it, or a grant that drifted, is seen in the log rather
// than read later as a quiet "unproven" (novox/hq issue 365; expected for the controller's seat until hq issue
// 464).
func (c *Conn) saidFallBack(subject string) {
c.calls.mu.Lock()
if c.calls.fellBack == nil {
c.calls.fellBack = map[string]bool{}
}
first := !c.calls.fellBack[subject]
c.calls.fellBack[subject] = true
c.calls.mu.Unlock()
if first {
c.Logf("[mesh-tools] nothing answers the caller-named %s; asked on the subject that names no caller, where "+
"the module is told no caller (said once per subject; novox/hq issue 365)", subject)
}
}