Give each delivery's hold bound its own key's time to live and map a seat's finding to the module the placement says holds it, so the keeper holds the right findings for the right time (issue 1952 C7)
This commit is contained in:
@@ -52,8 +52,14 @@ func (k *Keeper) aboutOf(kind, source string, s health.Subject) []about {
|
||||
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 {
|
||||
out = append(out, about{Node: parts[len(parts)-1], Module: strings.Join(parts[:len(parts)-1], ".")})
|
||||
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
|
||||
@@ -143,9 +149,8 @@ func (k *Keeper) keepHeld(h held) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// bound is a module's delivery bound: its manifest names none yet, so the default (clause 13 (d)). The state `bounds`
|
||||
// keeps each key that long (its ttl-seconds), since the runtime writes no key with a time to live of its own; a bound a
|
||||
// manifest names needs that first.
|
||||
// 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
|
||||
@@ -211,6 +216,33 @@ func (k *Keeper) Have(key string, value []byte, deleted bool) error {
|
||||
return k.judge(node, module, "")
|
||||
}
|
||||
|
||||
// Placement takes one change of the placement: which module holds which seat on a node.
|
||||
func (k *Keeper) Placement(key string, value []byte, deleted bool) {
|
||||
node, err := delivery.ParsePlacementKey(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.init()
|
||||
for hk := range k.holders {
|
||||
if strings.HasPrefix(hk, node+".") {
|
||||
delete(k.holders, hk)
|
||||
}
|
||||
}
|
||||
if deleted {
|
||||
return
|
||||
}
|
||||
p, err := delivery.DecodePlacement(value)
|
||||
if err != nil {
|
||||
k.Logf("the placement of %s is not readable, so its seats' holders are not known: %v", node, err)
|
||||
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)
|
||||
|
||||
@@ -71,6 +71,10 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
if err := r.k.Controller("core.controller.deaf", deaf, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 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 {
|
||||
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 {
|
||||
t.Errorf("%s: during the delivery said %q: another module's and the machine's at once, nothing of its own", c.name, got)
|
||||
}
|
||||
@@ -106,4 +110,20 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
t.Errorf("%s: the carried deaf condition was raised, or something is still held: %+v", c.name, r.k.Held())
|
||||
}
|
||||
}
|
||||
// A seat's finding is about the module the placement says holds the seat on that node (the lead, 2026-10-29): held
|
||||
// while the assigner is delivered on ace, though the seat is named mesh-placement.
|
||||
r := newRig()
|
||||
placed, err := delivery.EncodePlacement(delivery.Placement{Node: "ace", Modules: []delivery.PlacedModule{{Module: "mesh-delivery-assigner"}},
|
||||
Holds: []delivery.Holding{{Claim: "mesh-placement", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.k.Placement("placement.ace", placed, 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)
|
||||
seat, seatRaw := finding(t, selfCheck, health.ScopeSeat, "mesh-placement.ace", "holder-silent", "D3", health.Warning, r.now)
|
||||
_ = r.k.Finding(selfCheck, seat, 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,11 @@ const (
|
||||
ControllerLease = "mesh-controller.lease"
|
||||
ControllerLeaseKey = "holder"
|
||||
WantsRead = delivery.AssignerModule + ".wants"
|
||||
HavesRead = "node-engine.have"
|
||||
ConditionWrites = 8
|
||||
// PlacementRead is the mesh-placement 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
|
||||
HavesRead = "node-engine.have"
|
||||
ConditionWrites = 8
|
||||
)
|
||||
|
||||
// Store is the bus as the keeper uses it: its own states, written by compare-and-set, and a key with its own time to
|
||||
@@ -104,8 +107,10 @@ type Keeper struct {
|
||||
takeover health.Takeover
|
||||
wants map[string]delivery.Want
|
||||
haves map[string]delivery.Have
|
||||
stuck map[string]string
|
||||
held map[string]held
|
||||
// holders are, by `<node>.<seat>`, the module the placement says holds the seat there.
|
||||
holders map[string]string
|
||||
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
|
||||
// line a key raised out of a hold says (hold.go).
|
||||
passed map[string]string
|
||||
@@ -121,7 +126,7 @@ func (k *Keeper) init() {
|
||||
k.wants, k.haves, k.stuck, k.held, k.cleared = map[string]delivery.Want{}, map[string]delivery.Have{},
|
||||
map[string]string{}, map[string]held{}, map[string]clearedMemo{}
|
||||
k.takeover = health.Takeover{V: health.TakeoverVersion, Follow: health.FollowController}
|
||||
k.passed, k.outOfHold = map[string]string{}, map[string]string{}
|
||||
k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{}
|
||||
}
|
||||
if k.Now == nil {
|
||||
k.Now = time.Now
|
||||
|
||||
@@ -20,6 +20,8 @@ type fakeStore struct {
|
||||
vals map[string][]byte
|
||||
revs map[string]uint64
|
||||
next uint64
|
||||
// ttls are the times to live the keys were created with.
|
||||
ttls map[string]time.Duration
|
||||
}
|
||||
|
||||
func newFakeStore() *fakeStore {
|
||||
@@ -79,7 +81,13 @@ func (f *fakeStore) Keys(state string) ([]string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) CreateTTL(state, key string, value any, _ time.Duration) error {
|
||||
func (f *fakeStore) CreateTTL(state, key string, value any, ttl time.Duration) error {
|
||||
f.mu.Lock()
|
||||
if f.ttls == nil {
|
||||
f.ttls = map[string]time.Duration{}
|
||||
}
|
||||
f.ttls[state+"/"+key] = ttl
|
||||
f.mu.Unlock()
|
||||
_, err := f.Create(state, key, value)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -44,10 +44,9 @@ func (runtimeStore) Delete(state, key string) error { return stdio.State(state).
|
||||
|
||||
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
||||
|
||||
// CreateTTL creates the key in a state whose own age is the time to live (`bounds`, ttl-seconds): the runtime writes no
|
||||
// key with a time to live of its own.
|
||||
func (runtimeStore) CreateTTL(state, key string, value any, _ time.Duration) error {
|
||||
_, err := stdio.State(state).Create(key, value)
|
||||
// CreateTTL creates the key with its own time to live (`bounds` is key-ttl); its expiry is handed over as OpExpire.
|
||||
func (runtimeStore) CreateTTL(state, key string, value any, ttl time.Duration) error {
|
||||
_, err := stdio.State(state).CreateTTL(key, value, ttl)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -132,11 +131,15 @@ func Keep(k *Keeper) {
|
||||
})
|
||||
}
|
||||
watch(k, BoundsState, "", func(c stdio.StateChange) error {
|
||||
if c.Op != "put" && !c.Current {
|
||||
if c.Op == stdio.OpExpire {
|
||||
return said(k.BoundPassed(c.Key))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
watch(k, PlacementRead, "", func(c stdio.StateChange) error {
|
||||
k.Placement(c.Key, c.Value, c.Op != stdio.OpPut)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
!slices.Equal(Verbs, catalogue.VerbNames(seat.Serves)) {
|
||||
t.Errorf("module %s runs as %s and claims %+v; the seat serves %v", m.Module, m.RunsAs, m.Claims, catalogue.VerbNames(seat.Serves))
|
||||
}
|
||||
reads := []string{ControllerConditions, ControllerLease, WantsRead, HavesRead, AttemptsRead}
|
||||
reads := []string{ControllerConditions, ControllerLease, WantsRead, HavesRead, AttemptsRead, PlacementRead}
|
||||
for _, o := range Observers {
|
||||
reads = append(reads, o+"."+health.FindingsState)
|
||||
}
|
||||
@@ -39,12 +39,14 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
if !slices.Equal(got, reads) || slices.Contains(m.Reads, health.HealersModule+"."+health.WouldState) {
|
||||
t.Errorf("reads %v, want %v and never the healers' would", got, reads)
|
||||
}
|
||||
ttl := map[string]int{}
|
||||
ttl, keyTTL := map[string]int{}, map[string]bool{}
|
||||
for _, s := range m.State {
|
||||
ttl[s.Name] = s.TTLSeconds
|
||||
ttl[s.Name], keyTTL[s.Name] = s.TTLSeconds, s.KeyTTL
|
||||
}
|
||||
if ttl[HistoryState] != int(HistoryKeptFor.Seconds()) || ttl[BoundsState] != 900 {
|
||||
t.Errorf("history is kept %d s and a bound %d s", ttl[HistoryState], ttl[BoundsState])
|
||||
// Each bound is its own key's time to live (the runtime's per-key TTL), never the bucket's age.
|
||||
if ttl[HistoryState] != int(HistoryKeptFor.Seconds()) || ttl[BoundsState] != 0 || !keyTTL[BoundsState] {
|
||||
t.Errorf("history is kept %d s, a bound's bucket ages %d s, its keys have their own time to live: %v",
|
||||
ttl[HistoryState], ttl[BoundsState], keyTTL[BoundsState])
|
||||
}
|
||||
// Clause 12: each state with the tool that reads it, each act with the tool that starts it.
|
||||
readBy := map[string]string{ConditionsState: Seat + ".conditions", HistoryState: HistoryTool, TakeoverState: TakeoverTool,
|
||||
|
||||
@@ -261,7 +261,7 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buckets := []string{"mesh-controller_conditions", "mesh-controller_lease", "mesh-delivery-assigner_wants", "node-engine_have",
|
||||
buckets := []string{"mesh-controller_conditions", "mesh-controller_lease", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_placement",
|
||||
"mesh-healers_attempts"}
|
||||
for _, o := range Observers {
|
||||
buckets = append(buckets, o+"_findings")
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-conditions
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029210043-32f5ce371268
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029211442-bc6c8a6d244a
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029210043-32f5ce371268 h1:+jH77IJmxUiFDdbJljhynQRY0Cc5/5jtwmMV7UUqRI4=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029210043-32f5ce371268/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029211442-bc6c8a6d244a h1:haFs0zoHDUOrdtKn0Cw9lhCg/K3Me6Ff3Mg+IdVlK34=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029211442-bc6c8a6d244a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"mesh-controller.conditions",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"node-engine.have"
|
||||
"node-engine.have",
|
||||
"mesh-delivery-assigner.placement"
|
||||
],
|
||||
"state": [
|
||||
"conditions",
|
||||
@@ -40,7 +41,6 @@
|
||||
"held",
|
||||
{
|
||||
"name": "bounds",
|
||||
"ttl-seconds": 900,
|
||||
"key-ttl": true
|
||||
},
|
||||
"asked"
|
||||
|
||||
Reference in New Issue
Block a user