A controller restart lost every call's outcome, `status` composed the mesh while its caller waited (18.6s live on 2026-10-06, past the 10s window), a repair by hand left no trace, and the core's bounds had nothing measured to be set from. - calls: kept in the controller's bucket mesh-controller_calls (last 1000 or 14 days, answers bounded to 64 KiB), read by id across a restart; a controller starting marks a stopped one's running calls abandoned; each call names its caller from the inbox its answer goes to. - status: the serving controller composes it at start, after news from a machine, a build or an acting verb, and every minute; the verb answers the last composition at once with when and how long it took. Composing resolves each machine once instead of twice. - hand-act log in mesh-controller_hand-acts: push (required through the seat), plans stop/close, broker consumer-reset and the new hand-act record take --why/--cause/--condition; `hand-acts` lists them and repeated causes; status counts the week's. - durations (migration 0066): apply (send to first report), heartbeat gap, plan tier and build, recorded as heard; `durations` summarises them. - the controller's seat row takes this binary's definition of its own verbs, so the console no longer judges calls against an older build's schema. - the controller is granted its two buckets' subjects.
198 lines
6.8 KiB
Go
198 lines
6.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/nats-io/nats.go"
|
||
|
||
"github.com/novox/mesh-controller/internal/broker"
|
||
"github.com/novox/mesh-controller/internal/link"
|
||
)
|
||
|
||
// Acts done by hand, and why (novox/hq to-be 45 §7).
|
||
//
|
||
// **Which verbs ask why.** `plans close` and `plans stop`, `broker consumer-reset` and `hand-act
|
||
// record` refuse without it everywhere: nothing automated runs them, so a call without a reason is a
|
||
// person who has not given one. A named `push` asks for it through the mesh-controller seat, which is
|
||
// how a person or an agent acts by hand on the mesh; at a shell `--why` is recorded when given and not
|
||
// required, because the installer and the lab push by command line as a step of what they do, and a
|
||
// step of a procedure is not a repair. `conditions silence` joins them when the condition store does
|
||
// (Phase 1).
|
||
|
||
// handActFlags are the flags every repairing verb takes.
|
||
type handActFlags struct {
|
||
why, cause, condition *string
|
||
}
|
||
|
||
func addHandActFlags(set *flag.FlagSet) handActFlags {
|
||
return handActFlags{
|
||
why: set.String("why", "", "why this is done by hand — recorded in the hand-act log (novox/hq to-be 45 §7)"),
|
||
cause: set.String("cause", "", "the cause, in a word or a condition's kind; the verb's own name when not given"),
|
||
condition: set.String("condition", "", "the key of the condition this act addresses, if any"),
|
||
}
|
||
}
|
||
|
||
// given is whether a reason was given.
|
||
func (f handActFlags) given() bool { return strings.TrimSpace(*f.why) != "" }
|
||
|
||
// require refuses an act without a reason, before anything is done.
|
||
func (f handActFlags) require(verb string) error {
|
||
if f.given() {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s is a repair done by hand, and says why: --why <text> (recorded in the hand-act "+
|
||
"log, novox/hq to-be 45 §7). Nothing was done", verb)
|
||
}
|
||
|
||
// handActConn is the serving controller's connection, for what it reads of the log itself; a
|
||
// command dials its own.
|
||
var handActConn *nats.Conn
|
||
|
||
// onTheBus runs f with a connection to the bus: the serving controller's, or one of its own.
|
||
func onTheBus(f func(*nats.Conn) error) error {
|
||
if handActConn != nil {
|
||
return f(handActConn)
|
||
}
|
||
address, err := broker.BusAddress()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
js, err := broker.Dial(address)
|
||
if err != nil {
|
||
return fmt.Errorf("cannot reach the bus: %w", err)
|
||
}
|
||
defer js.Close()
|
||
return f(js.Conn())
|
||
}
|
||
|
||
// record writes the entry for an act about to be done. **Before the act, and never instead of it**:
|
||
// a log that cannot be written is said loudly, and the repair it was about still happens — a mesh
|
||
// whose bus is down is exactly the mesh somebody is repairing by hand.
|
||
func (f handActFlags) record(ctx context.Context, verb string, args []string) {
|
||
if !f.given() {
|
||
return
|
||
}
|
||
act := link.HandAct{Verb: verb, Args: args, Why: strings.TrimSpace(*f.why),
|
||
Cause: strings.TrimSpace(*f.cause), Condition: strings.TrimSpace(*f.condition)}
|
||
err := onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
act = written
|
||
return err
|
||
})
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "this act by hand could NOT be recorded in the hand-act log, and is done anyway: %v\n", err)
|
||
return
|
||
}
|
||
fmt.Printf("recorded as %s in the hand-act log: %s, because %q (cause: %s)\n", act.ID, act.By, act.Why, act.Cause)
|
||
}
|
||
|
||
// handActCommand is `hand-act record` and `hand-acts`.
|
||
func handActCommand(ctx context.Context, args []string) error {
|
||
if len(args) > 0 && args[0] == "record" {
|
||
set := flag.NewFlagSet("hand-act record", flag.ContinueOnError)
|
||
f := addHandActFlags(set)
|
||
positionals, err := parseAround(set, args[1:])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
what := strings.TrimSpace(strings.Join(positionals, " "))
|
||
if what == "" {
|
||
return errors.New("hand-act record <what was done> --why <text> [--cause <word>] [--condition <key>]")
|
||
}
|
||
if err := f.require("hand-act record"); err != nil {
|
||
return err
|
||
}
|
||
if strings.TrimSpace(*f.cause) == "" {
|
||
return errors.New("hand-act record says the cause too: --cause <word>, the word a second " +
|
||
"act for the same reason will use — it is how a repair done twice is found")
|
||
}
|
||
act := link.HandAct{Verb: "hand-act record", Args: []string{what}, Why: strings.TrimSpace(*f.why),
|
||
Cause: strings.TrimSpace(*f.cause), Condition: strings.TrimSpace(*f.condition)}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
if err != nil {
|
||
return fmt.Errorf("the act could not be recorded: %w", err)
|
||
}
|
||
fmt.Printf("recorded as %s: %s did %q, because %q (cause: %s)\n", written.ID, written.By, what,
|
||
written.Why, written.Cause)
|
||
return nil
|
||
})
|
||
}
|
||
if len(args) > 0 && args[0] != "list" && !strings.HasPrefix(args[0], "-") {
|
||
return errors.New("hand-act record <what> --why <text> --cause <word> | hand-acts [--days N] [--json]")
|
||
}
|
||
if len(args) > 0 && args[0] == "list" {
|
||
args = args[1:]
|
||
}
|
||
set := flag.NewFlagSet("hand-acts", flag.ContinueOnError)
|
||
days := set.Int("days", 14, "how many days back")
|
||
asJSON := set.Bool("json", false, "as data")
|
||
if _, err := parseAround(set, args); err != nil {
|
||
return err
|
||
}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
now := time.Now()
|
||
acts, err := link.HandActs(ctx, conn, now.Add(-time.Duration(*days)*24*time.Hour))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
repeated := link.RepeatedCauses(acts, now)
|
||
if *asJSON {
|
||
body, err := json.MarshalIndent(map[string]any{"acts": acts, "repeated": repeated}, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fmt.Println(string(body))
|
||
return nil
|
||
}
|
||
if len(acts) == 0 {
|
||
fmt.Printf("nothing was done by hand in the last %d day(s)\n", *days)
|
||
return nil
|
||
}
|
||
for i := len(acts) - 1; i >= 0; i-- {
|
||
a := acts[i]
|
||
fmt.Printf("%s %s %s %s\n by %s — %s (cause: %s", a.At.Local().Format("2006-01-02 15:04"), a.ID,
|
||
a.Verb, strings.Join(a.Args, " "), a.By, a.Why, a.Cause)
|
||
if a.Condition != "" {
|
||
fmt.Printf(", condition %s", a.Condition)
|
||
}
|
||
fmt.Println(")")
|
||
}
|
||
if len(repeated) > 0 {
|
||
causes := make([]string, 0, len(repeated))
|
||
for c, n := range repeated {
|
||
causes = append(causes, fmt.Sprintf("%s ×%d", c, n))
|
||
}
|
||
sort.Strings(causes)
|
||
fmt.Printf("\ndone by hand more than once in a fortnight — a healer is wanted (to-be 45 S15): %s\n",
|
||
strings.Join(causes, ", "))
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// handActsThisWeek is how many acts were done by hand in the last seven days, for `status`; -1 when
|
||
// the log could not be read, which status says rather than reading as none.
|
||
func handActsThisWeek(ctx context.Context) (int, string) {
|
||
n := -1
|
||
err := onTheBus(func(conn *nats.Conn) error {
|
||
reading, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
acts, err := link.HandActs(reading, conn, time.Now().Add(-7*24*time.Hour))
|
||
n = len(acts)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return -1, err.Error()
|
||
}
|
||
return n, ""
|
||
}
|