Files
mesh-catalog/modules/nats/cmd/nats-tools/broker.go
T
jschoubben 6be2e91da8 CAT-N7: the seat mesh-broker's reading verbs list, show, check and orphans, and nats_history
Served by the bus module's bundle from the keeper's own state asked and the
bus's monitoring endpoint (design 25 § 12.3): each entry's template, bus
names, settings asked and held and fill against its cap; check names each
object missing or differing; orphans what no accepted manifest asks for.
nats_history reads one entry's events, in order, from the bus module's own
stream BROKER_EVENTS on the tools' credential. The manifest test now holds
every tool and reading verb the manifest names to what the bundle serves.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:34:59 +01:00

282 lines
10 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"github.com/nats-io/nats.go/jetstream"
"nats-tools/internal/keeper"
)
// The seat mesh-broker's reading verbs (design 25 § 12.3), served by this bundle: every entry the keeper made, one
// entry, what the bus holds against what was asked, and the orphans; each read from the keeper's own state asked and
// the bus's monitoring endpoint, never from another part's state. And nats_history: every event the bus module said of
// one entry, in order.
// heldEntries reads the keeper's own state asked: every module it holds.
type heldEntries func() (map[string]keeper.Asked, error)
// HeldInState reads asked through the runtime that launched this bundle.
func HeldInState() (map[string]keeper.Asked, error) {
s := stdio.State(keeper.StateAsked)
keys, err := s.Keys()
if err != nil {
return nil, fmt.Errorf("the keeper's own %s cannot be read: %w", keeper.StateAsked, err)
}
out := map[string]keeper.Asked{}
for _, key := range keys {
if key == keeper.KeyStatus {
continue
}
e, err := s.Get(key)
if err != nil || e == nil {
continue
}
var a keeper.Asked
if json.Unmarshal(e.Value, &a) == nil {
out[key] = a
}
}
return out, nil
}
// busStream is one stream as the monitoring endpoint answers it.
type busStream struct {
Config jetstream.StreamConfig `json:"config"`
State struct {
Messages uint64 `json:"messages"`
Bytes uint64 `json:"bytes"`
} `json:"state"`
}
// streamsHeld reads every stream the bus holds from the monitoring endpoint's jsz, by name.
func streamsHeld(jsz []byte) (map[string]busStream, error) {
var doc struct {
Accounts []struct {
Streams []struct {
Name string `json:"name"`
busStream
} `json:"stream_detail"`
} `json:"account_details"`
}
if err := json.Unmarshal(jsz, &doc); err != nil {
return nil, fmt.Errorf("the bus's streams cannot be read: %w", err)
}
out := map[string]busStream{}
for _, a := range doc.Accounts {
for _, s := range a.Streams {
out[s.Name] = s.busStream
}
}
return out, nil
}
// EntryView is one entry as list and show answer it: its template, its binding, each object's settings asked and held,
// its fill against its cap, and what stands of it.
type EntryView struct {
Module string `json:"module"`
Local string `json:"local"`
Storage string `json:"storage"`
Template string `json:"template,omitempty"`
Binding any `json:"binding"`
Objects []ObjectView `json:"objects"`
State keeper.CheckLine `json:"state"`
}
// ObjectView is one bus object of an entry.
type ObjectView struct {
Stream string `json:"stream"`
Asked keeper.Settings `json:"asked"`
Held *keeper.Settings `json:"held,omitempty"`
Bytes uint64 `json:"bytes"`
Count uint64 `json:"count"`
MaxBytes int64 `json:"max_bytes"`
Differs string `json:"differs,omitempty"`
Missing bool `json:"missing,omitempty"`
}
func viewOf(module, local string, m keeper.Made, held map[string]busStream) EntryView {
v := EntryView{Module: module, Local: local, Storage: m.Plan.Storage, Template: m.Plan.Template, Binding: m.Plan.Binding,
State: keeper.CheckLine{Module: module, Local: local, Bus: keeper.BusName(module, local), Created: m.Created,
Refused: m.Refused, Waiting: m.Waiting, Drift: m.Drift, Orphaned: m.Orphaned, Full: m.Full, Fill: m.Fill}}
for _, o := range m.Plan.Objects {
ov := ObjectView{Stream: o.Stream(), Asked: o.Settings, MaxBytes: o.Settings.MaxBytes}
if s, ok := held[o.Stream()]; ok {
h := keeper.SettingsOf(o.Kind, s.Config)
ov.Held, ov.Bytes, ov.Count, ov.MaxBytes = &h, s.State.Bytes, s.State.Messages, s.Config.MaxBytes
ov.Differs = keeper.Differs(o.Settings, h)
} else {
ov.Missing = true
}
v.Objects = append(v.Objects, ov)
}
return v
}
// brokerTools are the four reading verbs and nats_history.
func brokerTools(read heldEntries, jsz func() ([]byte, error), history func(module, local string, limit int) ([]keeper.Event, error)) []stdio.Tool {
both := func() (map[string]keeper.Asked, map[string]busStream, error) {
entries, err := read()
if err != nil {
return nil, nil, err
}
doc, err := jsz()
if err != nil {
return nil, nil, fmt.Errorf("the bus's monitoring endpoint cannot be read: %w", err)
}
held, err := streamsHeld(doc)
return entries, held, err
}
each := func(entries map[string]keeper.Asked, held map[string]busStream, keep func(keeper.Made) bool) []EntryView {
var out []EntryView
for _, module := range sortedNames(entries) {
for _, local := range sortedNames(entries[module].Made) {
if m := entries[module].Made[local]; keep(m) {
out = append(out, viewOf(module, local, m, held))
}
}
}
return out
}
entryArgs := map[string]any{"module": str("the module, e.g. mesh-knowledge"), "local": str("its local name, e.g. records")}
return []stdio.Tool{
{Name: "mesh-broker.list",
Description: "Every module's kept data on the bus that the bus module created (design 25 § 12.3): each entry's template, its bus names, its settings asked and held, and its fill against its cap. Narrowed by a module.",
Input: map[string]any{"module": str("one module")},
Run: func(a map[string]any) (any, error) {
entries, held, err := both()
if err != nil {
return nil, err
}
if m := arg(a, "module"); m != "" {
entries = map[string]keeper.Asked{m: entries[m]}
}
out := each(entries, held, func(keeper.Made) bool { return true })
return map[string]any{"entries": out, "count": len(out)}, nil
}},
{Name: "mesh-broker.show",
Description: "One entry of a module's kept data on the bus: its template, its bus names, its settings asked and held, its fill against its cap, and whether it is refused, waiting, drifted, orphaned or full.",
Input: entryArgs,
Run: func(a map[string]any) (any, error) {
entries, held, err := both()
if err != nil {
return nil, err
}
module, local := arg(a, "module"), arg(a, "local")
m, ok := entries[module].Made[local]
if !ok {
return nil, fmt.Errorf("the bus module made no entry %q of %q", local, module)
}
return viewOf(module, local, m, held), nil
}},
{Name: "mesh-broker.check",
Description: "What the bus holds against what was asked, entry by entry: each object missing, or differing from its manifest's settings and how. A difference that only grows is applied by the bus module itself; any other is a drift, raised to the operator.",
Input: map[string]any{"module": str("one module")},
Run: func(a map[string]any) (any, error) {
entries, held, err := both()
if err != nil {
return nil, err
}
if m := arg(a, "module"); m != "" {
entries = map[string]keeper.Asked{m: entries[m]}
}
var differing []EntryView
all := each(entries, held, func(m keeper.Made) bool { return m.Created && !m.Orphaned })
for _, v := range all {
for _, o := range v.Objects {
if o.Missing || o.Differs != "" {
differing = append(differing, v)
break
}
}
}
return map[string]any{"checked": len(all), "differing": differing}, nil
}},
{Name: "mesh-broker.orphans",
Description: "Kept data on the bus that no accepted manifest asks for any more: each goes only by the bus module's drop, on the operator's warrant (nats_drop). Never an entry of a refused or set-aside manifest, nor a foundation part's old section.",
Run: func(map[string]any) (any, error) {
entries, held, err := both()
if err != nil {
return nil, err
}
out := each(entries, held, func(m keeper.Made) bool { return m.Orphaned })
return map[string]any{"orphans": out, "count": len(out)}, nil
}},
{Name: "nats_history",
Description: "Every event the bus module said of one entry of a module's kept data, in order (created, refused, drifted, drift-cleared, full, full-cleared, orphaned, recreated, dropped), from its own stream, kept 30 days.",
Input: map[string]any{"module": str("the module"), "local": str("its local name"), "limit": num("at most this many, the newest (default 100)")},
Run: func(a map[string]any) (any, error) {
module, local := arg(a, "module"), arg(a, "local")
if module == "" || local == "" {
return nil, fmt.Errorf("history names one entry: a module and its local name")
}
events, err := history(module, local, limitOf(a, 100))
if err != nil {
return nil, err
}
return map[string]any{"module": module, "local": local, "events": events}, nil
}},
}
}
// StreamHistory reads the bus module's events of one entry from its own stream, on the tools' own credential.
func StreamHistory(bus Bus) func(module, local string, limit int) ([]keeper.Event, error) {
return func(module, local string, limit int) ([]keeper.Event, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
js, done, err := bus(ctx)
if err != nil {
return nil, err
}
defer done()
cons, err := js.OrderedConsumer(ctx, BrokerEventsStream, jetstream.OrderedConsumerConfig{})
if err != nil {
return nil, fmt.Errorf("the bus module's stream %s cannot be read: %w", BrokerEventsStream, err)
}
info, err := cons.Info(ctx)
if err != nil {
return nil, err
}
var out []keeper.Event
for left := info.NumPending; left > 0; {
batch, err := cons.Fetch(int(min(left, 256)), jetstream.FetchMaxWait(5*time.Second))
if err != nil {
return nil, err
}
n := uint64(0)
for m := range batch.Messages() {
n++
var e keeper.Event
if json.Unmarshal(m.Data(), &e) == nil && e.Module == module && e.Local == local {
out = append(out, e)
}
}
if n == 0 {
break
}
left -= min(n, left)
}
if len(out) > limit {
out = out[len(out)-limit:]
}
return out, nil
}
}
// BrokerEventsStream is the bus module's own stream on the bus (catalogue.StreamBusName("broker-events")).
const BrokerEventsStream = "BROKER_EVENTS"
func sortedNames[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}