Files
mesh-host/internal/link/engineseat.go
T

383 lines
13 KiB
Go

package link
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/micro"
)
// The node seat node-engine, served on the engine's own link (novox/hq ADR 0563 §1).
//
// Each verb is answered on this node's subject alone, `mesh.seat.node-engine.tool.<verb>.<node>`, and on the same
// address naming its caller, `mesh.seat.node-engine.call.<verb>.<node>.<caller>`, whose last token the bus lets each user
// publish with its own name alone (novox/hq issue 365): the caller is that token, and a call on the subject naming none is
// from an unproven caller, recorded so. The body is the call's arguments; the answer is {result, error, node}, as every
// tool's (mesh-tools node-tools). One service, `node-engine`, answers discovery with each verb's seat, tool and node.
// EngineSeat is the seat.
const EngineSeat = catalogue.EngineSeat
// EngineToolSubject is where a verb of this node's seat is asked naming no caller.
func EngineToolSubject(verb, node string) string {
return "mesh.seat." + EngineSeat + ".tool." + verb + "." + node
}
// EngineCallPattern is where a verb of this node's seat is asked naming its caller, any caller.
func EngineCallPattern(verb, node string) string {
return "mesh.seat." + EngineSeat + ".call." + verb + "." + node + ".*"
}
// EngineEventSubject is where the seat says one of its events about this node.
func EngineEventSubject(event, node string) string {
return "mesh.seat." + EngineSeat + ".event." + event + "." + node
}
// EngineCall is one call of the seat: its verb, its caller as the bus proved it, and its arguments.
type EngineCall struct {
Verb string
Caller string
Args map[string]any
}
// EngineFunc answers one call: its result, or why it is refused.
type EngineFunc func(EngineCall) (any, error)
// CallerOfSubject is the caller a call's subject names for this node's verb: the last token of a caller-named subject,
// each `~` a dot, or stdio.Unproven on the subject that names none or on any other.
func CallerOfSubject(subject, verb, node string) string {
token, ok := strings.CutPrefix(subject, "mesh.seat."+EngineSeat+".call."+verb+"."+node+".")
if !ok || token == "" || strings.ContainsAny(token, ".*> ") {
return stdio.Unproven
}
return strings.ReplaceAll(token, "~", ".")
}
// MaxEngineCall bounds a call's arguments: a path, an access and a few words.
const MaxEngineCall = 16 << 10
// AnswerEngineCall is the envelope one call is answered with: the verb's result, or its refusal in words.
func AnswerEngineCall(handle EngineFunc, verb, node, subject string, body []byte) []byte {
envelope := map[string]any{"node": node}
say := func() []byte {
raw, err := json.Marshal(envelope)
if err != nil {
raw, _ = json.Marshal(map[string]any{"node": node, "error": "the answer could not be written: " + err.Error()})
}
return raw
}
args := map[string]any{}
switch {
case handle == nil:
envelope["error"] = "this engine serves no verb of " + EngineSeat
return say()
case len(body) > MaxEngineCall:
envelope["error"] = fmt.Sprintf("the call is %d bytes, more than a call of %s takes", len(body), EngineSeat)
return say()
case len(strings.TrimSpace(string(body))) > 0:
if err := json.Unmarshal(body, &args); err != nil {
envelope["error"] = "the call's arguments are not an object: " + err.Error()
return say()
}
}
result, err := handle(EngineCall{Verb: verb, Caller: CallerOfSubject(subject, verb, node), Args: args})
if err != nil {
envelope["error"] = err.Error()
} else {
envelope["result"] = result
}
return say()
}
// EngineSeats is a link that serves this node's seat. Optional, as Asked is.
type EngineSeats interface {
ServeEngineSeat(EngineFunc)
}
// ServeEngineSeat answers every verb of the seat on this node's subjects, as the service node-engine, which says each
// verb's seat, tool and node to discovery (novox/hq ADR 0197). A bus whose user list is older than the grant refuses the
// subscriptions, said by the client; a caller then hears nobody answer.
func (l *natsLink) ServeEngineSeat(handle EngineFunc) {
if handle == nil || l.conn == nil {
return
}
svc, err := micro.AddService(l.conn, micro.Config{Name: EngineSeat, Version: "1.0.0",
Description: "the node seat node-engine of " + l.node + " (novox/hq ADR 0563)", QueueGroupDisabled: true})
if err != nil {
return
}
for _, v := range catalogue.EngineVerbs() {
verb := v.Name
answer := micro.HandlerFunc(func(r micro.Request) {
_ = r.Respond(AnswerEngineCall(handle, verb, l.node, r.Subject(), r.Data()))
})
meta := map[string]string{"seat": EngineSeat, "tool": verb, "node": l.node}
_ = svc.AddEndpoint(verb, answer, micro.WithEndpointSubject(EngineToolSubject(verb, l.node)),
micro.WithEndpointMetadata(meta))
_ = svc.AddEndpoint(verb+"-called", answer, micro.WithEndpointSubject(EngineCallPattern(verb, l.node)),
micro.WithEndpointMetadata(meta))
}
l.engine = svc
}
// SayEngine says one of the seat's events about this node, on the events stream.
func (l *natsLink) SayEngine(event string, body any) error {
raw, err := json.Marshal(body)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err = l.js.Publish(EngineEventSubject(event, l.node), raw, nats.Context(ctx))
return err
}
// The engine asks the operator under its own asker name (asks.EngineAsker) on the router's seat (ADR 0259 §3, as ADR
// 0563 §3 changes it): its ask and cancel on the seat's work stream, its warrants heard on its own decided subject, and
// its asks' state read from the router's records under its own name.
// RouterRecords is the router's bucket of asks, which each asker reads under its own name: the state `asks` of the
// operator-channel's holder, the messenger (mesh-catalog modules/messenger).
const RouterRecords = "messenger_asks"
// OperatorAsks is a link that asks the operator as this node's engine. Optional.
type OperatorAsks interface {
PublishAsk(subject string, body any, id string) error
RouterRecord(key string) (*stdio.StateEntry, error)
HearWarrants(asker string, heard func(asks.Warrant)) error
}
// PublishAsk publishes an ask or a cancel on the router's seat, once per id within the stream's duplicate window.
func (l *natsLink) PublishAsk(subject string, body any, id string) error {
raw, err := json.Marshal(body)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err = l.js.Publish(subject, raw, nats.MsgId(id), nats.Context(ctx))
return err
}
// RouterRecord reads one key of the router's records, `<asker>.<id>`; nil when it holds none.
func (l *natsLink) RouterRecord(key string) (*stdio.StateEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
msg, err := l.conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+RouterRecords+".$KV."+RouterRecords+"."+key, nil)
if err != nil {
return nil, err
}
if status := msg.Header.Get("Status"); status == "404" {
return nil, nil
} else if status != "" {
return nil, fmt.Errorf("the router's record %s could not be read: %s %s", key, status, msg.Header.Get("Description"))
}
return &stdio.StateEntry{Key: key, Value: msg.Data}, nil
}
// HearWarrants hears the warrants for asker on its decided subject, each handed to heard as it arrives.
func (l *natsLink) HearWarrants(asker string, heard func(asks.Warrant)) error {
sub, err := l.conn.Subscribe(asks.DecidedSubject(asker), func(msg *nats.Msg) {
var w asks.Warrant
if json.Unmarshal(msg.Data, &w) == nil {
heard(w)
}
})
if err != nil {
return err
}
l.warrants = sub
return nil
}
// errNoAsking is a link that cannot ask the operator.
var errNoAsking = errors.New("the link open now cannot ask the operator")
// discoveryQuiet is how long the engine waits after the last answer to the bus's discovery, and discoveryMost the
// longest it waits in all.
const (
discoveryQuiet = 300 * time.Millisecond
discoveryMost = 2 * time.Second
)
// HeardServed asks the bus's discovery (novox/hq ADR 0197) whether a seat's verb is answered on this machine: an
// endpoint naming the seat, the verb as its tool and this node.
func (l *natsLink) HeardServed(seat, verb string) (bool, error) {
inbox := l.conn.NewRespInbox()
sub, err := l.conn.SubscribeSync(inbox)
if err != nil {
return false, err
}
defer func() { _ = sub.Unsubscribe() }()
if err := l.conn.PublishRequest("$SRV.INFO", inbox, nil); err != nil {
return false, fmt.Errorf("asking the bus who serves what: %w", err)
}
heard := false
deadline := time.Now().Add(discoveryMost)
for time.Now().Before(deadline) {
msg, err := sub.NextMsg(discoveryQuiet)
if err != nil {
break
}
var info micro.Info
if json.Unmarshal(msg.Data, &info) != nil {
continue
}
for _, e := range info.Endpoints {
node := e.Metadata["node"]
if node == "" {
node = info.ID
}
if e.Metadata["seat"] == seat && e.Metadata["tool"] == verb && node == l.node {
heard = true
}
}
}
return heard, nil
}
// HeardServed is the link open now asking the bus's discovery whether a seat's verb is answered on this machine.
func (q *Session) HeardServed(seat, verb string) (bool, error) {
q.init()
q.mu.Lock()
bus := q.bus
q.mu.Unlock()
asker, ok := bus.(interface {
HeardServed(string, string) (bool, error)
})
if bus == nil || !ok {
return false, fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked)
}
return asker.HeardServed(seat, verb)
}
// ---- the engine's acts in the bus's own storage (ADR 0563 §2, §3) -------------------------------------------------
// actsStore is this node's keys of ActsBucket over the link open now: bound, never created (the grants part creates
// it, as the node-engine's declared state).
func (l *natsLink) actsStore() (nats.KeyValue, error) {
kv, err := l.js.KeyValue(ActsBucket)
if err != nil {
return nil, fmt.Errorf("the node-engine's state %s cannot be bound (it is created from the node-engine's "+
"manifest; asked again once it is): %w", ActsBucket, err)
}
return kv, nil
}
// BusState is the engine's ActStore: its own node's keys of ActsBucket, `<node>.<key>`, over whichever link the session
// has open; with none open every read and write is refused, and nothing is kept anywhere else.
type BusState struct {
Session *Session
Node string
}
func (b BusState) kv() (nats.KeyValue, error) {
b.Session.init()
b.Session.mu.Lock()
bus := b.Session.bus
b.Session.mu.Unlock()
l, ok := bus.(interface{ actsStore() (nats.KeyValue, error) })
if bus == nil || !ok {
return nil, fmt.Errorf("%w: this machine has no link to the bus open now, where its acts are kept", ErrNotAsked)
}
return l.actsStore()
}
func (b BusState) key(k string) string { return b.Node + "." + k }
// changed is a write that lost its compare-and-set, as stdio.ErrChanged.
func changed(err error) error {
var api *nats.APIError
if errors.Is(err, nats.ErrKeyExists) || (errors.As(err, &api) && api.ErrorCode == nats.JSErrCodeStreamWrongLastSequence) {
return stdio.ErrChanged
}
return err
}
func (b BusState) Get(key string) (*stdio.StateEntry, error) {
kv, err := b.kv()
if err != nil {
return nil, err
}
e, err := kv.Get(b.key(key))
if errors.Is(err, nats.ErrKeyNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &stdio.StateEntry{Key: key, Value: e.Value(), Revision: e.Revision()}, nil
}
func (b BusState) Create(key string, value any) (uint64, error) {
kv, err := b.kv()
if err != nil {
return 0, err
}
raw, err := json.Marshal(value)
if err != nil {
return 0, err
}
rev, err := kv.Create(b.key(key), raw)
return rev, changed(err)
}
func (b BusState) Update(key string, value any, revision uint64) (uint64, error) {
kv, err := b.kv()
if err != nil {
return 0, err
}
raw, err := json.Marshal(value)
if err != nil {
return 0, err
}
rev, err := kv.Update(b.key(key), raw, revision)
return rev, changed(err)
}
func (b BusState) Delete(key string) error {
kv, err := b.kv()
if err != nil {
return err
}
return kv.Delete(b.key(key))
}
// Keys are this node's keys under prefix, without the node: a watch of `<node>.>` alone, the one filter the node's
// grant names, its values left out.
func (b BusState) Keys(prefix string) ([]string, error) {
kv, err := b.kv()
if err != nil {
return nil, err
}
w, err := kv.Watch(b.Node+".>", nats.MetaOnly(), nats.IgnoreDeletes())
if err != nil {
return nil, err
}
defer func() { _ = w.Stop() }()
var out []string
for {
select {
case e := <-w.Updates():
if e == nil {
sort.Strings(out)
return out, nil
}
if k := strings.TrimPrefix(e.Key(), b.Node+"."); strings.HasPrefix(k, prefix) {
out = append(out, k)
}
case <-time.After(10 * time.Second):
return nil, errors.New("the node-engine's acts could not be listed within 10s")
}
}
}