Route a call key naming its machine after '@', and refuse a malformed key before publishing
The lease write asked seat:node-service-manager.stop@novox-be through Conn.Call, which published the '@' inside the subject: nobody served it and the call timed out after 30 s with nothing said (issue 774 notes 84-87). Every key now parses in one place, calladdress: Call routes '@node' as CallOn does, and stdio Ask refuses a key the runtime could not route, before anything is sent. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
+21
-24
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"git.novox.be/novox/mesh-sdk/go/calladdress"
|
||||
)
|
||||
|
||||
// CallTimeout bounds a call to a tool or a seat verb (mesh-tools node-tools/internal/bus/bus.go:35,
|
||||
@@ -24,9 +25,10 @@ var CallTimeout = 30 * time.Second
|
||||
// verb, or "<module>.<tool>" for a module's own tool, as a process's bus.invokes names it. The reply is
|
||||
// {result, error, node} (mesh-tools node-tools/internal/bus/bus.go answerOf); a non-empty error is the
|
||||
// call's failure. It offers no way to answer a call — a process reaches the bus only as itself, and
|
||||
// never serves anything.
|
||||
// never serves anything. A key naming its machine after '@' (`seat:node-service-manager.stop@novox`) is CallOn of that
|
||||
// machine; any other malformed key is refused before anything is published (calladdress; novox/hq issue 774).
|
||||
func (c *Conn) Call(key string, body any) (json.RawMessage, error) {
|
||||
called, fallback, err := callSubjects(key, c.callerToken())
|
||||
called, fallback, err := callSubjectsOn(key, "", c.callerToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,7 +41,7 @@ func (c *Conn) Call(key string, body any) (json.RawMessage, error) {
|
||||
// (mesh-tools node-tools/internal/bus/caller.go:15-19). node is one subject token: an empty one, or one
|
||||
// holding '.', '*', '>' or white space, is refused before anything is published.
|
||||
func (c *Conn) CallOn(node, key string, body any) (json.RawMessage, error) {
|
||||
if err := checkNode(node); err != nil {
|
||||
if err := calladdress.CheckNode(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
called, fallback, err := callSubjectsOn(key, node, c.callerToken())
|
||||
@@ -99,33 +101,28 @@ func callSubjects(key, token string) (called, fallback string, err error) {
|
||||
return callSubjectsOn(key, "", token)
|
||||
}
|
||||
|
||||
// checkNode refuses a node name that is not one subject token.
|
||||
func checkNode(node string) error {
|
||||
if node == "" || strings.ContainsAny(node, ".*> \t\r\n") {
|
||||
return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>' or space)", node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// callSubjectsOn is callSubjects for one named node ("" names none): the node is a token between the tool and
|
||||
// the caller on the called subject, and the last one of the fallback.
|
||||
// callSubjectsOn is callSubjects for one named node ("" names none, and then the key's own '@' names it): the node is a
|
||||
// token between the tool and the caller on the called subject, and the last one of the fallback. A key naming another
|
||||
// machine than node is refused.
|
||||
func callSubjectsOn(key, node, token string) (called, fallback string, err error) {
|
||||
a, err := calladdress.Parse(key)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
switch {
|
||||
case a.Node != "" && node != "" && a.Node != node:
|
||||
return "", "", fmt.Errorf("%q names the machine %s, and it is asked of %s", key, a.Node, node)
|
||||
case a.Node != "":
|
||||
node = a.Node
|
||||
}
|
||||
at := ""
|
||||
if node != "" {
|
||||
at = "." + node
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(key, "seat:"); ok {
|
||||
seat, verb, ok := strings.Cut(rest, ".")
|
||||
if !ok || seat == "" || verb == "" {
|
||||
return "", "", fmt.Errorf("%q names no seat and verb (seat:<seat>.<verb>)", key)
|
||||
}
|
||||
return "mesh.seat." + seat + ".call." + verb + at + "." + token, "mesh.seat." + seat + ".tool." + verb + at, nil
|
||||
if a.Seat != "" {
|
||||
return "mesh.seat." + a.Seat + ".call." + a.Verb + at + "." + token, "mesh.seat." + a.Seat + ".tool." + a.Verb + at, nil
|
||||
}
|
||||
module, tool, ok := strings.Cut(key, ".")
|
||||
if !ok || module == "" || tool == "" {
|
||||
return "", "", fmt.Errorf("%q is not <module>.<tool>, and does not start seat:<seat>.<verb>", key)
|
||||
}
|
||||
return "mesh.mod." + module + ".call." + tool + at + "." + token, "mesh.mod." + module + ".tool." + tool + at, nil
|
||||
return "mesh.mod." + a.Module + ".call." + a.Tool + at + "." + token, "mesh.mod." + a.Module + ".tool." + a.Tool + at, nil
|
||||
}
|
||||
|
||||
// request asks one subject once and reads its reply ({result, error, node}); a reply naming an error is
|
||||
|
||||
@@ -237,7 +237,8 @@ func TestReleaseAcceptanceAProcessDoesExactlyItsBusDeclaration(t *testing.T) {
|
||||
}
|
||||
|
||||
// 1470/AC4: two answerers of the same verb on the subjects of nodes ace and g14; CallOn naming g14 gets g14's
|
||||
// answer, and ace's answerer hears nothing.
|
||||
// answer, and ace's answerer hears nothing. Issue 774 notes 84-87: Call with the key `<verb>@g14` is published on the
|
||||
// same subject, never on one holding the '@' that nobody serves.
|
||||
func TestCallOnReachesTheNamedNodeAlone(t *testing.T) {
|
||||
url, account := grantedSendProcess(t)
|
||||
adminNC, _ := adminJS(t, url)
|
||||
@@ -271,6 +272,9 @@ func TestCallOnReachesTheNamedNodeAlone(t *testing.T) {
|
||||
if string(result) != `{"node":"g14"}` {
|
||||
t.Fatalf("result: %s, want g14's answer", result)
|
||||
}
|
||||
if result, err = conn.Call("seat:operator-channel.tell@g14", map[string]any{}); err != nil || string(result) != `{"node":"g14"}` {
|
||||
t.Fatalf("Call with @g14: %s, %v, want g14's answer", result, err)
|
||||
}
|
||||
select {
|
||||
case <-aceHeard:
|
||||
t.Error("ace's answerer heard a call named for g14")
|
||||
|
||||
+15
-1
@@ -73,17 +73,31 @@ func TestCallSubjectsOnANodeForASeatVerb(t *testing.T) {
|
||||
if called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" {
|
||||
t.Errorf("called %q, fallback %q", called, fallback)
|
||||
}
|
||||
// Issue 774 notes 84-87: the machine named in the key after '@' is the same subject, never a token holding '@'.
|
||||
called, fallback, err = callSubjects("seat:node-files.read@g14", "tok")
|
||||
if err != nil || called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" {
|
||||
t.Errorf("@g14: called %q, fallback %q, %v", called, fallback, err)
|
||||
}
|
||||
if _, _, err := callSubjectsOn("seat:node-files.read@ace", "g14", "tok"); err == nil {
|
||||
t.Error("a key naming ace was asked of g14")
|
||||
}
|
||||
}
|
||||
|
||||
// 1470/AC3: a node that is not one token is refused before anything is published (the Conn has no
|
||||
// connection, so only the name check can answer with this error).
|
||||
func TestCallOnRefusesANodeThatIsNotOneToken(t *testing.T) {
|
||||
c := &Conn{}
|
||||
for _, node := range []string{"", "a.b", "*", ">", "a b", "a\tb"} {
|
||||
for _, node := range []string{"", "a.b", "*", ">", "a b", "a\tb", "a@b"} {
|
||||
if _, err := c.CallOn(node, "mesh-build-go.fetch", nil); err == nil || !strings.Contains(err.Error(), "not a node name") {
|
||||
t.Errorf("CallOn(%q) = %v, want the refusal of the name", node, err)
|
||||
}
|
||||
}
|
||||
// Issue 774 notes 84-87: Call refuses a malformed key the same way, before anything is published.
|
||||
for _, key := range []string{"seat:node-service-manager.stop@novox-be.lan", "seat:a.b@x@y", "seat:a.*", "a.b@"} {
|
||||
if _, err := c.Call(key, nil); err == nil {
|
||||
t.Errorf("Call(%q) was published", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The review of 2026-10-30 (issue 774): a callee's own answer is delivered, whatever its words, and is never asked
|
||||
|
||||
Reference in New Issue
Block a user