The assigner's state and verb placement are assignments, with its events, tools, views and every caller; the assigner copies the retired state at its start (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
// Package assignments is the assigner's own check of assignments and of a proposal that changes it (novox/hq ADR 0567 §2,
|
||||
// ADR 0634 §1 on where a seat's code goes): what the holder of the seat mesh-module-assigner decides before it writes
|
||||
// anything. The layouts it works on are the seat's contract's (package meshmoduleassigner); what every reader of the
|
||||
// assignments shares (the keys assembled into a node's assignments, the node key's digest, who holds a seat) is the seat's
|
||||
// client's (package meshmoduleassignerclient). Nothing here is read by another module.
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// KeptApart are the modules never placed on one node, each row naming the record that asks it (novox/hq ADR 0564 clause 6
|
||||
// and Limits 5; issue 1952 S8): no manifest word says it, so the check below holds the table. mesh-watcher hears the
|
||||
// self-check's `checked` from a second machine (ADR 0227 §6), which on the self-check's own node would fall silent with
|
||||
// it.
|
||||
var KeptApart = []struct{ A, B, Record string }{
|
||||
{A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"},
|
||||
}
|
||||
|
||||
// CheckAssignments is the holder's check of a proposal laid over every assignments (ADR 0567 §2), over the assignments by
|
||||
// node, the modules that are known and the nodes with a record. It returns every refusal, in node order, never only the
|
||||
// first: assignments naming a node not enrolled or a module that is not known (held by the indexer, or provided by the
|
||||
// mesh itself: the caller's `known` says both); two modules of one node on the same host port; a pin, binding or holding
|
||||
// naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated
|
||||
// (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private
|
||||
// network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a
|
||||
// requirement left unbound are the composition's to find (its ForeseenPorts and Unbound).
|
||||
func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool) []string {
|
||||
var refusals []string
|
||||
nodes := sortedKeys(assignments)
|
||||
placed := func(p seat.Provider) bool {
|
||||
other, ok := assignments[p.Node]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, m := range other.Modules {
|
||||
if m.Module == p.Module {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
meshSeats := map[string]string{}
|
||||
addresses := map[string]string{}
|
||||
var hubs []string
|
||||
for _, node := range nodes {
|
||||
p := assignments[node]
|
||||
if p.Node != node {
|
||||
refusals = append(refusals, fmt.Sprintf("the assignments of %s names the node %q", node, p.Node))
|
||||
}
|
||||
if a := strings.TrimSpace(p.Decided.Address); a != "" {
|
||||
if other, twice := addresses[a]; twice {
|
||||
refusals = append(refusals, fmt.Sprintf("%s and %s both have the address %s on the private network", other, node, a))
|
||||
}
|
||||
addresses[a] = node
|
||||
}
|
||||
if p.Decided.Hub {
|
||||
hubs = append(hubs, node)
|
||||
}
|
||||
if !enrolled(node) {
|
||||
refusals = append(refusals, fmt.Sprintf("%s is a node with no record", node))
|
||||
}
|
||||
placedHere := map[string]bool{}
|
||||
for _, m := range p.Modules {
|
||||
placedHere[m.Module] = true
|
||||
}
|
||||
for _, apart := range KeptApart {
|
||||
if placedHere[apart.A] && placedHere[apart.B] {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: %s and %s are never placed on one node (%s)", node, apart.A, apart.B, apart.Record))
|
||||
}
|
||||
}
|
||||
hostPorts := map[int64]string{}
|
||||
for _, m := range p.Modules {
|
||||
if !known(m.Module) {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: the indexer holds no module %s", node, m.Module))
|
||||
}
|
||||
for _, port := range sortedKeys(m.Ports) {
|
||||
at := m.Ports[port]
|
||||
if other, clash := hostPorts[at]; clash && other != m.Module {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: %s and %s are both published on host port %d", node, other, m.Module, at))
|
||||
}
|
||||
hostPorts[at] = m.Module
|
||||
}
|
||||
for _, provision := range sortedKeys(m.Bindings) {
|
||||
if to := m.Bindings[provision]; !placed(to) {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: %s's %s is bound to %s/%s, which is not placed there", node, m.Module, provision, to.Node, to.Module))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, provision := range sortedKeys(p.Pins) {
|
||||
if to := p.Pins[provision]; !placed(to) {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: the pin of %s names %s/%s, which is not placed there", node, provision, to.Node, to.Module))
|
||||
}
|
||||
}
|
||||
for _, h := range p.Holds {
|
||||
if h.Scope != "mesh" {
|
||||
continue
|
||||
}
|
||||
name := h.Claim + "/" + h.Kind
|
||||
if s, known := catalogue.SeatNamed(h.Claim); known && s.Replicated {
|
||||
// A replicated mesh seat (ADR 0223) is held on several nodes at once, each answering the same thing;
|
||||
// it is still held once on each node.
|
||||
name += "/" + node
|
||||
}
|
||||
if other, twice := meshSeats[name]; twice {
|
||||
refusals = append(refusals, fmt.Sprintf("the seat %s, held once for the mesh, is held on %s and %s", h.Claim, other, node))
|
||||
}
|
||||
meshSeats[name] = node
|
||||
}
|
||||
}
|
||||
if len(hubs) > 1 {
|
||||
refusals = append(refusals, fmt.Sprintf("the private network has one hub, and %s are each placed as it", strings.Join(hubs, ", ")))
|
||||
}
|
||||
return refusals
|
||||
}
|
||||
|
||||
// Change is what `assignments-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had
|
||||
// none) and after (0: the key was deleted), and its modules added, removed or changed against the assignments before.
|
||||
type Change struct {
|
||||
Node string `json:"node"`
|
||||
Before uint64 `json:"before"`
|
||||
After uint64 `json:"after"`
|
||||
Added []string `json:"added,omitempty"`
|
||||
Removed []string `json:"removed,omitempty"`
|
||||
Changed []string `json:"changed,omitempty"`
|
||||
}
|
||||
|
||||
// Body is the change as the seat's event carries it, one entry of its `nodes`.
|
||||
func (c Change) Body() map[string]any {
|
||||
raw, _ := json.Marshal(c)
|
||||
var out map[string]any
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// ChangeOf is what changed from before to after on one node; before is the zero Assignments for a node new to assignments.
|
||||
// A change of the node's pins or holdings counts as a change of every module placed on it both times, since the
|
||||
// composition of each may read them.
|
||||
func ChangeOf(before, after seat.Assignments) Change {
|
||||
c := Change{Node: after.Node}
|
||||
if c.Node == "" {
|
||||
c.Node = before.Node
|
||||
}
|
||||
was := map[string]seat.PlacedModule{}
|
||||
for _, m := range before.Modules {
|
||||
was[m.Module] = m
|
||||
}
|
||||
nodeWide := !jsonEqual(before.Pins, after.Pins) || !jsonEqual(before.Holds, after.Holds)
|
||||
now := map[string]bool{}
|
||||
for _, m := range after.Modules {
|
||||
now[m.Module] = true
|
||||
old, ok := was[m.Module]
|
||||
switch {
|
||||
case !ok:
|
||||
c.Added = append(c.Added, m.Module)
|
||||
case nodeWide || !jsonEqual(old, m):
|
||||
c.Changed = append(c.Changed, m.Module)
|
||||
}
|
||||
}
|
||||
for _, m := range before.Modules {
|
||||
if !now[m.Module] {
|
||||
c.Removed = append(c.Removed, m.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(c.Added)
|
||||
sort.Strings(c.Removed)
|
||||
sort.Strings(c.Changed)
|
||||
return c
|
||||
}
|
||||
|
||||
// SortChanges orders assignments-changed event's nodes by node.
|
||||
func SortChanges(c []Change) {
|
||||
sort.Slice(c, func(i, j int) bool { return c[i].Node < c[j].Node })
|
||||
}
|
||||
|
||||
func jsonEqual(a, b any) bool {
|
||||
ja, _ := json.Marshal(a)
|
||||
jb, _ := json.Marshal(b)
|
||||
return string(ja) == string(jb)
|
||||
}
|
||||
|
||||
// SeatHoldersOf is the state composed from every node's assignments (ADR 0567 §1): each holding by the seat it claims,
|
||||
// keyed as the client's SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name
|
||||
// is keyed by the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes
|
||||
// only a key that differs.
|
||||
func SeatHoldersOf(assignments map[string]seat.Assignments) map[string]seat.SeatHolders {
|
||||
out := map[string]seat.SeatHolders{}
|
||||
for _, n := range sortedKeys(assignments) {
|
||||
for _, h := range assignments[n].Holds {
|
||||
node := ""
|
||||
if h.Scope == "node" {
|
||||
node = n
|
||||
}
|
||||
s, known := catalogue.SeatNamed(h.Claim)
|
||||
name := h.Claim
|
||||
if known {
|
||||
name = s.Name
|
||||
}
|
||||
key, err := client.SeatHoldersKey(name, node)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sh := out[key]
|
||||
sh.V, sh.Seat, sh.Scope, sh.Node, sh.Outside = keys.Version, name, h.Scope, node, !known
|
||||
sh.Holders = append(sh.Holders, seat.SeatHoldersHoldersItem{Module: h.Module, Node: n})
|
||||
out[key] = sh
|
||||
}
|
||||
}
|
||||
for k, sh := range out {
|
||||
sort.Slice(sh.Holders, func(i, j int) bool {
|
||||
return sh.Holders[i].Node+"/"+sh.Holders[i].Module < sh.Holders[j].Node+"/"+sh.Holders[j].Module
|
||||
})
|
||||
out[k] = sh
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sortedKeys[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
|
||||
}
|
||||
Reference in New Issue
Block a user