mesh/delivery-group group adr-0307-state-history delivered: every member is delivered
mesh/delivery delivered
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/state.history answers every value a bucket still keeps for a key, oldest first, to whoever may get it, by the same watch the runtime already uses, so no new grant is needed.
871 lines
28 KiB
Go
871 lines
28 KiB
Go
// Package runtime is the node's tool runtime (novox/hq ADR 0175, ADR 0193), the Go port of
|
|
// node-tools' runtime.ts in its launch-only form: it launches every assigned module's bundle,
|
|
// serves each module's tools on that module's subjects and each held seat's verbs on the seat's,
|
|
// and answers for every module the verb that says what it serves. It imports nothing and knows no
|
|
// language.
|
|
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"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"
|
|
)
|
|
|
|
// ToolsVerb is the verb every module's runtime answers for it (ADR 0152): its tools, from the code
|
|
// that answers them.
|
|
const ToolsVerb = "tools"
|
|
|
|
// Words the mesh sets for the runtime (ADR 0175, ADR 0192).
|
|
const (
|
|
ToolModules = "MESH_TOOL_MODULES"
|
|
ToolEnv = "MESH_TOOL_ENV"
|
|
OperatorAccount = "MESH_OPERATOR_ACCOUNT"
|
|
OperatorHome = "MESH_OPERATOR_HOME"
|
|
)
|
|
|
|
// Served is one module this runtime serves and its entrypoints.
|
|
type Served struct {
|
|
Module string
|
|
Entrypoints []string
|
|
}
|
|
|
|
// ToolsAnswer is what `tools` answers for one module.
|
|
type ToolsAnswer struct {
|
|
Module string `json:"module"`
|
|
Tools []ListedTool `json:"tools"`
|
|
Failed string `json:"failed,omitempty"`
|
|
}
|
|
|
|
// ListedTool is one tool as a module's `tools` answer lists it.
|
|
type ListedTool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Input json.RawMessage `json:"input"`
|
|
Subjects []string `json:"subjects,omitempty"`
|
|
}
|
|
|
|
// NodeRuntimeModule is the module that is a machine's runtime, serving every module on it.
|
|
const NodeRuntimeModule = "node-tools"
|
|
|
|
// runtimeOnly are the words of the runtime's own environment no bundle is given.
|
|
var runtimeOnly = map[string]bool{ToolEnv: true, ToolModules: true, "MESH_BROKER_FILE": true,
|
|
"MESH_BROKER_URL": true, "MESH_CONSOLE_LISTEN": true}
|
|
|
|
// ServedModulesFrom reads MESH_TOOL_MODULES: `<module>=<entrypoint>` entries, comma-separated, several
|
|
// per module. The one-module form — a bare path, or the runtime's own module — is the per-module
|
|
// containers' (to-be 38 WP4c) and refused here: the node's runtime imports nothing.
|
|
func ServedModulesFrom(spec, own string) ([]Served, error) {
|
|
order := []string{}
|
|
by := map[string][]string{}
|
|
for _, raw := range strings.Split(spec, ",") {
|
|
entry := strings.TrimSpace(raw)
|
|
if entry == "" {
|
|
continue
|
|
}
|
|
module, path, ok := strings.Cut(entry, "=")
|
|
module, path = strings.TrimSpace(module), strings.TrimSpace(path)
|
|
if !ok || module == "" || path == "" {
|
|
return nil, fmt.Errorf("%s: %q is not <module>=<entrypoint>; the runtime launches the bundles it "+
|
|
"is given and imports nothing (novox/hq ADR 0193)", ToolModules, entry)
|
|
}
|
|
// The node's runtime serves the machine's modules and never itself. **A runtime on a module's own
|
|
// account serves that module and nothing else** (novox/hq ADR 0259 §8): the router and a channel that
|
|
// proves its sender reach the bus on an account of their own, never the machine's runtime.
|
|
switch {
|
|
case own == NodeRuntimeModule && module == own:
|
|
return nil, fmt.Errorf("%s: %q is the runtime itself; it launches the bundles it is given and "+
|
|
"imports nothing (novox/hq ADR 0193)", ToolModules, entry)
|
|
case own != NodeRuntimeModule && module != own:
|
|
return nil, fmt.Errorf("%s: %q is another module's; a runtime on %s's own account serves %s alone "+
|
|
"(novox/hq ADR 0259)", ToolModules, entry, own, own)
|
|
}
|
|
if _, seen := by[module]; !seen {
|
|
order = append(order, module)
|
|
}
|
|
by[module] = append(by[module], path)
|
|
}
|
|
out := make([]Served, 0, len(order))
|
|
for _, m := range order {
|
|
out = append(out, Served{Module: m, Entrypoints: by[m]})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// TakeToolEnvs reads the composed environments (ADR 0192) and removes them from the process's, so no
|
|
// bundle finds another's there.
|
|
func TakeToolEnvs() (map[string]map[string]string, error) {
|
|
raw := os.Getenv(ToolEnv)
|
|
os.Unsetenv(ToolEnv)
|
|
out := map[string]map[string]string{}
|
|
if raw == "" {
|
|
return out, nil
|
|
}
|
|
var parsed map[string]map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
return nil, fmt.Errorf(`%s is not JSON of the shape {"<module>": {"<word>": "<value>"}}: %w`, ToolEnv, err)
|
|
}
|
|
for module, words := range parsed {
|
|
own := map[string]string{}
|
|
for k, v := range words {
|
|
if s, ok := v.(string); ok {
|
|
own[k] = s
|
|
} else {
|
|
b, _ := json.Marshal(v)
|
|
own[k] = string(b)
|
|
}
|
|
}
|
|
out[module] = own
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type registration struct {
|
|
module string // the module's own name, or a seat's
|
|
owner string // the module whose bundle made it
|
|
tools []launch.Tool
|
|
}
|
|
|
|
// Run launches, binds and serves. It answers a stop function.
|
|
func Run(conn *bus.Conn, served []Served, envs map[string]map[string]string, logf func(string, ...any)) (func(), error) {
|
|
if account := os.Getenv(OperatorAccount); account != "" {
|
|
home := ""
|
|
if h := os.Getenv(OperatorHome); h != "" {
|
|
home = " (home " + h + ")"
|
|
}
|
|
logf("[mesh-tools] the operator's account here is %s%s", account, home)
|
|
}
|
|
modules := make([]string, 0, len(served))
|
|
isServed := map[string]bool{}
|
|
for _, s := range served {
|
|
modules = append(modules, s.Module)
|
|
isServed[s.Module] = true
|
|
conn.Follow(s.Module)
|
|
}
|
|
node := conn.Node()
|
|
|
|
base := os.Environ()
|
|
envFor := func(module string) []string { return BundleEnv(base, envs[module], module, node) }
|
|
|
|
// The module's events, for every child of it that subscribes (ADR 0198): one consumer per module,
|
|
// bound the first time any of its children subscribes, each event handed to every child that did.
|
|
events := &consumers{conn: conn, logf: logf, of: map[string]*moduleEvents{}}
|
|
|
|
failed := map[string]string{}
|
|
var registrations []registration
|
|
var stops []func()
|
|
for _, s := range served {
|
|
for _, entry := range s.Entrypoints {
|
|
path, _ := filepath.Abs(entry)
|
|
module := s.Module
|
|
fail := func(why string) {
|
|
failed[module] = why
|
|
logf("[mesh-tools] %s's bundle %s failed to load: %s; its tools are not served here", module, entry, why)
|
|
}
|
|
if !launch.Executable(path) {
|
|
fail(path + " is not executable; a bundle the runtime serves is started, never imported, and its build makes it executable (novox/hq ADR 0193)")
|
|
continue
|
|
}
|
|
child, err := launch.Start(module, path, envFor(module), events.forModule(module), logf)
|
|
if err != nil {
|
|
fail(err.Error())
|
|
continue
|
|
}
|
|
stops = append(stops, child.Stop)
|
|
for _, r := range child.Registrations {
|
|
registrations = append(registrations, registration{module: r.Module, owner: module, tools: r.Tools})
|
|
}
|
|
}
|
|
}
|
|
|
|
claimed := map[string]bool{}
|
|
for _, m := range modules {
|
|
if mem := conn.Membership(m); mem != nil {
|
|
for _, s := range mem.Seats {
|
|
claimed[s.Seat] = true
|
|
}
|
|
}
|
|
}
|
|
var own []registration
|
|
for _, r := range registrations {
|
|
switch {
|
|
case isServed[r.module]:
|
|
own = append(own, r)
|
|
case claimed[r.module]:
|
|
default:
|
|
logf(`[mesh-tools] %s registers tools under "%s", which is neither a module served here nor a seat one of them claims; not served until the mesh issues the claim`, r.owner, r.module)
|
|
}
|
|
}
|
|
stopAll := func() {
|
|
events.stopAll()
|
|
for i := len(stops) - 1; i >= 0; i-- {
|
|
stops[i]()
|
|
}
|
|
}
|
|
for _, r := range own {
|
|
seen := map[string]bool{}
|
|
for _, t := range r.tools {
|
|
if t.Name == ToolsVerb {
|
|
stopAll()
|
|
return nil, fmt.Errorf(`%s names a tool "%s", which is the verb the runtime answers for every module with what it serves (novox/hq ADR 0152) — refused, rename it`, r.module, ToolsVerb)
|
|
}
|
|
if seen[t.Name] {
|
|
stopAll()
|
|
return nil, fmt.Errorf("%s exposes two tools named %s — refused", r.module, t.Name)
|
|
}
|
|
seen[t.Name] = true
|
|
}
|
|
}
|
|
|
|
var names []string
|
|
byModule := map[string][]launch.Tool{}
|
|
for _, r := range own {
|
|
for _, t := range r.tools {
|
|
t := t
|
|
// Told who called, from the subject the call arrived on, never from its body (novox/hq issue 365).
|
|
stop, err := conn.HandleCalled(r.module+"."+t.Name, func(caller string, body json.RawMessage) (any, error) {
|
|
return t.Run(caller, argsOf(body))
|
|
})
|
|
if err != nil {
|
|
logf("[mesh-tools] cannot serve %s.%s: %v", r.module, t.Name, err)
|
|
continue
|
|
}
|
|
names = append(names, r.module+"."+t.Name)
|
|
stops = append(stops, stop)
|
|
}
|
|
byModule[r.module] = append(byModule[r.module], r.tools...)
|
|
}
|
|
for _, module := range modules {
|
|
module := module
|
|
tools := byModule[module]
|
|
why := failed[module]
|
|
if len(tools) == 0 && why == "" {
|
|
continue // a pure-events module: silent, as it always was
|
|
}
|
|
stop, err := conn.Handle(module+"."+ToolsVerb, func(json.RawMessage) (any, error) {
|
|
answer := ToolsAnswer{Module: module, Tools: []ListedTool{}, Failed: why}
|
|
for _, t := range tools {
|
|
answer.Tools = append(answer.Tools, ListedTool{Name: t.Name, Description: t.Description,
|
|
Input: t.Input, Subjects: subjectsOf(conn, module, t.Name)})
|
|
}
|
|
return answer, nil
|
|
})
|
|
if err == nil {
|
|
stops = append(stops, stop)
|
|
}
|
|
}
|
|
|
|
failedNames := make([]string, 0, len(failed))
|
|
for _, m := range modules {
|
|
if _, f := failed[m]; f {
|
|
failedNames = append(failedNames, m)
|
|
}
|
|
}
|
|
line := fmt.Sprintf("[mesh-tools] serving %d tool(s) for %d module(s): %s", len(names), len(modules), orNone(names))
|
|
if len(failedNames) > 0 {
|
|
line += fmt.Sprintf("; not serving %s, whose bundle(s) failed to load", strings.Join(failedNames, ", "))
|
|
}
|
|
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)"
|
|
}
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
// argsOf is a call's arguments as the tool receives them: an object, `{}` for none.
|
|
func argsOf(body json.RawMessage) json.RawMessage {
|
|
trimmed := strings.TrimSpace(string(body))
|
|
if trimmed == "" || trimmed == "null" {
|
|
return json.RawMessage("{}")
|
|
}
|
|
return body
|
|
}
|
|
|
|
// subjectsOf is where a tool is answered as the mesh issued it: the plain subject first, then this
|
|
// machine's; nothing before a membership is issued.
|
|
func subjectsOf(conn *bus.Conn, module, tool string) []string {
|
|
m := conn.Membership(module)
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
var plain, mine []string
|
|
for _, s := range m.Serves {
|
|
subject := strings.ReplaceAll(s.Subject, "{tool}", tool)
|
|
if s.Queue != "" {
|
|
plain = append(plain, subject)
|
|
} else {
|
|
mine = append(mine, subject)
|
|
}
|
|
}
|
|
return append(plain, mine...)
|
|
}
|
|
|
|
// serveSeats serves every verb of every seat a served module holds, where the mesh issued it, by
|
|
// the tool of the same name registered under the seat's name (ADR 0159, 0160) — and serves again
|
|
// whenever a membership changes. Whether this machine holds the seat is the bus's to decide.
|
|
func serveSeats(conn *bus.Conn, modules []string, registrations []registration, logf func(string, ...any)) func() {
|
|
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
|
|
}
|
|
}
|
|
var mu sync.Mutex
|
|
var stops []func()
|
|
serve := func() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for _, s := range stops {
|
|
s()
|
|
}
|
|
stops = nil
|
|
have := map[string]bool{}
|
|
for _, module := range modules {
|
|
m := conn.Membership(module)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
for _, v := range m.Seats {
|
|
if have[v.Subject] {
|
|
continue
|
|
}
|
|
have[v.Subject] = true
|
|
t, ok := impl[v.Seat][v.Verb]
|
|
if !ok {
|
|
logf("[mesh-tools] %s claims %s and implements no %s, which that seat promises; not served", module, v.Seat, v.Verb)
|
|
continue
|
|
}
|
|
stop, err := conn.HandleSubjectCalled(v.Subject, func(caller string, body json.RawMessage) (any, error) {
|
|
return t.Run(caller, argsOf(body))
|
|
})
|
|
if err != nil {
|
|
logf("[mesh-tools] cannot serve %s's %s on %s: %v", v.Seat, v.Verb, v.Subject, err)
|
|
continue
|
|
}
|
|
stops = append(stops, stop)
|
|
logf("[mesh-tools] serving %s's %s on %s, admitted where %s holds the seat", v.Seat, v.Verb, v.Subject, module)
|
|
}
|
|
}
|
|
}
|
|
serve()
|
|
conn.OnMembership(func(bus.Membership) { go func() { serve(); conn.Flush() }() })
|
|
return func() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for _, s := range stops {
|
|
s()
|
|
}
|
|
stops = nil
|
|
}
|
|
}
|
|
|
|
// consumers holds, per module the runtime serves, the one durable consumer its events arrive on and
|
|
// the children its events are handed to (novox/hq ADR 0198).
|
|
type consumers struct {
|
|
conn *bus.Conn
|
|
logf func(string, ...any)
|
|
mu sync.Mutex
|
|
of map[string]*moduleEvents
|
|
// seatBound is each module's seat traffic bound once: a worker taken, a proof subject answered.
|
|
seatBound map[string]func()
|
|
}
|
|
|
|
type moduleEvents struct {
|
|
stop func()
|
|
delivers []*func(json.RawMessage) error
|
|
}
|
|
|
|
// stopAll unbinds every module's consumer.
|
|
func (c *consumers) stopAll() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for _, m := range c.of {
|
|
if m.stop != nil {
|
|
m.stop()
|
|
}
|
|
}
|
|
c.of = map[string]*moduleEvents{}
|
|
for _, stop := range c.seatBound {
|
|
stop()
|
|
}
|
|
c.seatBound = map[string]func(){}
|
|
}
|
|
|
|
// forModule is the bus one launched bundle of a module reaches the mesh through.
|
|
func (c *consumers) forModule(module string) launch.Bus {
|
|
return &moduleBus{all: c, module: module}
|
|
}
|
|
|
|
type moduleBus struct {
|
|
all *consumers
|
|
module string
|
|
mu sync.Mutex
|
|
deliver *func(json.RawMessage) error
|
|
}
|
|
|
|
// Publish emits an event as the module (ADR 0193).
|
|
func (b *moduleBus) Publish(params json.RawMessage) error {
|
|
var env bus.Envelope
|
|
if err := json.Unmarshal(params, &env); err != nil {
|
|
return fmt.Errorf("not an event envelope: %w", err)
|
|
}
|
|
return b.all.conn.PublishAs(b.module, env)
|
|
}
|
|
|
|
// Ask calls a tool as the module: `{key, body}`, answered with the tool's result (ADR 0198).
|
|
func (b *moduleBus) Ask(params json.RawMessage) (json.RawMessage, error) {
|
|
var asked struct {
|
|
Key string `json:"key"`
|
|
Body json.RawMessage `json:"body"`
|
|
}
|
|
if err := json.Unmarshal(params, &asked); err != nil || asked.Key == "" {
|
|
return nil, fmt.Errorf("mesh/ask names no tool: {key, body}")
|
|
}
|
|
body := any(asked.Body)
|
|
if len(asked.Body) == 0 {
|
|
body = map[string]any{}
|
|
}
|
|
answered, err := b.all.conn.AskAs(b.module, asked.Key, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(answered.Result) == 0 {
|
|
return json.RawMessage("null"), nil
|
|
}
|
|
return answered.Result, nil
|
|
}
|
|
|
|
// Subscribe hands this bundle the module's events. The consumer is bound once per module; each of
|
|
// the module's children that subscribed is handed every event, and the event is acknowledged only
|
|
// when all of them took it — one consumer split between two readers would give each half.
|
|
func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error) error {
|
|
b.mu.Lock()
|
|
if b.deliver == nil {
|
|
d := deliver
|
|
b.deliver = &d
|
|
} else {
|
|
*b.deliver = deliver
|
|
}
|
|
mine := b.deliver
|
|
b.mu.Unlock()
|
|
|
|
c := b.all
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
m := c.of[b.module]
|
|
if m == nil {
|
|
m = &moduleEvents{}
|
|
c.of[b.module] = m
|
|
}
|
|
listed := false
|
|
for _, d := range m.delivers {
|
|
if d == mine {
|
|
listed = true
|
|
}
|
|
}
|
|
if !listed {
|
|
m.delivers = append(m.delivers, mine)
|
|
}
|
|
if m.stop != nil {
|
|
return nil
|
|
}
|
|
module := b.module
|
|
stop, err := c.conn.ConsumeAs(module, func(env bus.Envelope) error {
|
|
raw, err := json.Marshal(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.mu.Lock()
|
|
targets := append([]*func(json.RawMessage) error(nil), c.of[module].delivers...)
|
|
c.mu.Unlock()
|
|
for _, d := range targets {
|
|
if err := (*d)(raw); err != nil {
|
|
c.logf("[mesh-tools] %s did not take %s%s: %s; offered again in %s", module, env.Key,
|
|
eventRef(env), whyNotTaken(err), bus.NakDelay)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.stop = stop
|
|
c.logf("[mesh-tools] %s's events arrive on its consumer %s", module, bus.ConsumerOf(c.conn.Node(), module))
|
|
return nil
|
|
}
|
|
|
|
// whyNotTaken says why a bundle did not take an event, so the line names who failed: the module's
|
|
// handler, in its own words — an error it answered — or the runtime not reaching it. A bundle's
|
|
// handler error read bare ("Unexpected end of JSON input") looked like the runtime's (issue 276).
|
|
func whyNotTaken(err error) string {
|
|
var refused *launch.Refused
|
|
if errors.As(err, &refused) {
|
|
return fmt.Sprintf("its handler answered an error, in its own words: %q (an event is taken by any answer that is not an error)", refused.Message)
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
// eventRef names the event a line is about by its id, when it has one, so its offers can be told
|
|
// apart from another event's.
|
|
func eventRef(env bus.Envelope) string {
|
|
if id := env.Headers["x-event-id"]; id != "" {
|
|
return " (event " + id + ")"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stateAsked is what a bundle names when it reaches its state (ADR 0201): the state by the name its
|
|
// module uses, a key, and for a put the value.
|
|
type stateAsked struct {
|
|
State string `json:"state"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
Revision uint64 `json:"revision"`
|
|
}
|
|
|
|
// State answers a bundle's `get`, `put`, `delete`, `keys`, `create`, `update` and `history` on its
|
|
// module's state (ADR 0201, ADR 0259, ADR 0307). The runtime refuses, with the reason, a state the module was not issued and a write to one it only reads.
|
|
func (b *moduleBus) State(verb string, params json.RawMessage) (json.RawMessage, error) {
|
|
var asked stateAsked
|
|
if err := json.Unmarshal(params, &asked); err != nil || asked.State == "" {
|
|
return nil, fmt.Errorf("mesh/state.%s names no state: {state, key, value}", verb)
|
|
}
|
|
conn := b.all.conn
|
|
var answer any
|
|
switch verb {
|
|
case "get":
|
|
entry, err := conn.StateGet(b.module, asked.State, asked.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry == nil {
|
|
return json.RawMessage("null"), nil
|
|
}
|
|
answer = entry
|
|
case "history":
|
|
// Every value the bucket still keeps for the key, oldest first (novox/hq ADR 0307).
|
|
kept, err := conn.StateHistory(b.module, asked.State, asked.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = kept
|
|
case "put":
|
|
revision, err := conn.StatePut(b.module, asked.State, asked.Key, asked.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = map[string]any{"revision": revision}
|
|
case "delete":
|
|
if err := conn.StateDelete(b.module, asked.State, asked.Key); err != nil {
|
|
return nil, err
|
|
}
|
|
answer = map[string]any{}
|
|
case "keys":
|
|
keys, err := conn.StateKeys(b.module, asked.State)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = keys
|
|
case "create":
|
|
// Only when the key has no value: of two writers making it, one wins (novox/hq ADR 0259).
|
|
revision, err := conn.StateCreate(b.module, asked.State, asked.Key, asked.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = map[string]any{"revision": revision}
|
|
case "update":
|
|
// Only when the key is still at the revision read: a second answer to one ask loses (ADR 0259).
|
|
revision, err := conn.StateUpdate(b.module, asked.State, asked.Key, asked.Value, asked.Revision)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = map[string]any{"revision": revision}
|
|
default:
|
|
return nil, fmt.Errorf("the runtime answers no mesh/state.%s", verb)
|
|
}
|
|
return json.Marshal(answer)
|
|
}
|
|
|
|
// logAsked is what a bundle names when it reaches its log (novox/hq ADR 0297).
|
|
type logAsked struct {
|
|
Log string `json:"log"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
Expect *uint64 `json:"expect"`
|
|
From uint64 `json:"from"`
|
|
Keys string `json:"keys"`
|
|
Limit int `json:"limit"`
|
|
}
|
|
|
|
// Log answers a bundle's `append`, `read`, `last` and `info` on its module's log (ADR 0297). The runtime
|
|
// refuses, with the reason, a log the module was not issued; an append whose `expect` is stale is
|
|
// refused with the code of a lost state update.
|
|
func (b *moduleBus) Log(verb string, params json.RawMessage) (json.RawMessage, error) {
|
|
var asked logAsked
|
|
if err := json.Unmarshal(params, &asked); err != nil || asked.Log == "" {
|
|
return nil, fmt.Errorf("mesh/log.%s names no log: {log, ...}", verb)
|
|
}
|
|
conn := b.all.conn
|
|
var answer any
|
|
switch verb {
|
|
case "append":
|
|
seq, err := conn.LogAppend(b.module, asked.Log, asked.Key, asked.Value, asked.Expect)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = map[string]any{"sequence": seq}
|
|
case "read":
|
|
page, err := conn.LogRead(b.module, asked.Log, asked.From, asked.Keys, asked.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = page
|
|
case "last":
|
|
entry, err := conn.LogLast(b.module, asked.Log, asked.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry == nil {
|
|
return json.RawMessage("null"), nil
|
|
}
|
|
answer = entry
|
|
case "info":
|
|
info, err := conn.LogInfo(b.module, asked.Log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer = info
|
|
default:
|
|
return nil, fmt.Errorf("the runtime answers no mesh/log.%s", verb)
|
|
}
|
|
return json.Marshal(answer)
|
|
}
|
|
|
|
// Watch hands a bundle its module's state as it is and as it changes (ADR 0201): `{state, key}`, the
|
|
// key a pattern with `*` and `**`, empty for every key.
|
|
func (b *moduleBus) Watch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {
|
|
var asked stateAsked
|
|
if err := json.Unmarshal(params, &asked); err != nil || asked.State == "" {
|
|
return nil, fmt.Errorf("mesh/state.watch names no state: {state, key}")
|
|
}
|
|
return b.all.conn.StateWatch(b.module, asked.State, asked.Key, func(change bus.StateChange) error {
|
|
raw, err := json.Marshal(change)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return deliver(raw)
|
|
})
|
|
}
|
|
|
|
// seatAsked is what a bundle names for its seat traffic (novox/hq ADR 0259 §3).
|
|
type seatAsked struct {
|
|
Subject string `json:"subject"`
|
|
Body json.RawMessage `json:"body"`
|
|
ID string `json:"id"`
|
|
Worker string `json:"worker"`
|
|
Bucket string `json:"bucket"`
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
// Seat answers a bundle's seat traffic, each as the module and only as far as its membership lists:
|
|
// `publish` an accept or an event, `prove` (ask a proof), `record` (read a record kept for it), `take` a
|
|
// worker's work and `answer` a proof subject — the last two bound once per module and subject, each
|
|
// message or proof handed to the module's child that asked last (novox/hq ADR 0259 §3).
|
|
func (b *moduleBus) Seat(verb string, params json.RawMessage, handOn func(string, any) (json.RawMessage, error)) (json.RawMessage, error) {
|
|
var asked seatAsked
|
|
if err := json.Unmarshal(params, &asked); err != nil {
|
|
return nil, fmt.Errorf("mesh/seat.%s: %w", verb, err)
|
|
}
|
|
conn := b.all.conn
|
|
switch verb {
|
|
case "publish":
|
|
seq, duplicate, err := conn.SeatPublishSaid(b.module, asked.Subject, asked.Body, asked.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(map[string]any{"sequence": seq, "duplicate": duplicate})
|
|
case "prove":
|
|
return conn.SeatProve(b.module, asked.Subject, asked.Body)
|
|
case "kinds":
|
|
kinds, err := conn.SeatKinds(b.module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(kinds)
|
|
case "record":
|
|
entry, err := conn.SeatRecord(b.module, asked.Bucket, asked.Key)
|
|
if err != nil || entry == nil {
|
|
return json.RawMessage("null"), err
|
|
}
|
|
return json.Marshal(entry)
|
|
case "take":
|
|
key := "take " + asked.Worker
|
|
if b.bound(key) {
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
stop, err := conn.SeatTake(b.module, asked.Worker, func(w bus.Work) error {
|
|
_, err := handOn("mesh/work", map[string]any{"work": w})
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.bind(key, stop)
|
|
b.all.logf("[mesh-tools] %s takes its work from %s", b.module, asked.Worker)
|
|
return json.RawMessage(`{}`), nil
|
|
case "answer":
|
|
key := "answer " + asked.Subject
|
|
if b.bound(key) {
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
stop, err := conn.SeatAnswer(b.module, asked.Subject, func(subject string, body json.RawMessage) (json.RawMessage, error) {
|
|
return handOn("mesh/proof", map[string]any{"subject": subject, "body": body})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.bind(key, stop)
|
|
b.all.logf("[mesh-tools] %s answers the proofs on %s", b.module, asked.Subject)
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
return nil, fmt.Errorf("the runtime answers no mesh/seat.%s", verb)
|
|
}
|
|
|
|
// bound and bind keep, per module, what its seat traffic is bound to, so a child that starts again and
|
|
// asks again is handed what is already bound instead of a second reader of it.
|
|
func (b *moduleBus) bound(key string) bool {
|
|
b.all.mu.Lock()
|
|
defer b.all.mu.Unlock()
|
|
_, ok := b.all.seatBound[b.module+" "+key]
|
|
return ok
|
|
}
|
|
|
|
func (b *moduleBus) bind(key string, stop func()) {
|
|
b.all.mu.Lock()
|
|
defer b.all.mu.Unlock()
|
|
if b.all.seatBound == nil {
|
|
b.all.seatBound = map[string]func(){}
|
|
}
|
|
b.all.seatBound[b.module+" "+key] = stop
|
|
}
|
|
|
|
// BundleEnv is what one module's bundle is started with: the runtime's environment without its own words,
|
|
// the module's composed words over it, and the module's and machine's names. **No bus word reaches a
|
|
// bundle** (security review of 2026-10-08): the credential and the bus's address are the runtime's, and a
|
|
// bundle reaches the bus only through the runtime.
|
|
func BundleEnv(base []string, given map[string]string, module, node string) []string {
|
|
words := map[string]string{}
|
|
for _, kv := range base {
|
|
if k, v, ok := strings.Cut(kv, "="); ok && !runtimeOnly[k] {
|
|
words[k] = v
|
|
}
|
|
}
|
|
for k, v := range given {
|
|
if !runtimeOnly[k] {
|
|
words[k] = v
|
|
}
|
|
}
|
|
words["MESH_SERVED_MODULE"] = module
|
|
words["MESH_MODULE"] = module
|
|
if node != "" {
|
|
words["MESH_NODE"] = node
|
|
}
|
|
out := make([]string, 0, len(words))
|
|
for k, v := range words {
|
|
out = append(out, k+"="+v)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|