300 lines
11 KiB
Go
300 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// What this mesh can have one of, and who fills each (novox/hq ADR 0110).
|
|
//
|
|
// **Derived every time, never stored.** A seat is held by a module assignment, so the answer is
|
|
// computed from assignments by the same resolution that decides what every machine runs. A table
|
|
// of holders kept beside the assignments would be a second copy of one fact, and the first thing
|
|
// to be wrong about it.
|
|
|
|
// seatHolder is one assignment holding a seat.
|
|
type seatHolder struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
}
|
|
|
|
// seatRow is one seat and who holds it. Unheld is an answer — "this mesh has no X" — not a fault.
|
|
type seatRow struct {
|
|
Seat string `json:"seat"`
|
|
Scope string `json:"scope"`
|
|
Delivers string `json:"delivers,omitempty"`
|
|
Decision string `json:"decision"`
|
|
// Replicated says the seat may be held on several machines at once (novox/hq ADR 0223).
|
|
Replicated bool `json:"replicated,omitempty"`
|
|
Holders []seatHolder `json:"holders"`
|
|
}
|
|
|
|
// seatsHeld is every seat the mesh defines with its holders, and every claim held that names no
|
|
// seat in the set.
|
|
//
|
|
// **The second list is not empty by construction.** Manifests are held to the set when they are
|
|
// registered, and a mesh can hold one registered before the set closed. Leaving its claim out of the
|
|
// overview would make the one thing the overview is for — what does this mesh have — quietly
|
|
// incomplete.
|
|
func seatsHeld(seats []catalogue.Seat, held []catalogue.Held) ([]seatRow, []catalogue.Held) {
|
|
rows := make([]seatRow, 0, len(seats))
|
|
for _, s := range seats {
|
|
row := seatRow{Seat: s.Name, Scope: s.Scope, Delivers: s.Delivers, Decision: s.Decision,
|
|
Replicated: s.Replicated, Holders: []seatHolder{}}
|
|
seen := map[seatHolder]bool{}
|
|
for _, h := range held {
|
|
// Resolve the held claim to a seat rather than comparing names, so a record naming a
|
|
// seat's former name groups under it after a rename (novox/hq ADR 0122).
|
|
hs, ok := catalogue.SeatNamed(h.Claim)
|
|
if !ok || hs.Name != s.Name || h.Scope != s.Scope {
|
|
continue
|
|
}
|
|
holder := seatHolder{Node: h.Node, Module: h.Module}
|
|
if !seen[holder] {
|
|
seen[holder] = true
|
|
row.Holders = append(row.Holders, holder)
|
|
}
|
|
}
|
|
sort.Slice(row.Holders, func(i, j int) bool {
|
|
if row.Holders[i].Node != row.Holders[j].Node {
|
|
return row.Holders[i].Node < row.Holders[j].Node
|
|
}
|
|
return row.Holders[i].Module < row.Holders[j].Module
|
|
})
|
|
rows = append(rows, row)
|
|
}
|
|
var outside []catalogue.Held
|
|
for _, h := range held {
|
|
// Outside the set only if it resolves to no seat at all — a former name still resolves.
|
|
if _, ok := catalogue.SeatNamed(h.Claim); !ok {
|
|
outside = append(outside, h)
|
|
}
|
|
}
|
|
sort.Slice(outside, func(i, j int) bool {
|
|
if outside[i].Claim != outside[j].Claim {
|
|
return outside[i].Claim < outside[j].Claim
|
|
}
|
|
return outside[i].Node < outside[j].Node
|
|
})
|
|
return rows, outside
|
|
}
|
|
|
|
// seatCommand changes the set — the whole point of it being data (novox/hq ADR 0122) — and, since
|
|
// ADR 0131, changes who holds a seat.
|
|
func seatCommand(ctx context.Context, args []string) error {
|
|
if len(args) == 3 && args[0] == "rename" {
|
|
from, to := args[1], args[2]
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
if err := open.inventory.RenameSeat(ctx, from, to); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s is now %s — its former name still resolves, so nothing is rebuilt, "+
|
|
"re-registered or frozen (novox/hq ADR 0122)\n", from, to)
|
|
return nil
|
|
}
|
|
if len(args) == 3 && args[1] == "--to" {
|
|
return handOver(ctx, args[0], args[2], false)
|
|
}
|
|
if len(args) == 3 && args[1] == "--add" {
|
|
return handOver(ctx, args[0], args[2], true)
|
|
}
|
|
return fmt.Errorf("seat rename <from> <to> | seat <name> --to <node>/<module> | " +
|
|
"seat <name> --add <node>/<module>")
|
|
}
|
|
|
|
// handOver makes one assignment the holder of a seat, as one act, so the seat is never without a
|
|
// holder in between (novox/hq ADR 0131, design 28 task 5.3). The control plane finds its own bus
|
|
// through one of these seats; the day it was left empty mid-change is why this exists.
|
|
//
|
|
// Everything that could make the new holder wrong is refused here, before the row is written: the
|
|
// seat must exist, the assignment must exist, and the module must be able to hold the seat —
|
|
// claim it at its scope and provide what it delivers, judged against the store's row. What is
|
|
// **not** checked is whether the module is running yet: that is what `push` confirms afterwards,
|
|
// and refusing to record a handover to a module the node has not started would make the handover
|
|
// impossible to do before the switch instead of as the switch.
|
|
//
|
|
// **Or adds one holder beside the others, for a replicated seat** (novox/hq ADR 0223): `--add`
|
|
// records the named assignment as a further holder and leaves every holder on record as it is. A
|
|
// seat held once refuses it, naming `--to`; `--to` on a replicated seat replaces every holder with
|
|
// the one named, as it always did.
|
|
func handOver(ctx context.Context, seatName, to string, adding bool) error {
|
|
nodeName, module, ok := strings.Cut(to, "/")
|
|
if !ok || nodeName == "" || module == "" {
|
|
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
|
|
}
|
|
// A kinded bench is held once per kind, by the claims themselves (novox/hq ADR 0234 §2, ADR 0259): the
|
|
// record of who holds a seat has no kind, so a handover would name one holder for every kind.
|
|
if catalogue.KindedBenches[seatName] {
|
|
return fmt.Errorf("%s is a kinded bench: each kind is held by the module claiming it, and is not handed "+
|
|
"over by `seat` — assign the module that claims the kind, or unassign the one that does", seatName)
|
|
}
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
|
|
seat, known := catalogue.SeatNamed(seatName)
|
|
if !known {
|
|
return fmt.Errorf("%q is not a seat this mesh defines — `seats` lists them", seatName)
|
|
}
|
|
if adding && !seat.Replicated {
|
|
return fmt.Errorf("%s is held once per %s, so a second holder cannot be added beside the first — "+
|
|
"`seat %s --to %s` hands it over", seat.Name, seat.Scope, seat.Name, to)
|
|
}
|
|
assigned, err := inv.Assigned(ctx, nodeName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(assigned, module) {
|
|
return fmt.Errorf("%s is not assigned to %s, so it cannot hold anything there — "+
|
|
"`assign %s %s` first", module, nodeName, nodeName, module)
|
|
}
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var m *catalogue.Manifest
|
|
for i := range entries {
|
|
if entries[i].Manifest.Module == module {
|
|
m = &entries[i].Manifest
|
|
}
|
|
}
|
|
if m == nil {
|
|
return fmt.Errorf("%s is assigned but not in the catalogue, which should not happen", module)
|
|
}
|
|
var was string
|
|
var held []string
|
|
holdings, err := inv.Holdings(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, h := range holdings {
|
|
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name {
|
|
was = h.Node
|
|
held = append(held, h.Node)
|
|
}
|
|
}
|
|
|
|
// **Recording who already holds the seat is not making a new holder, and is not judged like
|
|
// one.** On a mesh that predates the record, the first handover has to begin by writing down
|
|
// the standing holder — otherwise the next holder cannot be assigned beside it, because two
|
|
// eligible claimants with nothing on record are refused. That standing holder may no longer
|
|
// satisfy what the seat delivers (the row moved under it, on purpose, as ADR 0131's first step),
|
|
// and it holds regardless: derivation never read that column. So when nothing is on record and
|
|
// the named assignment is the one holding by derivation, only the claim itself is checked here.
|
|
// Every *change* of holder is judged in full.
|
|
claimsIt := false
|
|
for _, c := range m.Claims {
|
|
if cs, ok := catalogue.SeatNamed(c.Name); ok && cs.Name == seat.Name && c.At() == seat.Scope {
|
|
claimsIt = true
|
|
}
|
|
}
|
|
if was == "" && claimsIt {
|
|
fmt.Printf("nothing was on record for %s; recording %s on %s as its standing holder\n",
|
|
seat.Name, module, nodeName)
|
|
} else if err := catalogue.CanHold(*m, seat); err != nil {
|
|
return fmt.Errorf("%s cannot hold %s: %w", module, seat.Name, err)
|
|
}
|
|
if adding {
|
|
if err := inv.AddSeatHolder(ctx, seat.Name, seat.Scope, nodeName, module); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s is held by %s on %s, beside what was on record: %s\n", seat.Name, module, nodeName,
|
|
strings.Join(held, ", "))
|
|
fmt.Printf(" `push --behind` re-declares every machine that reads the seat's holders\n")
|
|
return nil
|
|
}
|
|
if err := inv.HoldSeat(ctx, seat.Name, seat.Scope, nodeName, module); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s is held by %s on %s\n", seat.Name, module, nodeName)
|
|
if was != "" && was != nodeName {
|
|
fmt.Printf(" `push %s` and `push %s` send both machines what changed\n", was, nodeName)
|
|
} else {
|
|
fmt.Printf(" `push %s` sends the machine what changed; every other machine that reads the "+
|
|
"seat is re-declared by `push --behind`\n", nodeName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func seatsCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("seats", flag.ContinueOnError)
|
|
asJSON := set.Bool("json", false, "the same, as JSON")
|
|
if err := set.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Every node, none excluded: the same view of what each machine holds that planning uses.
|
|
world, err := theRestOfTheMesh(ctx, inv, shelf, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, outside := seatsHeld(catalogue.Seats(), world.Held)
|
|
|
|
if *asJSON {
|
|
out := struct {
|
|
Seats []seatRow `json:"seats"`
|
|
Outside []catalogue.Held `json:"outside,omitempty"`
|
|
}{rows, outside}
|
|
body, err := json.MarshalIndent(out, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
return nil
|
|
}
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "SEAT\tSCOPE\tDELIVERS\tHELD BY")
|
|
for _, r := range rows {
|
|
delivers := r.Delivers
|
|
if delivers == "" {
|
|
delivers = "—"
|
|
}
|
|
holders := "unheld"
|
|
if len(r.Holders) > 0 {
|
|
parts := make([]string, 0, len(r.Holders))
|
|
for _, h := range r.Holders {
|
|
parts = append(parts, h.Module+" on "+h.Node)
|
|
}
|
|
holders = strings.Join(parts, ", ")
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Seat, r.Scope, delivers, holders)
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return err
|
|
}
|
|
if len(outside) > 0 {
|
|
fmt.Println("\nheld, and not a seat this mesh defines (registered before the set closed — novox/hq ADR 0110):")
|
|
for _, h := range outside {
|
|
fmt.Printf(" %s %s on %s\n", h.Claim, h.Module, h.Node)
|
|
}
|
|
}
|
|
return nil
|
|
}
|