On a mesh that predates the record, every handover has to begin by writing down who already holds the seat — otherwise the next holder cannot be assigned beside it, because two eligible claimants with nothing on record are refused. Found on the live mesh minutes after 0040 moved the bus seat's row: the standing holder no longer satisfies what the seat delivers, on purpose, and so could not be recorded, and so nothing could stand beside it. Recording who already holds is not making a new holder. Derivation never read what the seat delivers, so the standing holder holds regardless; when nothing is on record and the named assignment claims the seat at its scope, only that claim is checked. Every change of holder is still judged in full.
268 lines
9.0 KiB
Go
268 lines
9.0 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"`
|
|
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,
|
|
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])
|
|
}
|
|
return fmt.Errorf("seat rename <from> <to> | seat <name> --to <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.
|
|
func handOver(ctx context.Context, seatName, to string) error {
|
|
nodeName, module, ok := strings.Cut(to, "/")
|
|
if !ok || nodeName == "" || module == "" {
|
|
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
|
|
}
|
|
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)
|
|
}
|
|
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
|
|
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
|
|
}
|
|
}
|
|
|
|
// **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 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
|
|
}
|