COND8 (haves and attempts as events): the keeper hears what a node runs and the healers' attempts, and reads neither state

The node-engine's composed, running, wait-held and wait-ended make the have the hold compares with the wants; the healers'
attempt-changed makes an attempt, dropped at its until on the keeper's own clock (a bound with its own time to live) or when
removed. The manifest's reads are gone and its consumes gain the five events.

Builds: design 47 G §541, §665, §556, §499, §434

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:53:09 +01:00
parent 72ccb75c1b
commit 265d3b83b2
10 changed files with 335 additions and 83 deletions
@@ -0,0 +1,201 @@
package main
// What a node runs and what the healers attempt, heard as events (design 47 G §541): the keeper's `reads` of
// `node-engine.have` and `mesh-healers.attempts` are gone. The node-engine seat's `composed`, `running` (with `healthy`
// and `stopped`), `wait-held` and `wait-ended` make the have the hold compares with the wants (ADR 0564 §13); the seat
// `mesh-healers`' `attempt-changed` makes an attempt of a condition, dropped at its `until` on the keeper's own clock or
// when the healer says `removed`. Each is read as §556 has it: a message older by `at` than what the keeper holds changes nothing.
import (
"encoding/json"
"errors"
"strings"
"time"
engine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/motion"
)
// haveParts are the newest of each event a node-engine said of one module on one node.
type haveParts struct {
composed *engine.Composed
running *engine.Running
wait *engine.Wait
waitAt time.Time
}
// The prefix of the keys of `bounds` that are an attempt's end.
const attemptPrefix = "attempt="
// engineKey reads the event and the node and module a key names: `node-engine.<event>[.<node>[.<module>]]`.
func engineKey(key string) (event, node, module string, ok bool) {
rest, found := strings.CutPrefix(key, engine.Seat+".")
if !found {
return "", "", "", false
}
parts := strings.SplitN(rest, ".", 3)
event = parts[0]
if len(parts) > 1 {
node = parts[1]
}
if len(parts) > 2 {
module = parts[2]
}
return event, node, module, true
}
// EngineEvent takes one `composed`, `running`, `wait-held` or `wait-ended` of a node-engine.
func (k *Keeper) EngineEvent(e stdio.Envelope) error {
event, node, module, ok := engineKey(e.Key)
if !ok {
return nil
}
switch event {
case engine.EventRunning:
var r engine.Running
if err := json.Unmarshal(e.Body, &r); err != nil || r.Module == "" || r.Node == "" {
k.Logf("the node-engine's running on %s is not readable: %v", node, err)
return nil
}
err := k.haveChanged(r.Node, r.Module, func(p *haveParts) {
if p.running == nil || !r.At.Before(p.running.At) {
p.running = &r
}
})
if err != nil {
return err
}
return k.EngineRunning(node, e.Body)
case engine.EventComposed:
var c engine.Composed
if err := json.Unmarshal(e.Body, &c); err != nil || c.Module == "" || c.Node == "" {
k.Logf("the node-engine's composed on %s is not readable: %v", node, err)
return nil
}
return k.haveChanged(c.Node, c.Module, func(p *haveParts) {
if p.composed == nil || !c.At.Before(p.composed.At) {
p.composed = &c
}
})
case engine.EventWaitHeld, engine.EventWaitEnded:
var w engine.Wait
if err := json.Unmarshal(e.Body, &w); err != nil || node == "" || module == "" {
k.Logf("the node-engine's %s on %s is not readable, or names no node and module: %v", event, node, err)
return nil
}
at := w.Since
return k.haveChanged(node, module, func(p *haveParts) {
if !p.waitAt.IsZero() && at.Before(p.waitAt) {
return
}
p.waitAt = at
if p.wait = &w; event == engine.EventWaitEnded {
p.wait = nil
}
})
}
return nil
}
// haveChanged applies one change to what a node-engine said of a module on a node, makes the have the hold reads, and
// judges the delivery.
func (k *Keeper) haveChanged(node, module string, apply func(*haveParts)) error {
k.mu.Lock()
k.init()
if k.parts == nil {
k.parts = map[string]*haveParts{}
}
p := k.parts[node+"."+module]
if p == nil {
p = &haveParts{}
k.parts[node+"."+module] = p
}
apply(p)
h, has := p.have()
if has {
k.haves[node+"."+module] = h
} else {
delete(k.haves, node+"."+module)
}
k.mu.Unlock()
return k.judge(node, module, "")
}
// have is the have those events make: a hold is the phase `held` with its reason; a running build with its health, on the
// composition of the composed with its fingerprint; a composed alone is applying; a stopped module has none.
func (p *haveParts) have() (motion.Have, bool) {
switch {
case p.wait != nil && p.wait.Step == engine.WaitStepCompose:
h := motion.Have{Phase: motion.PhaseHeld, Detail: p.wait.Reason}
if p.composed != nil {
h.FP, h.Composed = p.composed.Fingerprint, p.composed.Digest
}
return h, true
case p.running != nil && !p.running.Stopped && (p.composed == nil || p.composed.At.Before(p.running.At) || p.composed.Fingerprint == p.running.Fingerprint):
h := motion.Have{FP: p.running.Fingerprint, Phase: motion.PhaseRunning, Health: motion.HealthOK}
if p.running.Healthy != nil && !*p.running.Healthy {
h.Health = motion.HealthFailed
}
if p.composed != nil && p.composed.Fingerprint == h.FP {
h.Composed = p.composed.Digest
}
return h, true
case p.composed != nil && (p.running == nil || p.composed.At.After(p.running.At)):
return motion.Have{FP: p.composed.Fingerprint, Composed: p.composed.Digest, Phase: motion.PhaseApplying}, true
}
return motion.Have{}, false
}
// AttemptChanged takes one `attempt-changed` of the healers, kept by its subject's `<condition>.<attempt>`: the
// attempt reaches its condition's tried, resolver and escalation (ADR 0564 §7); it is dropped when the healer says
// `removed` and at its `until`, on the keeper's own clock.
func (k *Keeper) AttemptChanged(key string, body []byte) error {
rest, ok := strings.CutPrefix(key, meshhealers.Seat+".attempt-changed.")
if !ok || rest == "" {
return nil
}
var e meshhealers.AttemptChangedEvent
if err := json.Unmarshal(body, &e); err != nil || e.Condition == "" {
k.Logf("the healers' attempt-changed %s is not readable, so it is left out: %v", key, err)
return nil
}
now := k.Now().UTC()
k.mu.Lock()
k.init()
if e.Removed || e.Until != nil && !now.Before(*e.Until) {
delete(k.attempts, rest)
k.mu.Unlock()
_ = k.Store.Delete(BoundsState, attemptPrefix+rest)
return k.Settle(e.Condition)
}
a := meshhealers.AttemptsByAttemptValue{Act: e.Act, BudgetKey: e.BudgetKey, Condition: e.Condition, Finished: e.Finished,
Healer: e.Healer, Kind: e.Kind, Outcome: e.Outcome, Said: e.Said}
if e.Begun != nil {
a.Begun = *e.Begun
}
k.attempts[rest] = a
k.mu.Unlock()
if e.Until != nil {
if err := k.Store.CreateTTL(BoundsState, attemptPrefix+rest, map[string]any{"key": rest, "condition": e.Condition, "until": *e.Until},
e.Until.Sub(now)); err != nil && !errors.Is(err, stdio.ErrChanged) {
k.Logf("the end of the attempt %s could not be timed: %v", rest, err)
}
}
return k.Settle(e.Condition)
}
// AttemptPassed is an attempt's `until` passing: it is dropped from its condition.
func (k *Keeper) AttemptPassed(bound string) error {
rest := strings.TrimPrefix(bound, attemptPrefix)
k.mu.Lock()
k.init()
a, ok := k.attempts[rest]
delete(k.attempts, rest)
k.mu.Unlock()
if !ok {
return nil
}
return k.Settle(a.Condition)
}
@@ -18,6 +18,7 @@ import (
engine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
stdio "git.novox.be/novox/mesh-sdk/go"
)
@@ -71,8 +72,11 @@ func (k *Keeper) FindingEvent(e stdio.Envelope) error {
if _, ok := emitterOf(e.Key, watchdogs.EventMachineBack); ok && strings.HasPrefix(e.Key, watchdogs.Seat+".") {
return k.MachinePresence(e.Body, false)
}
if e.Key == engine.Seat+"."+engine.EventRunning {
return k.EngineRunning(e.Node, e.Body)
if _, _, _, ok := engineKey(e.Key); ok {
return k.EngineEvent(e)
}
if strings.HasPrefix(e.Key, meshhealers.Seat+".attempt-changed.") {
return k.AttemptChanged(e.Key, e.Body)
}
if e.Key == broker.Seat+"."+broker.EventCreated {
return k.BrokerCreated(e.Body)
@@ -230,7 +234,9 @@ func (k *Keeper) MachinePresence(body []byte, away bool) error {
func hear(k *Keeper) {
patterns := []string{"*." + EventFindingWritten, "*." + EventFindingCleared, "*." + EventFindingsStated,
watchdogs.Seat + "." + watchdogs.EventMachineAway, watchdogs.Seat + "." + watchdogs.EventMachineBack,
engine.Seat + "." + engine.EventRunning, broker.Seat + "." + broker.EventCreated}
engine.Seat + "." + engine.EventRunning, engine.Seat + "." + engine.EventComposed, engine.Seat + "." + engine.EventWaitHeld,
engine.Seat + "." + engine.EventWaitEnded, meshhealers.Seat + "." + meshhealers.EventAttemptChanged,
broker.Seat + "." + broker.EventCreated}
for _, pattern := range patterns {
go func() {
<-stdio.Served()
@@ -18,7 +18,6 @@ import (
"strings"
"time"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/motion"
@@ -161,8 +160,8 @@ func (k *Keeper) dropHeld(key, why string) {
}
}
// 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.
// Want takes one change of the assigner's wants, which may end a delivery, which raises or drops what it held (the nodes' haves
// are heard as the node-engine's events, engine_events.go).
func (k *Keeper) Want(key string, value []byte, deleted bool) error {
node, module, ok := meshwants.ParseWantsByWantNodeModuleKey(key)
if !ok {
@@ -181,25 +180,6 @@ func (k *Keeper) Want(key string, value []byte, deleted bool) error {
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 := engineclient.ParseHaveKey(key)
if err != nil || isNode {
return nil
}
k.mu.Lock()
k.init()
if deleted {
delete(k.haves, node+"."+module)
} else if h, err := engineclient.DecodeHave(value); err == nil {
k.haves[node+"."+module] = motion.Have{FP: h.Fp, Composed: h.Composed, Phase: h.Phase, Health: h.Health, Detail: h.Detail}
} else {
k.Logf("the have %s is not readable: %v", key, err)
}
k.mu.Unlock()
return k.judge(node, module, "")
}
// Assignments takes one change of the assignments: which module holds which seat on a node. The keys are a node key and
// its module keys (assignerclient.Keys; research 072 row 37).
func (k *Keeper) Assignments(key string, value []byte, deleted bool) {
@@ -238,6 +218,9 @@ func (k *Keeper) BoundPassed(key string) error {
if strings.HasPrefix(key, ownerPrefix) {
return k.OwnerPassed(key)
}
if strings.HasPrefix(key, attemptPrefix) {
return k.AttemptPassed(key)
}
node, module := splitNodeModule(key)
k.mu.Lock()
k.init()
@@ -3,6 +3,9 @@ package main
import (
"encoding/json"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"testing"
assignerclient "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
@@ -35,31 +38,28 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
raw, _ := json.Marshal(meshwants.WantsByWantNodeModuleValue{V: 1, Fp: fp, Composed: composed})
return raw
}
have := func(fp, composed string, phase, h string) []byte {
raw, _ := json.Marshal(nodeengine.HaveByNodeModuleValue{V: 1, Fp: fp, Composed: composed, Phase: phase, Health: h})
return raw
}
have := func(fp, composed string, phase, h string) haveSpec { return haveSpec{fp, composed, phase, h} }
for _, c := range []struct {
name string
// end is the have the delivery ends with, or nil for a delivery past its bound.
end []byte
end *haveSpec
standing bool
said string
line string
}{
{name: "gone by its end", end: have(ctrlNext, ctrlNextComp, motion.PhaseRunning, engineclient.HealthOK)},
{name: "still standing at its end", end: have(ctrlNext, ctrlNextComp, motion.PhaseRunning, engineclient.HealthOK),
{name: "gone by its end", end: ptr(have(ctrlNext, ctrlNextComp, motion.PhaseRunning, engineclient.HealthOK))},
{name: "still standing at its end", end: ptr(have(ctrlNext, ctrlNextComp, motion.PhaseRunning, engineclient.HealthOK)),
standing: true, said: "raised module.mesh-controller.novox.unhealthy", line: "during the delivery of version 0d1e2f3a4b5c"},
{name: "past its bound", standing: true, said: "raised module.mesh-controller.novox.unhealthy", line: "did not end within"},
{name: "failed", end: have(ctrlNext, ctrlNextComp, motion.PhaseFailed, engineclient.HealthFailed), standing: true,
{name: "failed", end: ptr(have(ctrlNext, ctrlNextComp, motion.PhaseFailed, engineclient.HealthFailed)), standing: true,
said: "raised module.mesh-controller.novox.unhealthy", line: "as the delivery of version 0d1e2f3a4b5c"},
} {
r := newRig()
_ = r.k.Want("want.novox.mesh-controller", want(ctrlRunning, ctrlComposed), false)
_ = r.k.Have("novox.mesh-controller", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK), false)
r.engineHave("novox.mesh-controller", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK))
// The delivery starts: the assigner wants the next fingerprint; the node applies it.
_ = r.k.Want("want.novox.mesh-controller", want(ctrlNext, ctrlNextComp), false)
_ = r.k.Have("novox.mesh-controller", have(ctrlNext, ctrlNextComp, motion.PhaseApplying, engineclient.HealthUnknown), false)
r.engineHave("novox.mesh-controller", have(ctrlNext, ctrlNextComp, motion.PhaseApplying, engineclient.HealthUnknown))
own, ownRaw := finding(t, watchman, cc.ScopeModule, "mesh-controller.novox", "unhealthy", "health", cc.Warning, r.now)
other, otherRaw := finding(t, watchman, cc.ScopeModule, "postgres.novox", "unhealthy", "health", cc.Warning, r.now)
machine, machineRaw := finding(t, watchman, cc.ScopeMachine, "novox", "silent", "S1", cc.Urgent, r.now)
@@ -83,7 +83,7 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
_ = r.k.Finding(watchman, own, nil, true)
}
if c.end != nil {
_ = r.k.Have("novox.mesh-controller", c.end, false)
r.engineHave("novox.mesh-controller", *c.end)
} else {
r.now = r.now.Add(motion.DefaultDeliveryBound)
_ = r.k.BoundPassed("novox.mesh-controller")
@@ -110,10 +110,34 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
r.k.Assignments("ace.mesh-delivery-assigner", placed["ace.mesh-delivery-assigner"], false)
r.k.Assignments("ace.node", placed["ace.node"], false)
_ = r.k.Want("want.ace.mesh-delivery-assigner", want(ctrlNext, ctrlNextComp), false)
_ = r.k.Have("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK), false)
r.engineHave("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK))
seatKey, seatRaw := finding(t, selfCheck, cc.ScopeSeat, "mesh-module-assigner.ace", "holder-silent", "D3", cc.Warning, r.now)
_ = r.k.Finding(selfCheck, seatKey, seatRaw, false)
if got := r.changes(); got != "" || len(r.k.Held()) != 1 || r.k.Held()[0].Module != "mesh-delivery-assigner" {
t.Errorf("a seat's finding while its holder is delivered: said %q, held %+v", got, r.k.Held())
}
}
// haveSpec is a have as the old tests spoke it; engineHave says it as the node-engine's events do (design 47 G §541).
type haveSpec struct{ fp, composed, phase, health string }
func ptr[T any](v T) *T { return &v }
func (r *rig) engineHave(nodeModule string, h haveSpec) {
node, module, _ := strings.Cut(nodeModule, ".")
say := func(event string, body any) {
raw, _ := json.Marshal(body)
if err := r.k.EngineEvent(stdio.Envelope{Key: nodeengine.Seat + "." + event + "." + node + "." + module, Node: node, Body: raw}); err != nil {
panic(err)
}
}
r.hevent++
at := r.now.Add(time.Duration(r.hevent) * time.Millisecond)
if h.phase != motion.PhaseFailed {
say(nodeengine.EventComposed, nodeengine.Composed{At: at, Digest: h.composed, Fingerprint: h.fp, Module: module, Node: node, V: 1})
}
if h.phase == motion.PhaseRunning || h.phase == motion.PhaseFailed {
healthy := h.health == engineclient.HealthOK
say(nodeengine.EventRunning, nodeengine.Running{At: at.Add(time.Microsecond), Fingerprint: h.fp, Healthy: &healthy, Module: module, Node: node, V: 1})
}
}
@@ -67,12 +67,10 @@ const HistoryKeptFor = 90 * 24 * time.Hour
// The other states it reads, as `<module>.<name>`.
const (
AttemptsRead = healersModule + "." + meshhealers.StateAttempts
WantsRead = assignerModule + ".wants"
WantsRead = assignerModule + ".wants"
// AssignmentsRead is the mesh-module-assigner seat's assignments, read for which module holds a seat on a node (hold.go; the
// lead, 2026-10-29, for ADR 0564 clause 2's inputs).
AssignmentsRead = assignerModule + "." + mesh.StateAssignments
HavesRead = "node-engine.have"
ConditionWrites = 8
)
@@ -129,6 +127,8 @@ type Keeper struct {
attempts map[string]meshhealers.AttemptsByAttemptValue
wants map[string]motion.Want
haves map[string]motion.Have
// parts are the newest of each event a node-engine said of a module on a node, which make its have (engine_events.go).
parts map[string]*haveParts
// holders are, by `<node>.<seat>`, the module the assignments says holds the seat there.
holders map[string]string
// assignments is the assignments' keys as heard, assembled per node.
@@ -228,33 +228,6 @@ func (k *Keeper) Blind(observer, why string) {
}
}
// Attempt takes one change of the healers' `attempts`: the condition it names gets its tried, resolver and escalation
// (clause 7).
func (k *Keeper) Attempt(key string, value []byte, deleted bool) error {
k.mu.Lock()
k.init()
var condition string
if old, ok := k.attempts[key]; ok {
condition = old.Condition
}
if deleted {
delete(k.attempts, key)
} else {
var a meshhealers.AttemptsByAttemptValue
if err := json.Unmarshal(value, &a); err != nil || a.Condition == "" {
k.mu.Unlock()
k.Logf("the healers' attempt %s is not readable, so it is left out: %v", key, err)
return nil
}
k.attempts[key], condition = a, a.Condition
}
k.mu.Unlock()
if condition == "" {
return nil
}
return k.Settle(condition)
}
// endedWhy is why a condition of a kind that ended with the controller is cleared.
const endedWhy = "its kind ended with the controller, which was unassigned (issue 1126): no finding raises it again"
@@ -111,6 +111,8 @@ type rig struct {
st *fakeStore
now time.Time
said []Event
// hevent orders the node-engine events a test says.
hevent int
}
func newRig() *rig {
@@ -231,8 +233,8 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
return ""
}},
{name: "a healer's attempt reaches tried and the resolver", do: func() error {
a, _ := json.Marshal(meshhealers.AttemptsByAttemptValue{Healer: "H3", Condition: key, Act: "asserted", Outcome: meshhealers.AttemptsByAttemptValueOutcomeActed, Begun: r.now})
return r.k.Attempt("h3-1", a, false)
a, _ := json.Marshal(meshhealers.AttemptChangedEvent{Healer: "H3", Condition: key, Act: "asserted", Outcome: meshhealers.AttemptChangedEventOutcomeActed, Begun: &r.now})
return r.k.AttemptChanged(meshhealers.Seat+".attempt-changed."+key+".h3-1", a)
}, said: "resolver machine.g14.silent", check: func(c Condition, _ bool) string {
if c.Resolver != "healer:H3" || len(c.Tried) != 1 || c.Tried[0].By != "healer H3" {
return fmt.Sprintf("%+v", c)
@@ -240,9 +242,10 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
return ""
}},
{name: "an escalation makes it urgent, the operator's", do: func() error {
a, _ := json.Marshal(meshhealers.AttemptsByAttemptValue{Healer: "H3", Condition: key, Act: "handed over", Outcome: meshhealers.AttemptsByAttemptValueOutcomeEscalated,
Begun: r.now.Add(time.Minute)})
return r.k.Attempt("h3-2", a, false)
begun := r.now.Add(time.Minute)
a, _ := json.Marshal(meshhealers.AttemptChangedEvent{Healer: "H3", Condition: key, Act: "handed over", Outcome: meshhealers.AttemptChangedEventOutcomeEscalated,
Begun: &begun})
return r.k.AttemptChanged(meshhealers.Seat+".attempt-changed."+key+".h3-2", a)
}, said: "severity machine.g14.silent, resolver machine.g14.silent", check: func(c Condition, _ bool) string {
if c.Severity != cc.Urgent || c.Resolver != cc.ResolverOperator || len(c.Tried) != 2 {
return fmt.Sprintf("%+v", c)
@@ -126,10 +126,6 @@ func Keep(k *Keeper, v *views.Reader) {
logf("cleared %d condition(s) of kinds that ended with the controller: %v", n, err)
}
watch(k, v, WantsRead, "", func(c stdio.StateChange) error { return said(k.Want(c.Key, c.Value, c.Op != "put")) })
watch(k, v, HavesRead, "", func(c stdio.StateChange) error { return said(k.Have(c.Key, c.Value, c.Op != "put")) })
watch(k, v, AttemptsRead, healersModule+" attempts", func(c stdio.StateChange) error {
return said(k.Attempt(c.Key, c.Value, c.Op != "put"))
})
hear(k)
go func() { <-stdio.Served(); ListBound(k, stdio.Ask) }()
watch(k, v, BoundsState, "", func(c stdio.StateChange) error {
@@ -35,11 +35,11 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
}
// The assigner's wants and assignments are asked once of their read tools and followed by their events (ADR 0615 §1);
// the observers' findings are heard as events and no `findings` is read (design 47 G §383).
reads := []string{HavesRead, AttemptsRead}
var reads []string
if !slices.Equal(m.Invokes, []string{WantsTool, AssignmentsTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-wants.revision",
"seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-broker.list"}) ||
!slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-assigner.views.assignments",
"*.finding-written", "*.finding-cleared", "*.findings-stated", "mesh-watchdogs.machine-away", "mesh-watchdogs.machine-back", "mesh-wants.assignment-revision", "mesh-catalog.registration-changed", "mesh-catalog.catalog-revision", "mesh-broker.created", "node-engine.running"}) {
"*.finding-written", "*.finding-cleared", "*.findings-stated", "mesh-watchdogs.machine-away", "mesh-watchdogs.machine-back", "mesh-wants.assignment-revision", "mesh-catalog.registration-changed", "mesh-catalog.catalog-revision", "mesh-broker.created", "node-engine.running", "node-engine.composed", "node-engine.wait-held", "node-engine.wait-ended", "mesh-healers.attempt-changed"}) {
t.Errorf("invokes %v, consumes %v", m.Invokes, m.Consumes)
}
for _, r := range m.Reads {
@@ -141,3 +141,69 @@ func TestAStatementCoversOnlyItsKindsAndEventsComeInEitherForm(t *testing.T) {
func stdioEnvelope(key, node string, body []byte) stdio.Envelope {
return stdio.Envelope{Key: key, Node: node, Body: body}
}
// Design 47 G §541: the healers' attempts and a node's holds are heard as events: an attempt shows on its condition and
// is dropped at its `until` or when the healer says it removed; a wait-held makes the module's have held with its
// reason, and a wait-ended ends it; a message older by `at` than the one held changes nothing.
func TestTheKeeperHearsAttemptsAndHoldsAsEvents(t *testing.T) {
r := newRig()
key, raw := finding(t, watchman, cc.ScopeMachine, "g14", "silent", "S1", cc.Warning, r.now)
if err := r.k.Finding(watchman, key, raw, false); err != nil {
t.Fatal(err)
}
subject := meshhealersSeat + ".attempt-changed." + key + ".a1"
until := r.now.Add(time.Hour)
a := mustJSON(t, map[string]any{"healer": "H3", "condition": key, "act": "asserted", "outcome": "acted", "begun": r.now, "until": until})
if err := r.k.AttemptChanged(subject, a); err != nil {
t.Fatal(err)
}
if c, _ := r.open(key); len(c.Tried) != 1 || c.Resolver != "healer:H3" {
t.Fatalf("an attempt said is shown on its condition: %+v", c)
}
if r.st.ttls["bounds/"+attemptPrefix+key+".a1"] != time.Hour {
t.Errorf("its end is a bound of its own: %v", r.st.ttls)
}
r.now = until
if err := r.k.BoundPassed(attemptPrefix + key + ".a1"); err != nil {
t.Fatal(err)
}
r.k.mu.Lock()
left := len(r.k.attempts)
r.k.mu.Unlock()
if left != 0 {
t.Errorf("an attempt past its until is dropped: %d held", left)
}
// The healer ending one says removed.
a = mustJSON(t, map[string]any{"healer": "H3", "condition": key, "act": "asserted", "outcome": "acted", "begun": r.now})
_ = r.k.AttemptChanged(meshhealersSeat+".attempt-changed."+key+".a2", a)
_ = r.k.AttemptChanged(meshhealersSeat+".attempt-changed."+key+".a2", mustJSON(t, map[string]any{"condition": key, "removed": true}))
r.k.mu.Lock()
left = len(r.k.attempts)
r.k.mu.Unlock()
if left != 0 {
t.Errorf("a removed attempt is dropped: %d held", left)
}
// A hold of a node-engine is the module's have held, until its wait-ended; an older message changes nothing.
say := func(event string, w map[string]any) {
if err := r.k.EngineEvent(stdioEnvelope(nodeEngineSeat+"."+event+".ace.mod", "ace", mustJSON(t, w))); err != nil {
t.Fatal(err)
}
}
say("wait-held", map[string]any{"step": "compose", "reason": "restore point missing", "since": r.now})
if h := r.k.haves["ace.mod"]; h.Phase != "held" || h.Detail != "restore point missing" {
t.Fatalf("a hold is heard: %+v", h)
}
say("wait-held", map[string]any{"step": "compose", "reason": "older", "since": r.now.Add(-time.Hour)})
if h := r.k.haves["ace.mod"]; h.Detail != "restore point missing" {
t.Errorf("an older message changes nothing: %+v", h)
}
say("wait-ended", map[string]any{"step": "compose", "since": r.now.Add(time.Minute)})
if h := r.k.haves["ace.mod"]; h.Phase == "held" {
t.Errorf("wait-ended ends the hold: %+v", h)
}
}
const (
meshhealersSeat = "mesh-healers"
nodeEngineSeat = "node-engine"
)
+5 -5
View File
@@ -13,10 +13,6 @@
]
}
],
"reads": [
"mesh-healers.attempts",
"node-engine.have"
],
"invokes": [
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-assigner.assigner_read_assignments",
@@ -38,7 +34,11 @@
"mesh-catalog.registration-changed",
"mesh-catalog.catalog-revision",
"mesh-broker.created",
"node-engine.running"
"node-engine.running",
"node-engine.composed",
"node-engine.wait-held",
"node-engine.wait-ended",
"mesh-healers.attempt-changed"
],
"state": [
"conditions",