A key that ages out of its bucket (the build seats' asks pass the stream's 168 h) leaves no marker a watch hears, and one deleted while the watchman was down is not heard either: its stuck key stood unmoved for ever, and 26 stuck-key conditions of asks from 2026-10-21 were open on 10-30. Quick fix: mark stuck keys older than the stream's maximum age moved. Thorough fix, chosen because it covers every bucket the watchman mirrors and any way a key goes: at the start's end, a stuck key not moved whose bucket was read whole at this start (each build seat's steps, the assigner's wants) and whose key was not read standing is written moved, which clears its finding, and the closing is said in the log. A bucket that could not be read closes nothing. A key that ages out while the watchman runs is closed at its next start: a KV age-out emits no event, and no timer is added.
166 lines
5.4 KiB
Go
166 lines
5.4 KiB
Go
package watch
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// A build ask taken by a node is a move (novox/hq ADR 0525 §4, issues 1887 A8b, 1908, 1909): the node writes the ask's step
|
|
// key `<node>.<ask id>` running in the build module's steps (delivery.StepsBucket) when it takes the ask, and writes it passed,
|
|
// failed or refused when the ask ends. The pulse of the move is the pulse of the bucket named as the seat's stream
|
|
// (delivery.AskStream), under the ask's id, and it begins when the node started the step.
|
|
//
|
|
// **An ask still waiting in the stream is a backlog, not a stall** (issue 1908): the rebuild after the switch puts about 170
|
|
// asks on a few nodes' slots, and most wait for hours with every worker healthy. No pulse is made for a waiting ask, so none
|
|
// can expire into a false stuck row; the view shows the backlog and the oldest waiting ask's age (delivery view, A8a). What
|
|
// is read of the steps at the watchman's start is mirrored the same way, so an ask running when the watchman was down is
|
|
// pulsed again (issue 1909).
|
|
|
|
// Step is a change of a key of the steps bucket of language's build module, read from the watch (at the start the keys that
|
|
// stand, then each change). A running step makes the pulse of its ask, from the step's own start; a step that ended, or a
|
|
// key deleted, ends it. The node's `paused.<node>` key, a key that is no step key and a value that cannot be read are left.
|
|
func (w *Watchman) Step(language, key string, value []byte, op Op) {
|
|
bucket, err := delivery.AskStream(language)
|
|
if err != nil {
|
|
w.say("no pulse for the step %s: %v", key, err)
|
|
return
|
|
}
|
|
node, id, err := delivery.ParseStepKey(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
switch op {
|
|
case Expired:
|
|
return
|
|
case Delete:
|
|
w.moved(bucket, id)
|
|
return
|
|
}
|
|
w.stands(bucket, id)
|
|
step, err := delivery.DecodeStep(value)
|
|
if err != nil {
|
|
w.say("the step %s cannot be read, and is left: %v", key, err)
|
|
return
|
|
}
|
|
if step.State != delivery.StepRunning {
|
|
w.moved(bucket, id)
|
|
return
|
|
}
|
|
since := step.Started
|
|
if since.IsZero() {
|
|
since = w.now()
|
|
}
|
|
w.mirrorWithin(w.stepBound, bucket, id, describe("the ask %s to end on %s", id, node), node, since, 0)
|
|
}
|
|
|
|
// A want is a move too (novox/hq ADR 0539 §19, issue 1906): the assigner writes `want.<node>.<module>` in its wants bucket,
|
|
// and the node's agent owes the node-engine's have naming the want's fingerprint. The pulse is the pulse of that bucket under
|
|
// the want's key, owed by the node's agent, and it begins when the assigner wrote the want. The have naming the fingerprint
|
|
// ends it; a want that is removed, or a key deleted, ends it as well. Nothing renews it: the bound is how long a node may
|
|
// take to apply a want, the same as a build step's (StepBound). Keys of that bucket that are not a want of a module (a hold,
|
|
// and the node's own key) are left.
|
|
|
|
// Want is a change of a key of the assigner's wants bucket, read from the watch (at the start the keys that stand, then each
|
|
// change), at the time the entry was written.
|
|
func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) {
|
|
node, module, err := delivery.ParseWantKey(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
switch op {
|
|
case Expired:
|
|
return
|
|
case Delete:
|
|
delete(w.wants, key)
|
|
w.moved(delivery.AssignerWantsBucket, key)
|
|
return
|
|
}
|
|
w.stands(delivery.AssignerWantsBucket, key)
|
|
want, err := delivery.DecodeWant(value)
|
|
if err != nil {
|
|
w.say("the want %s cannot be read, and is left: %v", key, err)
|
|
return
|
|
}
|
|
if want.Removed {
|
|
delete(w.wants, key)
|
|
w.moved(delivery.AssignerWantsBucket, key)
|
|
return
|
|
}
|
|
w.wants[key] = want.FP
|
|
if w.have(node, module) == want.FP {
|
|
w.moved(delivery.AssignerWantsBucket, key)
|
|
return
|
|
}
|
|
if at.IsZero() {
|
|
at = w.now()
|
|
}
|
|
w.mirrorWithin(w.stepBound, delivery.AssignerWantsBucket, key,
|
|
describe("the node agent of %s to run %s at %s", node, module, shortFP(want.FP)), want.FP, at, 0)
|
|
}
|
|
|
|
// Have is a change of a key of the node-engine's have bucket: the fingerprint a node runs of a module. One that names the
|
|
// fingerprint of the node's want for the module ends the want's pulse. A node's own key and a key deleted are left.
|
|
func (w *Watchman) Have(key string, value []byte, op Op) {
|
|
node, module, isNode, err := delivery.ParseHaveKey(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if isNode {
|
|
// The node's own record: its tunnel key, whose peers' sight of it is network-unreachable.
|
|
var record *delivery.NodeRecord
|
|
if op == Put {
|
|
if hn, err := delivery.DecodeHaveNode(value); err == nil {
|
|
record = hn.Record
|
|
}
|
|
}
|
|
w.tunnelKey(node, record)
|
|
return
|
|
}
|
|
switch op {
|
|
case Expired:
|
|
return
|
|
case Delete:
|
|
delete(w.haves, key)
|
|
w.haveFindings(key, node, module, nil)
|
|
return
|
|
}
|
|
have, err := delivery.DecodeHave(value)
|
|
if err != nil {
|
|
w.say("the have %s cannot be read, and is left: %v", key, err)
|
|
return
|
|
}
|
|
w.haves[key] = have.FP
|
|
w.haveFindings(key, node, module, &have)
|
|
wk, err := delivery.WantKey(node, module)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if fp, ok := w.wants[wk]; ok && fp == have.FP {
|
|
w.moved(delivery.AssignerWantsBucket, wk)
|
|
}
|
|
}
|
|
|
|
func (w *Watchman) have(node, module string) string {
|
|
key, err := delivery.HaveKey(node, module)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return w.haves[key]
|
|
}
|
|
|
|
func shortFP(fp string) string {
|
|
hex := strings.TrimPrefix(fp, delivery.SourcePrefix2)
|
|
if len(hex) > 12 {
|
|
hex = hex[:12]
|
|
}
|
|
return hex
|
|
}
|