206 lines
7.3 KiB
Go
206 lines
7.3 KiB
Go
package assign
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Module is what the six rules read of one module (ADR 0429 §6, ADR 0528 clause 3): its newest fingerprint F at the
|
|
// trunk (the indexer's modules), whether the builder's build of F is done, whether F is held, whether the controller
|
|
// issued its credential for F, its manifest at F, and the nodes it is assigned to.
|
|
type Module struct {
|
|
Name string
|
|
FP string
|
|
Built bool
|
|
Held bool
|
|
Granted bool
|
|
Manifest catalogue.Manifest
|
|
Nodes []string
|
|
}
|
|
|
|
// Node is what the rules read of the nodes: whether each is online (the watchman's presence) and each module's have.
|
|
type Nodes struct {
|
|
Online map[string]bool
|
|
Haves map[string]map[string]delivery.Have
|
|
// Providers is, by provision, the modules that provide it, with the nodes each is assigned to.
|
|
Providers map[string]map[string][]string
|
|
}
|
|
|
|
// healthy says whether a have runs fp healthy.
|
|
func healthy(h delivery.Have, fp string) bool {
|
|
return h.FP == fp && h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK
|
|
}
|
|
|
|
// Canary is the module's canary: the first of its nodes, by name, that is online (ADR 0435 §3; one ring, ordered by
|
|
// name, ADR 0528 clause 3). Empty when none is online.
|
|
func Canary(nodes []string, online map[string]bool) string {
|
|
sorted := slices.Sorted(slices.Values(nodes))
|
|
for _, n := range sorted {
|
|
if online[n] {
|
|
return n
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Holds is whether every rule holds for moving node's want for m to m.FP, and if not, the first rule that does not,
|
|
// with why (ADR 0429 §6). Rule 4, at most `step` others ahead, always holds: the mesh keeps no step, and step 0 is no
|
|
// limit (ADR 0528 clause 3).
|
|
func Holds(m Module, node string, n Nodes) (bool, int, string) {
|
|
switch {
|
|
case !m.Built:
|
|
return false, delivery.RuleBuildDone, fmt.Sprintf("the build of %s at %s is not done", m.Name, short(m.FP))
|
|
case m.Held:
|
|
return false, delivery.RuleNotHeld, fmt.Sprintf("%s is held at %s", m.Name, short(m.FP))
|
|
}
|
|
canary := Canary(m.Nodes, n.Online)
|
|
switch {
|
|
case canary == "":
|
|
return false, delivery.RuleCanary, fmt.Sprintf("none of the nodes of %s is online, so it has no canary", m.Name)
|
|
case node != canary && !healthy(n.Haves[canary][m.Name], m.FP):
|
|
return false, delivery.RuleCanary, fmt.Sprintf("the canary %s does not run %s healthy yet", canary, short(m.FP))
|
|
}
|
|
for _, provision := range m.Manifest.Requires {
|
|
if ok, why := providedHealthy(provision, node, n); !ok {
|
|
return false, delivery.RuleProviders, why
|
|
}
|
|
}
|
|
if !m.Granted {
|
|
return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s for %s", m.Name, short(m.FP))
|
|
}
|
|
return true, 0, ""
|
|
}
|
|
|
|
// providedHealthy is rule 5 for one provision: some module providing it runs healthy on the node, or, assigned
|
|
// elsewhere only, at its canary. A provision no module provides is not a module's to judge (the mesh or the node
|
|
// offers it) and holds.
|
|
func providedHealthy(provision, node string, n Nodes) (bool, string) {
|
|
providers := n.Providers[provision]
|
|
if len(providers) == 0 {
|
|
return true, ""
|
|
}
|
|
names := make([]string, 0, len(providers))
|
|
for p := range providers {
|
|
names = append(names, p)
|
|
}
|
|
sort.Strings(names)
|
|
for _, p := range names {
|
|
nodes := providers[p]
|
|
at := node
|
|
if !slices.Contains(nodes, node) {
|
|
at = Canary(nodes, n.Online)
|
|
}
|
|
if h, ok := n.Haves[at][p]; ok && at != "" && h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK {
|
|
return true, ""
|
|
}
|
|
}
|
|
return false, fmt.Sprintf("no provider of %s runs healthy on %s or at its canary", provision, node)
|
|
}
|
|
|
|
// keepsData says whether a module holds data a set-back would lose: an own item of a class that keeps (irreplaceable,
|
|
// valuable or rebuildable; cache and none do not).
|
|
func keepsData(m catalogue.Manifest) bool {
|
|
for _, it := range m.DataItems() {
|
|
if it.Class == catalogue.ClassIrreplaceable || it.Class == catalogue.ClassValuable || it.Class == catalogue.ClassRebuildable {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Outcome is what one module's pass decides: each node's want to write (only those that change), and a hold to write.
|
|
type Outcome struct {
|
|
Wants map[string]delivery.Want
|
|
// Held is the hold of m.FP to write, when a node's health check failed on it and it is not held yet.
|
|
Held *delivery.Held
|
|
// Rules is, by node, the rule holding the node back from m.FP and why, for the comparison.
|
|
Rules map[string]Holding
|
|
}
|
|
|
|
// Holding is a rule holding a node back, and why.
|
|
type Holding struct {
|
|
Rule int
|
|
Words string
|
|
}
|
|
|
|
// Wants is the pass of one module m: for each of its nodes, the want by the six rules, starting from the want written
|
|
// before (wants, by node); lastHealthy is, by node, the newest fingerprint other than m.FP the node ran healthy. A failed
|
|
// health check of m.FP on a node holds it: every node wanting m.FP is set back to its last healthy fingerprint, but a
|
|
// module holding data is not set back (in release 5 it is asked about from release 6 on: wants are only compared). A
|
|
// node that wants m but is no longer among its nodes gets its want written removed, never deleted (ADR 0408 §4).
|
|
func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[string]string) Outcome {
|
|
out := Outcome{Wants: map[string]delivery.Want{}, Rules: map[string]Holding{}}
|
|
held := m.Held
|
|
if !held && m.FP != "" {
|
|
for _, node := range slices.Sorted(slices.Values(m.Nodes)) {
|
|
if h, ok := n.Haves[node][m.Name]; ok && h.FP == m.FP && h.Health == delivery.HealthFailed {
|
|
out.Held = &delivery.Held{Node: node, Words: h.Detail}
|
|
held = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
mm := m
|
|
mm.Held = held
|
|
for _, node := range m.Nodes {
|
|
if m.FP == "" {
|
|
break
|
|
}
|
|
before, had := wants[node]
|
|
var next delivery.Want
|
|
switch ok, rule, words := Holds(mm, node, n); {
|
|
case ok:
|
|
next = delivery.Want{FP: m.FP}
|
|
case held && had && before.FP == m.FP:
|
|
out.Rules[node] = Holding{Rule: rule, Words: words}
|
|
last := lastHealthy[node]
|
|
switch {
|
|
case keepsData(m.Manifest):
|
|
next = before
|
|
next.Words = fmt.Sprintf("%s holds data, so it is not set back from %s: the operator is asked from release 6", m.Name, short(m.FP))
|
|
case last == "":
|
|
next = before
|
|
next.Words = fmt.Sprintf("%s is held at %s and %s ran no other fingerprint healthy to set it back to", m.Name, short(m.FP), node)
|
|
default:
|
|
next = delivery.Want{FP: last, Newest: m.FP, Rule: rule, Words: words}
|
|
}
|
|
default:
|
|
out.Rules[node] = Holding{Rule: rule, Words: words}
|
|
if !had || before.Removed {
|
|
continue
|
|
}
|
|
next = delivery.Want{FP: before.FP, Newest: m.FP, Rule: rule, Words: words}
|
|
if before.FP == m.FP {
|
|
next = delivery.Want{FP: m.FP}
|
|
}
|
|
}
|
|
if !had || !delivery.SameWant(normal(before), normal(next)) {
|
|
out.Wants[node] = next
|
|
}
|
|
}
|
|
for node, before := range wants {
|
|
if !slices.Contains(m.Nodes, node) && !before.Removed {
|
|
out.Wants[node] = delivery.Want{Removed: true, Words: fmt.Sprintf("%s is no longer assigned to %s", m.Name, node)}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// normal is a want without its version, as a decoded and a composed one are compared.
|
|
func normal(w delivery.Want) delivery.Want {
|
|
w.V = 0
|
|
return w
|
|
}
|
|
|
|
func short(fp string) string {
|
|
part, err := delivery.FingerprintKeyPart(fp)
|
|
if err != nil || len(part) < 12 {
|
|
return fp
|
|
}
|
|
return part[:12]
|
|
}
|