The node's runtime is its modules' bus: it binds each module's consumer and hands its events to the bundle (hq ADR 0198)
A launched bundle's mesh/subscribe binds the module's own durable consumer — EVENTS, <node>_<module>, by name as the module's own runtime bound it, so nothing is lost or replayed in the move — and every event goes to each child of the module that subscribed as mesh/event, acknowledged only when all answered, negatively acknowledged after a short delay when one failed or died, terminated when it is not an event. mesh/ask calls a tool as the module. Every launched bundle is started again when it exits, with backoff, since long-running code waits for no call. Requires SDK 0.1.6.
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/wire"
|
||||
)
|
||||
@@ -619,3 +621,115 @@ func SeatToolSubject(seat, verb, scope, node string) string {
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// AskAs calls a tool on a module's behalf (novox/hq ADR 0198): a bare key is that module's own tool,
|
||||
// `<module>.<tool>` another's, `seat:<seat>.<verb>[@<node>]` a role's — resolved through what the
|
||||
// mesh issued that module to reach, as its own runtime resolved it, else the derived shape.
|
||||
func (c *Conn) AskAs(module, key string, body any) (Answered, error) {
|
||||
name, wanted, _ := strings.Cut(key, "@")
|
||||
subject := ""
|
||||
if m := c.Membership(module); m != nil {
|
||||
if reach := m.Reaches[name]; len(reach) > 0 {
|
||||
subject = reach[0]
|
||||
if wanted != "" {
|
||||
subject = ""
|
||||
for _, s := range reach {
|
||||
if strings.HasSuffix(s, "."+wanted) {
|
||||
subject = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if subject == "" {
|
||||
s, err := ToolSubject(key, module)
|
||||
if err != nil {
|
||||
return Answered{}, err
|
||||
}
|
||||
subject = s
|
||||
}
|
||||
return c.Ask(key, body, subject)
|
||||
}
|
||||
|
||||
// EventsStream is where every module's events land, and ConsumerOf the durable consumer the
|
||||
// controller makes for a module on a machine: the same names the module's own runtime bound
|
||||
// (node-tools/src/broker-nats.ts), so moving the module into the node's runtime neither loses an
|
||||
// event nor sees one twice.
|
||||
const EventsStream = "EVENTS"
|
||||
|
||||
// ConsumerOf is a module's durable consumer on a machine: `<node>_<module>`.
|
||||
func ConsumerOf(node, module string) string {
|
||||
if node == "" {
|
||||
node = "?"
|
||||
}
|
||||
return node + "_" + module
|
||||
}
|
||||
|
||||
// NakDelay is how long an event a handler failed waits before it is offered again: a transient cause
|
||||
// gets another attempt, a permanent one exhausts the consumer's max-deliver rather than spinning.
|
||||
var NakDelay = 5 * time.Second
|
||||
|
||||
// ConsumeAs reads a module's durable consumer and hands each event to deliver (novox/hq ADR 0198):
|
||||
// acknowledged when deliver returns nil, negatively acknowledged after NakDelay when it returns an
|
||||
// error, terminated when it is not an event at all. The consumer is the controller's to create; this
|
||||
// binds to it and never makes one. Runs until stopped.
|
||||
func (c *Conn) ConsumeAs(module string, deliver func(Envelope) error) (func(), error) {
|
||||
durable := ConsumerOf(c.node, module)
|
||||
js, err := jetstream.New(c.nc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
// Bound by name, never created and never matched against a subject: the consumer and its
|
||||
// filters are the controller's (design 29 §3), exactly as the module's own runtime bound it.
|
||||
consumer, err := js.Consumer(ctx, EventsStream, durable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("binding %s's consumer %s: %w", module, durable, err)
|
||||
}
|
||||
reading, err := consumer.Consume(func(msg jetstream.Msg) {
|
||||
env, ok := envelopeOf(msg.Subject(), msg.Headers(), msg.Data())
|
||||
if !ok {
|
||||
// Unparseable: redelivering bytes no version of a handler can read is a loop.
|
||||
_ = msg.Term()
|
||||
return
|
||||
}
|
||||
if err := deliver(env); err != nil {
|
||||
_ = msg.NakWithDelay(NakDelay)
|
||||
return
|
||||
}
|
||||
_ = msg.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s's consumer %s: %w", module, durable, err)
|
||||
}
|
||||
var once sync.Once
|
||||
return func() { once.Do(reading.Stop) }, nil
|
||||
}
|
||||
|
||||
// envelopeOf rebuilds the envelope a module sees from a delivered event: its key is the emitter and
|
||||
// the event, recovered from the subject; its metadata rides as headers (novox/hq ADR 0042).
|
||||
func envelopeOf(subject string, header nats.Header, data []byte) (Envelope, bool) {
|
||||
if !json.Valid(data) {
|
||||
return Envelope{}, false
|
||||
}
|
||||
headers := map[string]string{}
|
||||
for k := range header {
|
||||
headers[k] = header.Get(k)
|
||||
}
|
||||
return Envelope{Key: keyOf(subject), Node: headers["x-node"], Body: json.RawMessage(data), Headers: headers}, true
|
||||
}
|
||||
|
||||
// keyOf is the key a module sees for an event subject: `mesh.mod.<emitter>.event.<event>` is
|
||||
// `<emitter>.<event>` — the vocabulary its manifest names what it consumes in.
|
||||
func keyOf(subject string) string {
|
||||
before, event, found := strings.Cut(subject, ".event.")
|
||||
if !found {
|
||||
return subject
|
||||
}
|
||||
parts := strings.Split(before, ".")
|
||||
if emitter := parts[len(parts)-1]; emitter != "" {
|
||||
return emitter + "." + event
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user