Files
mesh-sdk/go/delivery/assigner.go
T

409 lines
16 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// The assigner's keys (novox/hq ADR 0528 clauses 1 and 4, ADR 0429 §6, issue 1297). The assigner writes, by the six
// rules of ADR 0429 §6, what each node should run: `want.<node>.<module>` in its bucket wants, and beside it
// `held.<module>.<fingerprint>` for a fingerprint it holds after a failed health check. A module that leaves a node's
// modules gets its want written removed, never deleted (ADR 0408 §4). For each node and module it compares the want with
// what the controller sends (the build the controller's assignment names) and writes every difference under
// `<node>.<module>` in its bucket differences, with the rule that explains it or unexplained. In release 5 wants are
// only compared; the node agent applying them is release 6 (ADR 0429 §7). Only the assigner's credential writes these
// buckets; the watchman, the view and the controller read them.
//
// **The bucket is the first word, the key the rest** (as the builder's `builds.<module>.<fp>` is the key
// `<module>.<fp>` in its builds bucket): the differences bucket's keys are `<node>.<module>`. The wants bucket holds
// two kinds of key, so each keeps its word: `want.<node>.<module>` and `held.<module>.<fingerprint hex>`.
const (
// AssignerModule is the module of the assigner part.
AssignerModule = "mesh-delivery-assigner"
// AssignerWantsBucket is the assigner's bucket of wants and holds, `<owner module>_<name>`.
AssignerWantsBucket = AssignerModule + "_wants"
// AssignerWantsHistory is the revisions the wants bucket keeps per key, so a want's history shows each move and
// the operator's manual write (ADR 0429 §6, the one escape).
AssignerWantsHistory = 20
// AssignerDifferencesBucket is the assigner's bucket of differences between a want and what the controller sends.
AssignerDifferencesBucket = AssignerModule + "_differences"
// AssignerDifferencesHistory is the revisions the differences bucket keeps per key: acceptance test 17 of design 47
// reads a day of them.
AssignerDifferencesHistory = 64
)
// The words that open the wants bucket's two kinds of key.
const (
wantWord = "want"
heldWord = "held"
)
// WantKey is the key, in AssignerWantsBucket, of a node's want for a module: `want.<node>.<module>`. A module named
// `node` is refused, since `want.<node>.node` is the node's own key (ADR 0429 §4), as HaveKey refuses it.
func WantKey(node, module string) (string, error) {
have, err := HaveKey(node, module)
if err != nil {
return "", err
}
return wantWord + "." + have, nil
}
// ParseWantKey is the inverse of WantKey; it refuses a held key, a have key and any key of another bucket, naming it.
func ParseWantKey(key string) (node, module string, err error) {
rest, ok := strings.CutPrefix(key, wantWord+".")
if !ok {
return "", "", fmt.Errorf("delivery: %q is not a want key, want.<node>.<module>", key)
}
node, module, isNode, err := ParseHaveKey(rest)
if err != nil || isNode {
return "", "", fmt.Errorf("delivery: %q is not a want key, want.<node>.<module>", key)
}
return node, module, nil
}
// HeldKey is the key, in AssignerWantsBucket, of a module's fingerprint the assigner holds: `held.<module>.<hex>`,
// the hex being the fingerprint's key part (FingerprintKeyPart).
func HeldKey(module, fingerprint string) (string, error) {
key, err := BuilderBuildKey(module, fingerprint)
if err != nil {
return "", err
}
return heldWord + "." + key, nil
}
// ParseHeldKey is the inverse of HeldKey: the module and the fingerprint (`src2:` and the hex); it refuses a want key
// and any key of another bucket, naming it.
func ParseHeldKey(key string) (module, fingerprint string, err error) {
rest, ok := strings.CutPrefix(key, heldWord+".")
if !ok {
return "", "", fmt.Errorf("delivery: %q is not a held key, held.<module>.<fingerprint hex>", key)
}
if module, fingerprint, err = ParseBuilderBuildKey(rest); err != nil {
return "", "", fmt.Errorf("delivery: %q is not a held key: %w", key, err)
}
return module, fingerprint, nil
}
// DifferenceKey is the key, in AssignerDifferencesBucket, of the difference for a module on a node: `<node>.<module>`.
func DifferenceKey(node, module string) (string, error) {
return HaveKey(node, module)
}
// ParseDifferenceKey is the inverse of DifferenceKey; it refuses a want or held key and a node's own key, naming it.
func ParseDifferenceKey(key string) (node, module string, err error) {
node, module, isNode, err := ParseHaveKey(key)
if err != nil || isNode {
return "", "", fmt.Errorf("delivery: %q is not a difference key, <node>.<module>", key)
}
return node, module, nil
}
// The six rules of ADR 0429 §6, by number, as a want or a difference names the one that holds a node back.
const (
RuleBuildDone = 1 // the build of F is done
RuleNotHeld = 2 // F is not held
RuleCanary = 3 // the node is the module's canary, or the canary runs F healthy
RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3)
RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary
RuleGrant = 6 // the module's credential for F exists (grants.<module>.<fp>)
)
func checkRule(rule int, of string) error {
if rule < RuleBuildDone || rule > RuleGrant {
return fmt.Errorf("delivery: %s names the rule %d, which is not one of the six (1 to 6)", of, rule)
}
return nil
}
func checkFingerprint(fp, of string) error {
if _, err := FingerprintKeyPart(fp); err != nil {
return fmt.Errorf("delivery: %s: %w", of, err)
}
return nil
}
// Want is a node's want for a module: the fingerprint FP the node should run, or Removed for a module that left the
// node's modules (then FP is empty). When the module's newest fingerprint is not yet allowed on this node, Newest names
// it and Rule the rule that holds the node back; both are empty when FP is the newest. Words say why in a sentence.
// The entry's own time says since when.
type Want struct {
V int `json:"v"`
FP string `json:"fp,omitempty"`
Removed bool `json:"removed,omitempty"`
Newest string `json:"newest,omitempty"`
Rule int `json:"rule,omitempty"`
Words string `json:"words,omitempty"`
}
func (w Want) check() error {
if w.Removed {
if w.FP != "" || w.Newest != "" || w.Rule != 0 {
return fmt.Errorf("delivery: a removed want names no fingerprint and no rule")
}
return nil
}
if err := checkFingerprint(w.FP, "a want's fingerprint"); err != nil {
return err
}
if w.Newest == "" && w.Rule == 0 {
return nil
}
if err := checkFingerprint(w.Newest, "a want's newest fingerprint"); err != nil {
return err
}
if w.Newest == w.FP {
return fmt.Errorf("delivery: a want at its newest fingerprint names no rule holding it back")
}
return checkRule(w.Rule, "a want held back")
}
// Held is a fingerprint the assigner holds: the node whose health check failed on it, and the check's words.
type Held struct {
V int `json:"v"`
Node string `json:"node"`
Words string `json:"words,omitempty"`
}
func (h Held) check() error {
return checkName("node", h.Node)
}
// Difference is where a want and what the controller sends differ for a module on a node (ADR 0528 clause 4): the
// want's fingerprint (Want, empty when the node has no want for the module, WantRemoved when the want is removed), the
// fingerprint of the build the controller's assignment names (Sent, empty when it sends none or the build names no
// fingerprint; SentBuild names that build), and the rule that explains the difference, or Unexplained.
type Difference struct {
V int `json:"v"`
Want string `json:"want,omitempty"`
WantRemoved bool `json:"want-removed,omitempty"`
Sent string `json:"sent,omitempty"`
SentBuild string `json:"sent-build,omitempty"`
Rule int `json:"rule,omitempty"`
Unexplained bool `json:"unexplained,omitempty"`
Words string `json:"words,omitempty"`
}
func (d Difference) check() error {
if d.WantRemoved && d.Want != "" {
return fmt.Errorf("delivery: a difference with a removed want names no wanted fingerprint")
}
if d.Want != "" {
if err := checkFingerprint(d.Want, "a difference's want"); err != nil {
return err
}
}
if d.Sent != "" {
if err := checkFingerprint(d.Sent, "a difference's sent fingerprint"); err != nil {
return err
}
}
if !d.WantRemoved && d.Want == d.Sent && (d.Want != "" || d.SentBuild == "") {
return fmt.Errorf("delivery: a difference whose want and send are both %q is no difference", d.Want)
}
if d.Unexplained {
if d.Rule != 0 {
return fmt.Errorf("delivery: an unexplained difference names no rule")
}
return nil
}
return checkRule(d.Rule, "a difference")
}
// EncodeWant is w as JSON at Version; it refuses a fingerprint not of the new form, a removed want naming one, and a
// want held back without its rule.
func EncodeWant(w Want) ([]byte, error) {
if err := w.check(); err != nil {
return nil, err
}
w.V = Version
return json.Marshal(w)
}
// DecodeWant reads a want; it refuses an unknown version (ErrUnknownVersion) and what EncodeWant refuses.
func DecodeWant(data []byte) (Want, error) {
var w Want
if err := decode(data, &w); err != nil {
return Want{}, err
}
if err := w.check(); err != nil {
return Want{}, err
}
return w, nil
}
// SameWant says whether two wants are the same value, so the assigner writes only when they differ.
func SameWant(a, b Want) bool { return a == b }
// EncodeHeld is h as JSON at Version; it refuses a node name a key refuses.
func EncodeHeld(h Held) ([]byte, error) {
if err := h.check(); err != nil {
return nil, err
}
h.V = Version
return json.Marshal(h)
}
// DecodeHeld reads a hold; it refuses an unknown version (ErrUnknownVersion) and what EncodeHeld refuses.
func DecodeHeld(data []byte) (Held, error) {
var h Held
if err := decode(data, &h); err != nil {
return Held{}, err
}
if err := h.check(); err != nil {
return Held{}, err
}
return h, nil
}
// SameHeld says whether two holds are the same value.
func SameHeld(a, b Held) bool { return a == b }
// EncodeDifference is d as JSON at Version; it refuses a difference with neither a rule nor unexplained, one with both,
// a rule outside the six, and a want equal to the send.
func EncodeDifference(d Difference) ([]byte, error) {
if err := d.check(); err != nil {
return nil, err
}
d.V = Version
return json.Marshal(d)
}
// DecodeDifference reads a difference; it refuses an unknown version (ErrUnknownVersion) and what EncodeDifference
// refuses.
func DecodeDifference(data []byte) (Difference, error) {
var d Difference
if err := decode(data, &d); err != nil {
return Difference{}, err
}
if err := d.check(); err != nil {
return Difference{}, err
}
return d, nil
}
// SameDifference says whether two differences are the same value.
func SameDifference(a, b Difference) bool { return reflect.DeepEqual(a, b) }
// The assigner's composition check (novox/hq ADR 0446 clauses 1 to 5, ADR 0528 clause 5, ADR 0482 clauses 3 and 9): for
// each group the checker judges, the assigner composes the machines the change touches and writes its verdict under
// the checker's key of that check, `<escaped group>.<heads hash>` (CheckerVerdictKey's form), in its bucket
// compositions. The checker watches that key and reads it into its verdict's composition (Composition); a key not yet
// written is a composition still running.
const (
// AssignerCompositionsBucket is the assigner's bucket of composition verdicts.
AssignerCompositionsBucket = AssignerModule + "_compositions"
// AssignerCompositionsHistory is the revisions the bucket keeps per key.
AssignerCompositionsHistory = 5
// CompositionWaits is the state of a composition that waits for what it composes from (ADR 0446 clause 3: the
// indexer holds no candidates for the group yet), its Words saying what; it is no verdict.
CompositionWaits = "waits"
// CompositionRebuildWarning is the most modules a merge may rebuild before the composition warns (ADR 0446 clause 1).
CompositionRebuildWarning = 12
)
// CompositionKey is the key, in AssignerCompositionsBucket, of the composition of a group's check at its heads: the
// checker's verdict key of the same check (CheckerVerdictKey).
func CompositionKey(group, headsHash string) (string, error) {
return CheckerVerdictKey(group, headsHash)
}
// ParseCompositionKey is the inverse of CompositionKey; it refuses a want, held or difference key, naming it.
func ParseCompositionKey(key string) (group, headsHash string, err error) {
if group, headsHash, err = ParseCheckerVerdictKey(key); err != nil {
return "", "", fmt.Errorf("delivery: %q is not a composition key, <group>.<heads hash>: %w", key, err)
}
return group, headsHash, nil
}
// CompositionVerdict is the value of a composition key: the group and its heads hash, the State (pass, fail or waits),
// the Words of a refusal or of what it waits for, the mesh-sdk commit it composed with (the assigner's own, or the
// group's mesh-sdk member's head, ADR 0482 clause 3), the machines composed, the first machine that failed, the
// warnings that fail nothing (more than twelve modules rebuilt; what was already broken before the change, ADR 0446
// clause 1), and the time it took in integer milliseconds.
type CompositionVerdict struct {
V int `json:"v"`
Group string `json:"group"`
Heads string `json:"heads"`
State string `json:"state"`
Words string `json:"words,omitempty"`
SDK string `json:"sdk,omitempty"`
Machines []string `json:"machines,omitempty"`
Machine string `json:"machine,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Took int64 `json:"took,omitempty"`
}
func (c CompositionVerdict) check() error {
if c.Group == "" {
return fmt.Errorf("delivery: a composition names no group")
}
if !isHex12(c.Heads) {
return fmt.Errorf("delivery: the composition's heads hash %q is not 12 lowercase hex digits", c.Heads)
}
if c.Took < 0 {
return fmt.Errorf("delivery: the composition's time is negative")
}
switch c.State {
case CompositionPass:
if c.SDK == "" {
return fmt.Errorf("delivery: a composition that passed names no mesh-sdk commit")
}
if c.Machine != "" || c.Words != "" {
return fmt.Errorf("delivery: a composition that passed names no failure")
}
case CompositionFail:
if c.Words == "" {
return fmt.Errorf("delivery: a composition that failed gives no words")
}
if c.SDK == "" {
return fmt.Errorf("delivery: a composition that failed names no mesh-sdk commit")
}
case CompositionWaits:
if c.Words == "" {
return fmt.Errorf("delivery: a composition that waits does not say for what")
}
if c.Machine != "" || len(c.Machines) > 0 {
return fmt.Errorf("delivery: a composition that waits composed no machine")
}
default:
return fmt.Errorf("delivery: the composition state %q is not %s, %s or %s", c.State, CompositionPass, CompositionFail, CompositionWaits)
}
return nil
}
// Composition is what the checker writes into its verdict from c: nil while c waits (the check is still running).
func (c CompositionVerdict) Composition() *Composition {
if c.State == CompositionWaits {
return nil
}
return &Composition{State: c.State, Words: c.Words, SDK: c.SDK, Machines: c.Machines}
}
// EncodeCompositionVerdict is c as JSON at Version; it refuses an unknown state, a pass or fail naming no mesh-sdk
// commit, a fail without words, a pass naming a failure, and a wait that composed a machine.
func EncodeCompositionVerdict(c CompositionVerdict) ([]byte, error) {
if err := c.check(); err != nil {
return nil, err
}
c.V = Version
return json.Marshal(c)
}
// DecodeCompositionVerdict reads a composition; it refuses an unknown version (ErrUnknownVersion) and what
// EncodeCompositionVerdict refuses.
func DecodeCompositionVerdict(data []byte) (CompositionVerdict, error) {
var c CompositionVerdict
if err := decode(data, &c); err != nil {
return CompositionVerdict{}, err
}
if err := c.check(); err != nil {
return CompositionVerdict{}, err
}
return c, nil
}
// SameCompositionVerdict says whether two compositions are the same value.
func SameCompositionVerdict(a, b CompositionVerdict) bool { return reflect.DeepEqual(a, b) }