321 lines
9.9 KiB
Go
321 lines
9.9 KiB
Go
package main
|
|
|
|
// **A module being delivered raises no condition until its delivery ends** (novox/hq ADR 0564 clause 13 and Limits 12;
|
|
// issue 1952 C7, C8). The keeper reads the assigner's wants and the nodes' haves, and judges a module's delivery on a
|
|
// node with mesh-sdk's delivery.MotionOf, as the view does. While it is delivered there, a finding about it there, or
|
|
// about what it serves, is kept in the state
|
|
// `held` and raises nothing. When the delivery ends, healthy at the want or failed, what still stands is raised once,
|
|
// saying it happened during the delivery; what went is dropped with a log line. The hold lasts at most the delivery's
|
|
// bound, a key of the state `bounds` whose time to live is the bound: its expiry raises what is held, saying the delivery
|
|
// did not end. A finding about another module or about the machine itself is never held. A condition already open is
|
|
// not held: it is no news.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// The module whose delivery holds what it serves beyond their own findings (Limits 12).
|
|
const runtimeModule = "node-tools"
|
|
|
|
// about is a node and a module a finding or a condition is about, for the hold.
|
|
type about struct{ Node, Module string }
|
|
|
|
// aboutOf are the modules on nodes a condition's subject is about (Limits 12): a module on a machine, a provider on its
|
|
// node, a seat of the same name as its module on a machine, and every module's tools on a machine whose tool
|
|
// runner is delivered. Never the machine itself.
|
|
func (k *Keeper) aboutOf(kind, source string, s health.Subject) []about {
|
|
var out []about
|
|
parts := strings.Split(s.ID, ".")
|
|
switch s.Scope {
|
|
case health.ScopeModule:
|
|
if len(parts) >= 2 {
|
|
out = append(out, about{Node: parts[len(parts)-1], Module: strings.Join(parts[:len(parts)-1], ".")})
|
|
}
|
|
case health.ScopeProvider:
|
|
if len(parts) >= 2 {
|
|
out = append(out, about{Node: parts[1], Module: parts[0]})
|
|
}
|
|
case health.ScopeSeat:
|
|
// The module the placement says holds the seat on that node, or the seat's own name where none is read.
|
|
if len(parts) >= 2 {
|
|
node, seat := parts[len(parts)-1], strings.Join(parts[:len(parts)-1], ".")
|
|
module := k.holders[node+"."+seat]
|
|
if module == "" {
|
|
module = seat
|
|
}
|
|
out = append(out, about{Node: node, Module: module})
|
|
}
|
|
case health.ScopeMachine:
|
|
// The machine itself (presence, network, resolver, disk) is never held; its tools are, while its tool runner is
|
|
// delivered.
|
|
if source == "S11" || kind == "tools-silent" {
|
|
out = append(out, about{Node: s.Machine, Module: runtimeModule})
|
|
if s.Machine == "" && len(parts) > 0 {
|
|
out[len(out)-1].Node = parts[0]
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
if s.Machine != "" && s.Scope != health.ScopeMachine {
|
|
out = append(out, about{Node: s.Machine, Module: runtimeModule})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func splitNodeModule(hk string) (string, string) {
|
|
node, module, _ := strings.Cut(hk, ".")
|
|
return node, module
|
|
}
|
|
|
|
// motion is the delivery of module on node, from its want and have.
|
|
func (k *Keeper) motion(node, module string) (delivery.Motion, delivery.Want) {
|
|
w, wanted := k.wants[node+"."+module]
|
|
h, has := k.haves[node+"."+module]
|
|
return delivery.MotionOf(w, wanted, h, has, "", false), w
|
|
}
|
|
|
|
// holdFor says whether key is held now, and as what. Called with mu held.
|
|
func (k *Keeper) holdFor(key string, f *health.Finding, open bool) (held, bool) {
|
|
if open || f == nil {
|
|
return held{}, false
|
|
}
|
|
kind, source, subject := f.Kind, f.Source, f.Subject
|
|
if _, raisedOut := k.outOfHold[key]; raisedOut {
|
|
return held{}, false
|
|
}
|
|
for _, a := range k.aboutOf(kind, source, subject) {
|
|
m, w := k.motion(a.Node, a.Module)
|
|
if !m.Delivering || m.Ended() || k.passed[a.Node+"."+a.Module] == w.FP {
|
|
continue
|
|
}
|
|
since := k.Now().UTC()
|
|
if was, ok := k.held[key]; ok {
|
|
since = was.Since
|
|
}
|
|
return held{Key: key, Node: a.Node, Module: a.Module, Since: since, Version: w.FP}, true
|
|
}
|
|
return held{}, false
|
|
}
|
|
|
|
// keepHeld keeps a held key in the state `held`, and starts its delivery's bound once.
|
|
func (k *Keeper) keepHeld(h held) error {
|
|
k.mu.Lock()
|
|
_, was := k.held[h.Key]
|
|
k.held[h.Key] = h
|
|
bound := k.bound(h.Module)
|
|
k.mu.Unlock()
|
|
if was {
|
|
return nil
|
|
}
|
|
if _, err := k.Store.Create(HeldState, h.Key, h); err != nil && !errors.Is(err, stdio.ErrChanged) {
|
|
return fmt.Errorf("keeping %s held: %w", h.Key, err)
|
|
}
|
|
b := boundKey(h.Node, h.Module)
|
|
err := k.Store.CreateTTL(BoundsState, b, map[string]any{"node": h.Node, "module": h.Module, "version": h.Version,
|
|
"bound-seconds": int(bound / time.Second)}, bound)
|
|
if err != nil && !errors.Is(err, stdio.ErrChanged) {
|
|
return fmt.Errorf("starting the bound of %s's delivery on %s: %w", h.Module, h.Node, err)
|
|
}
|
|
k.Logf("held %s while %s is delivered on %s (version %s), at most %s", h.Key, h.Module, h.Node, h.Version, bound)
|
|
return nil
|
|
}
|
|
|
|
// bound is a module's delivery bound: its manifest names none yet, so the default (clause 13 (d)). Each key of the state
|
|
// `bounds` is written with it as its own time to live, and its expiry is the bound passing.
|
|
func (k *Keeper) bound(string) time.Duration {
|
|
if k.Bound > 0 {
|
|
return k.Bound
|
|
}
|
|
return delivery.DefaultDeliveryBound
|
|
}
|
|
|
|
func boundKey(node, module string) string { return node + "." + module }
|
|
|
|
// dropHeld lets a held key go: raised, or gone with why said.
|
|
func (k *Keeper) dropHeld(key, why string) {
|
|
k.mu.Lock()
|
|
h, ok := k.held[key]
|
|
delete(k.held, key)
|
|
k.mu.Unlock()
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := k.Store.Delete(HeldState, key); err != nil {
|
|
k.Logf("the held %s could not be removed from held: %v", key, err)
|
|
}
|
|
if why != "" {
|
|
k.Logf("dropped %s, held while %s was delivered on %s: %s", key, h.Module, h.Node, why)
|
|
}
|
|
}
|
|
|
|
// Want takes one change of the assigner's wants; Have one of the nodes' haves. Each may end a delivery, which raises or
|
|
// drops what it held.
|
|
func (k *Keeper) Want(key string, value []byte, deleted bool) error {
|
|
node, module, err := delivery.ParseWantKey(key)
|
|
if err != nil {
|
|
return nil // not a want of a module on a node (a held fingerprint's key)
|
|
}
|
|
k.mu.Lock()
|
|
k.init()
|
|
if deleted {
|
|
delete(k.wants, node+"."+module)
|
|
} else if w, err := delivery.DecodeWant(value); err == nil {
|
|
k.wants[node+"."+module] = w
|
|
} else {
|
|
k.Logf("the want %s is not readable: %v", key, err)
|
|
}
|
|
k.mu.Unlock()
|
|
return k.judge(node, module, "")
|
|
}
|
|
|
|
// Have takes one change of a node's have.
|
|
func (k *Keeper) Have(key string, value []byte, deleted bool) error {
|
|
node, module, isNode, err := delivery.ParseHaveKey(key)
|
|
if err != nil || isNode {
|
|
return nil
|
|
}
|
|
k.mu.Lock()
|
|
k.init()
|
|
if deleted {
|
|
delete(k.haves, node+"."+module)
|
|
} else if h, err := delivery.DecodeHave(value); err == nil {
|
|
k.haves[node+"."+module] = h
|
|
} else {
|
|
k.Logf("the have %s is not readable: %v", key, err)
|
|
}
|
|
k.mu.Unlock()
|
|
return k.judge(node, module, "")
|
|
}
|
|
|
|
// Placement takes one change of the placement: which module holds which seat on a node. The keys are read in either
|
|
// form, a node key and its module keys or the form before (delivery.PlacementKeys; research 072 row 37).
|
|
func (k *Keeper) Placement(key string, value []byte, deleted bool) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
k.init()
|
|
node, err := k.placement.Put(key, value, 0, deleted)
|
|
if node == "" {
|
|
return
|
|
}
|
|
if err != nil {
|
|
k.Logf("the placement of %s is not readable as it stands, so its seats' holders are kept as last read: %v", node, err)
|
|
}
|
|
for hk := range k.holders {
|
|
if strings.HasPrefix(hk, node+".") {
|
|
delete(k.holders, hk)
|
|
}
|
|
}
|
|
p, _, ok := k.placement.Of(node)
|
|
if !ok {
|
|
return
|
|
}
|
|
for _, h := range p.Holds {
|
|
k.holders[node+"."+h.Claim] = h.Module
|
|
}
|
|
}
|
|
|
|
// BoundPassed is a delivery's bound key expiring: what it holds is raised, saying the delivery did not end.
|
|
func (k *Keeper) BoundPassed(key string) error {
|
|
node, module := splitNodeModule(key)
|
|
k.mu.Lock()
|
|
k.init()
|
|
_, w := k.motion(node, module)
|
|
holding := false
|
|
for _, h := range k.held {
|
|
holding = holding || (h.Node == node && h.Module == module)
|
|
}
|
|
if !holding {
|
|
k.mu.Unlock()
|
|
return nil
|
|
}
|
|
k.passed[key] = w.FP
|
|
k.mu.Unlock()
|
|
return k.judge(node, module, fmt.Sprintf("Its delivery of version %s on %s did not end within %s.", short(w.FP), node,
|
|
k.bound(module)))
|
|
}
|
|
|
|
// judge raises or drops what node and module hold when their delivery ended, or with line past its bound.
|
|
func (k *Keeper) judge(node, module, line string) error {
|
|
k.mu.Lock()
|
|
m, w := k.motion(node, module)
|
|
var keys []string
|
|
for key, h := range k.held {
|
|
if h.Node == node && h.Module == module {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
if line == "" && !m.Ended() && m.Delivering {
|
|
k.mu.Unlock()
|
|
return nil
|
|
}
|
|
if line == "" {
|
|
line = fmt.Sprintf("It happened during the delivery of version %s of %s on %s.", short(w.FP), module, node)
|
|
if m.Failed {
|
|
line = fmt.Sprintf("It happened as the delivery of version %s of %s on %s failed.", short(w.FP), module, node)
|
|
}
|
|
delete(k.passed, node+"."+module)
|
|
}
|
|
for _, key := range keys {
|
|
k.outOfHold[key] = line
|
|
}
|
|
k.mu.Unlock()
|
|
sort.Strings(keys)
|
|
var problems []string
|
|
for _, key := range keys {
|
|
k.dropHeld(key, "")
|
|
if err := k.Settle(key); err != nil {
|
|
problems = append(problems, err.Error())
|
|
}
|
|
k.mu.Lock()
|
|
delete(k.outOfHold, key)
|
|
k.mu.Unlock()
|
|
}
|
|
if len(problems) > 0 {
|
|
return errors.New(strings.Join(problems, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func short(fp string) string {
|
|
if _, rest, ok := strings.Cut(fp, ":"); ok {
|
|
fp = rest
|
|
}
|
|
if len(fp) > 12 {
|
|
return fp[:12]
|
|
}
|
|
return fp
|
|
}
|
|
|
|
// Held is every key held now, for the read tool.
|
|
func (k *Keeper) Held() []held {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
out := make([]held, 0, len(k.held))
|
|
for _, h := range k.held {
|
|
out = append(out, h)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
|
|
return out
|
|
}
|
|
|
|
// restoreHeld takes the state `held` as read at the start.
|
|
func (k *Keeper) restoreHeld(key string, value []byte) {
|
|
var h held
|
|
if json.Unmarshal(value, &h) != nil {
|
|
return
|
|
}
|
|
k.mu.Lock()
|
|
k.init()
|
|
k.held[key] = h
|
|
k.mu.Unlock()
|
|
}
|