Every runtime announces what it serves in the NATS services protocol; the console discovers by asking the bus (hq ADR 0197)
The Go runtime answers $SRV.PING, $SRV.INFO and $SRV.STATS (and per name and id) in the io.nats.micro.v1 format with what it serves at the moment it is asked: one service per runtime process, since the bus admits one reply per request from each responder, and one endpoint per tool per subject, its metadata saying module, seat, scope, machine, description, schema and whether the module is interchangeable. Serving is unchanged. The console gathers one $SRV.INFO request's answers instead of asking the catalogue's roster and each module's tools, and reads the controller's records as JSON for what should have answered: an assignment with tools that did not announce is named, a module without tools never is. The text parsers of node list and module list are gone. Packages share the test bus: go test -p 1.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/micro"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0197: what a runtime serves, it announces — the NATS services protocol's discovery,
|
||||
// read here with NATS's own types, each endpoint a subject actually served — and the announcement
|
||||
// follows a re-issued membership.
|
||||
func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) {
|
||||
mesh := mt.New(t)
|
||||
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil))
|
||||
mesh.Issue(t, mt.MembershipOf("beta", "anchor", false, map[string][]string{"node-shelf": {"list", "clear"}}))
|
||||
nodeTools := connect(t, "node-tools", "anchor")
|
||||
stop, err := Run(nodeTools, []Served{
|
||||
{"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}},
|
||||
{"beta", []string{mt.Fixture("many-beta.serve.mjs")}},
|
||||
}, nil, (&mt.Logs{}).Logf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
|
||||
nc, err := nats.Connect(mt.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nc.Close()
|
||||
info := func() micro.Info {
|
||||
t.Helper()
|
||||
msg, err := nc.Request("$SRV.INFO.node-tools.anchor", nil, 2*time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var i micro.Info
|
||||
if err := json.Unmarshal(msg.Data, &i); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
subjects := func(i micro.Info) string {
|
||||
var out []string
|
||||
for _, e := range i.Endpoints {
|
||||
out = append(out, e.Subject+"|"+e.QueueGroup+"|"+e.Metadata["kind"]+"|"+e.Metadata["module"]+"|"+e.Metadata["seat"]+"|"+e.Metadata["scope"]+"|"+e.Metadata["interchangeable"])
|
||||
}
|
||||
sort.Strings(out)
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
got := info()
|
||||
if got.Type != micro.InfoResponseType || got.Name != "node-tools" || got.ID != "anchor" || got.Version == "" {
|
||||
t.Errorf("identity: %+v", got.ServiceIdentity)
|
||||
}
|
||||
want := strings.Join([]string{
|
||||
"mesh.mod.alpha.tool.one.anchor||tool|alpha|||false",
|
||||
"mesh.mod.alpha.tool.two.anchor||tool|alpha|||false",
|
||||
"mesh.mod.beta.tool.five.anchor||tool|beta|||false",
|
||||
"mesh.mod.beta.tool.four.anchor||tool|beta|||false",
|
||||
"mesh.mod.beta.tool.three.anchor||tool|beta|||false",
|
||||
"mesh.seat.node-shelf.tool.clear.anchor||seat|beta|node-shelf|node|false",
|
||||
"mesh.seat.node-shelf.tool.list.anchor||seat|beta|node-shelf|node|false",
|
||||
}, "\n")
|
||||
if s := subjects(got); s != want {
|
||||
t.Errorf("announced:\n%s\nwant:\n%s", s, want)
|
||||
}
|
||||
for _, e := range got.Endpoints {
|
||||
if e.Metadata["node"] != "anchor" || e.Metadata["description"] == "" || !json.Valid([]byte(e.Metadata["schema"])) {
|
||||
t.Errorf("endpoint metadata: %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Every subject announced is answered.
|
||||
asker := connect(t, "console", "workstation")
|
||||
for _, e := range got.Endpoints {
|
||||
if _, err := asker.Ask("", map[string]any{}, e.Subject); err != nil {
|
||||
t.Errorf("announced %s and does not answer it: %v", e.Subject, err)
|
||||
}
|
||||
}
|
||||
|
||||
// PING answers with the same identity, and a request for another service is not answered.
|
||||
if msg, err := nc.Request("$SRV.PING", nil, 2*time.Second); err != nil || !strings.Contains(string(msg.Data), micro.PingResponseType) {
|
||||
t.Errorf("ping: %v %v", msg, err)
|
||||
}
|
||||
if _, err := nc.Request("$SRV.INFO.somebody-else", nil, 300*time.Millisecond); err == nil {
|
||||
t.Error("answered a request for another service")
|
||||
}
|
||||
|
||||
// alpha is re-issued a plain subject: the announcement says so, and says it is interchangeable.
|
||||
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil))
|
||||
var after string
|
||||
for i := 0; i < 40; i++ {
|
||||
after = subjects(info())
|
||||
if strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if !strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") ||
|
||||
!strings.Contains(after, "mesh.mod.alpha.tool.one.anchor||tool|alpha|||true") {
|
||||
t.Errorf("after a re-issued membership:\n%s", after)
|
||||
}
|
||||
_ = bus.Served{}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/announce"
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
"github.com/novox/mesh-tools/node-tools/internal/launch"
|
||||
)
|
||||
@@ -276,10 +277,83 @@ func Run(conn *bus.Conn, served []Served, envs map[string]map[string]string, log
|
||||
}
|
||||
logf("%s", line)
|
||||
stops = append(stops, serveSeats(conn, modules, registrations, logf))
|
||||
|
||||
// **What it serves, it announces** (novox/hq ADR 0197): the NATS services protocol's discovery,
|
||||
// answered with what is served at the moment it is asked — re-served memberships included.
|
||||
announced, err := announce.Serve(conn, announce.Service{
|
||||
Name: conn.Module(), ID: instanceOf(conn),
|
||||
Description: "the mesh's tool runtime on " + node + ": every assigned module's tools and the seats they hold",
|
||||
Metadata: map[string]string{"node": node},
|
||||
}, func() []announce.Endpoint { return endpointsOf(conn, own, registrations, modules) })
|
||||
if err != nil {
|
||||
logf("[mesh-tools] cannot announce what it serves: %v", err)
|
||||
} else {
|
||||
stops = append(stops, announced)
|
||||
}
|
||||
conn.Flush()
|
||||
return stopAll, nil
|
||||
}
|
||||
|
||||
// instanceOf is this runtime's instance on the bus: its machine, which is what tells two instances of
|
||||
// one service apart; the connection's module where it has no machine.
|
||||
func instanceOf(conn *bus.Conn) string {
|
||||
if n := conn.Node(); n != "" {
|
||||
return n
|
||||
}
|
||||
return conn.Module()
|
||||
}
|
||||
|
||||
// endpointsOf is everything this runtime serves now: each served module's tools on every subject the
|
||||
// mesh issued for them, and each held seat's verbs on the seat's subject (ADR 0197).
|
||||
func endpointsOf(conn *bus.Conn, own []registration, registrations []registration, modules []string) []announce.Endpoint {
|
||||
node := conn.Node()
|
||||
var out []announce.Endpoint
|
||||
for _, r := range own {
|
||||
for _, t := range r.tools {
|
||||
served := conn.ServedOn(r.module, t.Name)
|
||||
interchangeable := false
|
||||
for _, s := range served {
|
||||
interchangeable = interchangeable || s.Subject == "mesh.mod."+r.module+".tool."+t.Name
|
||||
}
|
||||
for _, s := range served {
|
||||
out = append(out, announce.Endpoint{Kind: announce.KindTool, Module: r.module, Tool: t.Name,
|
||||
Node: node, Description: t.Description, Schema: t.Input, Interchangeable: interchangeable,
|
||||
Subject: s.Subject, Queue: s.Queue})
|
||||
}
|
||||
}
|
||||
}
|
||||
impl := map[string]map[string]launch.Tool{}
|
||||
for _, r := range registrations {
|
||||
if impl[r.module] == nil {
|
||||
impl[r.module] = map[string]launch.Tool{}
|
||||
}
|
||||
for _, t := range r.tools {
|
||||
impl[r.module][t.Name] = t
|
||||
}
|
||||
}
|
||||
have := map[string]bool{}
|
||||
for _, module := range modules {
|
||||
m := conn.Membership(module)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
for _, v := range m.Seats {
|
||||
t, ok := impl[v.Seat][v.Verb]
|
||||
if !ok || have[v.Subject] {
|
||||
continue
|
||||
}
|
||||
have[v.Subject] = true
|
||||
scope := "mesh"
|
||||
if node != "" && strings.HasSuffix(v.Subject, "."+node) {
|
||||
scope = "node"
|
||||
}
|
||||
out = append(out, announce.Endpoint{Kind: announce.KindSeat, Module: module, Tool: v.Verb, Seat: v.Seat,
|
||||
Scope: scope, Node: node, Description: t.Description, Schema: t.Input, Subject: v.Subject})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func orNone(names []string) string {
|
||||
if len(names) == 0 {
|
||||
return "(none)"
|
||||
|
||||
Reference in New Issue
Block a user