Files
mesh-delivery/modules/mesh-delivery-assigner/internal/assignment/check.go
T

234 lines
8.8 KiB
Go

// 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
}