mesh-conditions' hold reads wants, haves and placement through the seats' types and clients; the motion is mesh-sdk's go/motion (B5e; ADR 0634 §1, §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:40:54 +01:00
parent 0f1d4c5e0f
commit 42387b660c
7 changed files with 69 additions and 49 deletions
@@ -2,7 +2,7 @@ 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
// node with mesh-sdk's motion.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
@@ -18,8 +18,10 @@ import (
"strings"
"time"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/motion"
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"
@@ -79,10 +81,10 @@ func splitNodeModule(hk string) (string, string) {
}
// motion is the delivery of module on node, from its want and have.
func (k *Keeper) motion(node, module string) (delivery.Motion, delivery.Want) {
func (k *Keeper) motion(node, module string) (motion.Motion, motion.Want) {
w, wanted := k.wants[node+"."+module]
h, has := k.haves[node+"."+module]
return delivery.MotionOf(w, wanted, h, has, "", false), w
return motion.MotionOf(w, wanted, h, has, "", false), w
}
// holdFor says whether key is held now, and as what. Called with mu held.
@@ -137,7 +139,7 @@ func (k *Keeper) bound(string) time.Duration {
if k.Bound > 0 {
return k.Bound
}
return delivery.DefaultDeliveryBound
return motion.DefaultDeliveryBound
}
func boundKey(node, module string) string { return node + "." + module }
@@ -162,15 +164,15 @@ 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.
func (k *Keeper) Want(key string, value []byte, deleted bool) error {
node, module, err := delivery.ParseWantKey(key)
if err != nil {
node, module, ok := meshwants.ParseWantsByWantNodeModuleKey(key)
if !ok {
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 {
} else if w, err := decodeWant(value); err == nil {
k.wants[node+"."+module] = w
} else {
k.Logf("the want %s is not readable: %v", key, err)
@@ -181,7 +183,7 @@ func (k *Keeper) Want(key string, value []byte, deleted bool) error {
// 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)
node, module, isNode, err := engineclient.ParseHaveKey(key)
if err != nil || isNode {
return nil
}
@@ -189,8 +191,8 @@ func (k *Keeper) Have(key string, value []byte, deleted bool) error {
k.init()
if deleted {
delete(k.haves, node+"."+module)
} else if h, err := delivery.DecodeHave(value); err == nil {
k.haves[node+"."+module] = h
} 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)
}
@@ -320,3 +322,12 @@ func (k *Keeper) restoreHeld(key string, value []byte) {
k.held[key] = h
k.mu.Unlock()
}
// decodeWant reads a want of a module on a node as far as a delivery's motion needs it.
func decodeWant(value []byte) (motion.Want, error) {
var w meshwants.WantsByWantNodeModuleValue
if err := json.Unmarshal(value, &w); err != nil {
return motion.Want{}, err
}
return motion.Want{FP: w.Fp, Composed: w.Composed, Removed: w.Removed}, nil
}
@@ -5,8 +5,13 @@ import (
"strings"
"testing"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-sdk/go/delivery"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
meshwants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/motion"
)
// The fingerprints and composition of mesh-controller on novox, copied from its have (mesh-delivery-view.node_haves,
@@ -27,11 +32,11 @@ const (
// held key as held.
func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
want := func(fp, composed string) []byte {
raw, _ := json.Marshal(delivery.Want{V: delivery.Version, FP: fp, Composed: composed})
raw, _ := json.Marshal(meshwants.WantsByWantNodeModuleValue{V: 1, Fp: fp, Composed: composed})
return raw
}
have := func(fp, composed string, phase delivery.Phase, h delivery.Health) []byte {
raw, _ := json.Marshal(delivery.Have{V: delivery.Version, FP: fp, Composed: composed, Phase: phase, Health: h})
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
}
for _, c := range []struct {
@@ -42,19 +47,19 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
said string
line string
}{
{name: "gone by its end", end: have(ctrlNext, ctrlNextComp, delivery.PhaseRunning, delivery.HealthOK)},
{name: "still standing at its end", end: have(ctrlNext, ctrlNextComp, delivery.PhaseRunning, delivery.HealthOK),
{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),
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, delivery.PhaseFailed, delivery.HealthFailed), standing: true,
{name: "failed", end: 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, delivery.PhaseRunning, delivery.HealthOK), false)
_ = r.k.Have("novox.mesh-controller", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK), false)
// 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, delivery.PhaseApplying, delivery.HealthUnknown), false)
_ = r.k.Have("novox.mesh-controller", have(ctrlNext, ctrlNextComp, motion.PhaseApplying, engineclient.HealthUnknown), false)
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)
@@ -67,7 +72,7 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
}
}
// The delivery's bound is its own key's time to live, the module's bound (the default where it names none).
if r.st.ttls["bounds/novox.mesh-controller"] != delivery.DefaultDeliveryBound {
if r.st.ttls["bounds/novox.mesh-controller"] != motion.DefaultDeliveryBound {
t.Errorf("%s: the bound key lives %s", c.name, r.st.ttls["bounds/novox.mesh-controller"])
}
if got := r.changes(); got != "raised "+other+", raised "+machine {
@@ -80,7 +85,7 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
if c.end != nil {
_ = r.k.Have("novox.mesh-controller", c.end, false)
} else {
r.now = r.now.Add(delivery.DefaultDeliveryBound)
r.now = r.now.Add(motion.DefaultDeliveryBound)
_ = r.k.BoundPassed("novox.mesh-controller")
}
if got := r.changes(); got != c.said {
@@ -97,20 +102,20 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
// while the assigner is delivered on ace, though the seat is named mesh-module-assigner.
r := newRig()
// Research 072 row 37: the placement in its keys per module and node; a stale legacy key beside them is not read.
stale, err := delivery.EncodePlacement(delivery.Placement{Node: "ace"})
stale, err := assignerclient.EncodePlacement(mesh.Placement{Node: "ace"})
if err != nil {
t.Fatal(err)
}
r.k.Placement("placement.ace", stale, false)
placed, err := delivery.PlacementValues(delivery.Placement{Node: "ace", Modules: []delivery.PlacedModule{{Module: "mesh-delivery-assigner"}},
Holds: []delivery.Holding{{Claim: "mesh-module-assigner", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
placed, err := assignerclient.PlacementValues(mesh.Placement{Node: "ace", Modules: []mesh.PlacedModule{{Module: "mesh-delivery-assigner"}},
Holds: []mesh.Holding{{Claim: "mesh-module-assigner", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
if err != nil {
t.Fatal(err)
}
r.k.Placement("ace.mesh-delivery-assigner", placed["ace.mesh-delivery-assigner"], false)
r.k.Placement("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, delivery.PhaseRunning, delivery.HealthOK), false)
_ = r.k.Have("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, motion.PhaseRunning, engineclient.HealthOK), false)
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" {
@@ -17,8 +17,10 @@ import (
"sync"
"time"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
mesh "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/motion"
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"
@@ -31,6 +33,10 @@ const (
watchmanModule = "mesh-delivery-watchman"
selfCheckModule = "mesh-self-check"
healersModule = "mesh-healers"
// The holders of the placement and the catalog, whose views and findings the keeper follows (the manifest test holds
// these equal to the manifest's `consumes` and `invokes`).
assignerModule = "mesh-delivery-assigner"
indexerModule = "mesh-delivery-indexer"
)
// The module, its seat, and the states it keeps.
@@ -50,7 +56,7 @@ const HistoryKeptFor = 90 * 24 * time.Hour
// Observers are the modules whose `findings` the keeper reads (clause 2; ADR 0565 §8 for the assigner's; the lead's
// table of 2026-10-29 for the provisioner's and the router's; ADR 0571 clause 2 for the seat artifact-store's holder,
// which observes S25, image-missing).
var Observers = []string{watchmanModule, selfCheckModule, healersModule, delivery.AssignerModule,
var Observers = []string{watchmanModule, selfCheckModule, healersModule, assignerModule,
"mesh-provisioner", "messenger", artifactStore, configModule, vaultModule,
// The readers of the indexer's and the assigner's state say a first view unanswered past its bound in their own findings
// (ADR 0615 §6).
@@ -64,10 +70,10 @@ var Observers = []string{watchmanModule, selfCheckModule, healersModule, deliver
// The other states it reads, as `<module>.<name>`.
const (
AttemptsRead = healersModule + "." + meshhealers.StateAttempts
WantsRead = delivery.AssignerModule + ".wants"
WantsRead = assignerModule + ".wants"
// PlacementRead is the mesh-module-assigner seat's placement, read for which module holds a seat on a node (hold.go; the
// lead, 2026-10-29, for ADR 0564 clause 2's inputs).
PlacementRead = delivery.PlacementRead
PlacementRead = assignerModule + "." + mesh.StatePlacement
HavesRead = "node-engine.have"
ConditionWrites = 8
)
@@ -112,19 +118,19 @@ type Keeper struct {
Logf func(format string, a ...any)
// Homes says, by kind, where each kind the controller raised lives now (homes.go); Homes when nil.
Homes map[string]Home
// Bound is a delivery's hold bound; delivery.DefaultDeliveryBound when zero (hold.go).
// Bound is a delivery's hold bound; motion.DefaultDeliveryBound when zero (hold.go).
Bound time.Duration
mu sync.Mutex
findings map[string]map[string]seat.FindingsByKeyValue
blind map[string]string
attempts map[string]meshhealers.AttemptsByAttemptValue
wants map[string]delivery.Want
haves map[string]delivery.Have
wants map[string]motion.Want
haves map[string]motion.Have
// holders are, by `<node>.<seat>`, the module the placement says holds the seat there.
holders map[string]string
// placement is the placement's keys as heard, assembled per node.
placement *delivery.PlacementKeys
placement *assignerclient.Keys
stuck map[string]string
held map[string]held
// passed are the deliveries whose bound passed, by node and module, at the fingerprint wanted then; outOfHold the
@@ -139,10 +145,10 @@ func (k *Keeper) init() {
if k.findings == nil {
k.findings, k.blind, k.attempts = map[string]map[string]seat.FindingsByKeyValue{}, map[string]string{},
map[string]meshhealers.AttemptsByAttemptValue{}
k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]delivery.Want{}, map[string]delivery.Have{},
k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]motion.Want{}, map[string]motion.Have{},
map[string]string{}, map[string]held{}, map[string]clearedMemo{}
k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{}
k.placement = delivery.NewPlacementKeys()
k.placement = assignerclient.NewKeys()
}
if k.Now == nil {
k.Now = time.Now
@@ -18,7 +18,6 @@ import (
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"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// fakeStore is the bus's states in memory, with revisions, as the runtime answers them.
@@ -336,7 +335,7 @@ func TestEveryControllerKindHasItsHome(t *testing.T) {
// hearing, is read: the delivery parts and the tool runner by name (other repositories), and every catalogue module
// whose code writes a views finding, found in the catalogue's tree, so a new writer cannot go unobserved.
readers := []string{watchman, "mesh-delivery-teller", enrolment, builder, "mesh-delivery-checker", "mesh-delivery-view",
indexer, delivery.AssignerModule, configModule, vaultModule, gitea, "node-tools"}
indexer, assignerModule, configModule, vaultModule, gitea, "node-tools"}
readers = append(readers, viewFindingWriters(t, "../../..")...)
for _, reader := range readers {
if !slices.Contains(Observers, reader) {
@@ -15,7 +15,6 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
@@ -142,21 +141,21 @@ func Keep(k *Keeper, v *views.Reader) {
// The assigner's read tools of the states the keeper follows, which the manifest invokes (ADR 0615).
const (
WantsTool = delivery.AssignerModule + ".assigner_read_wants"
PlacementTool = delivery.AssignerModule + ".assigner_read_placement"
FindingsTool = delivery.AssignerModule + ".assigner_read_findings"
WantsTool = assignerModule + ".assigner_read_wants"
PlacementTool = assignerModule + ".assigner_read_placement"
FindingsTool = assignerModule + ".assigner_read_findings"
// IndexerFindingsTool is the indexer's read tool of its findings: the indexer's state, too, is asked of its owner.
IndexerFindingsTool = delivery.IndexerModule + ".indexer_read_findings"
IndexerFindingsTool = indexerModule + ".indexer_read_findings"
)
// NewViews is the keeper's pictures of the assigner's wants, placement and findings and of the indexer's findings. A first view it cannot get past its
// bound is a finding of its own, taken in as any observer's: the keeper raises the condition itself.
func NewViews(k *Keeper) *views.Reader {
v := &views.Reader{Module: Module, Logf: logf, Findings: ownFindings{k}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
v.Follow(delivery.AssignerModule, "wants", WantsTool)
v.Follow(delivery.AssignerModule, "placement", PlacementTool)
v.Follow(delivery.AssignerModule, seat.StateFindings, FindingsTool)
v.Follow(delivery.IndexerModule, seat.StateFindings, IndexerFindingsTool)
v.Follow(assignerModule, "wants", WantsTool)
v.Follow(assignerModule, "placement", PlacementTool)
v.Follow(assignerModule, seat.StateFindings, FindingsTool)
v.Follow(indexerModule, seat.StateFindings, IndexerFindingsTool)
return v
}
@@ -9,7 +9,6 @@ import (
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"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
)
@@ -37,7 +36,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
// followed by their events, not read (ADR 0615 §1).
reads := []string{HavesRead, AttemptsRead}
for _, o := range Observers {
if o != delivery.AssignerModule && o != delivery.IndexerModule {
if o != assignerModule && o != indexerModule {
reads = append(reads, o+"."+seat.StateFindings)
}
}
+1
View File
@@ -3,6 +3,7 @@ module mesh-conditions
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000