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 | seat --to /") } // 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 /, 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 }