From b71358e2cc14e73d0447d06d9865ccfbb63d7bc6 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 19:54:16 +0100 Subject: [PATCH] A put without a hold ends once the node runs its fingerprint healthy, and assigner_puts says a put older than the newest build A put with hold false waited for the node's composition digest to equal its own and never ended when the node composes its own, pinning the module's want (issue 1966, F21). Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/mesh-delivery-assigner/pass_test.go | 7 ++++++ .../cmd/mesh-delivery-assigner/tools.go | 23 ++++++++++++++++++- .../internal/assign/put.go | 10 +++++++- .../internal/assign/wants_test.go | 15 ++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index da114536..ee81b744 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -458,6 +458,13 @@ func putOfTheBus(t *testing.T) { if got := step(); got.Step != puts.Released { t.Fatalf("released, the put stands at %s", got.Step) } + // A put that stands at an older version than the newest says so (issue 1966): it pins the node's want. + okey, _ := puts.Key("g14", "mesh-catalog") + older, _ := puts.Encode(puts.Put{FP: meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64), Why: "old"}.Said(now, puts.Wanted, "wanted")) + putsKV.values[okey] = older + if l, _ := s.doPuts(map[string]any{"module": "mesh-catalog"}); l.(map[string]any)["puts"].([]putAnswer)[0].Older != fp { + t.Errorf("a put older than the newest build says %+v", l) + } listed, _ := s.doPuts(map[string]any{"module": "nats"}) if l := listed.(map[string]any)["puts"].([]putAnswer); len(l) != 1 || l[0].Step != puts.Released || len(l[0].Steps) < 6 { t.Errorf("puts lists %+v", l) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go index 74c56486..877b6c1e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go @@ -130,6 +130,9 @@ type putAnswer struct { Module string `json:"module"` puts.Put Follow string `json:"follow,omitempty"` + // Older says a put that has not ended stands at an older version than the newest build, and which that is: it pins the + // node's want until it ends or is released. + Older string `json:"older-than-newest,omitempty"` } // doPut is the tool assigner_put. @@ -249,6 +252,8 @@ func (s *putTools) doPuts(args map[string]any) (any, error) { } sort.Strings(keys) out := []putAnswer{} + newest := map[string]string{} // the indexer's newest fingerprint by module, asked once and only for a put that stands + asked := false for _, key := range keys { node, module, err := puts.ParseKey(key) if err != nil || (arg(args, "node") != "" && node != arg(args, "node")) || (arg(args, "module") != "" && module != arg(args, "module")) { @@ -262,7 +267,23 @@ func (s *putTools) doPuts(args map[string]any) (any, error) { if err != nil { continue } - out = append(out, putAnswer{Node: node, Module: module, Put: p}) + a := putAnswer{Node: node, Module: module, Put: p} + if !puts.Ended(p.Step) { + if !asked { + asked = true + if mods, err := indexerView(s.ask, indexerModules); err == nil { + for name, raw := range mods { + if m, err := meshcatalogclient.DecodeModule(raw); err == nil { + newest[name] = m.Fp2 + } + } + } + } + if n := newest[module]; n != "" && n != p.FP { + a.Older = n + } + } + out = append(out, a) } return map[string]any{"puts": out}, nil } diff --git a/modules/mesh-delivery-assigner/internal/assign/put.go b/modules/mesh-delivery-assigner/internal/assign/put.go index da4c7aa5..34d7ae9b 100644 --- a/modules/mesh-delivery-assigner/internal/assign/put.go +++ b/modules/mesh-delivery-assigner/internal/assign/put.go @@ -157,7 +157,15 @@ func StepPut(w PutWorld) PutNext { // applied is a wanted put once the node's have says how it went: holding or done when it runs the composition healthy. func applied(w PutWorld, p puts.Put) puts.Put { h := w.Have - if h == nil || h.Fp != p.FP || h.Composed != p.Composed { + if h == nil || h.Fp != p.FP { + return p + } + // A put without a hold ends on the fingerprint alone: a node that composes its own digests no composition of the put's, and + // a put that waits for that one pins the module's want for good (issue 1966). A held put keeps its composition. + if !p.Hold && h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK { + return p.Said(w.Now, puts.Done, fmt.Sprintf("%s runs %s at %s healthy; the rules write its want again", w.Node, w.Module, short(p.FP))) + } + if h.Composed != p.Composed { return p } switch { diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 7dc925fc..d15823b1 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -933,6 +933,21 @@ func stepsOfAPut(t *testing.T) { w.Have = &nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: d.Digest, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} return w }, puts.Done, "the rules write its want again", false, false, false}, + {"wanted without hold, the node composes its own: done at the fingerprint", func() PutWorld { + w := bus() + w.Put.Hold = false + w.Put = w.Put.Said(now, puts.Wanted, "wanted") + w.Put.Composed = d.Digest + w.Have = &nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: dig(fOld), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} + return w + }, puts.Done, "the rules write its want again", false, false, false}, + {"wanted with hold, the node composes its own: stays wanted", func() PutWorld { + w := bus() + w.Put = w.Put.Said(now, puts.Wanted, "wanted") + w.Put.Composed = d.Digest + w.Have = &nodeengine.HaveByNodeModuleValue{Fp: fNew, Composed: dig(fOld), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} + return w + }, puts.Wanted, "wanted", false, false, false}, {"released: nothing more", func() PutWorld { w := bus(); w.Put = w.Put.Said(now, puts.Released, "released"); return w }, puts.Released, "released", false, false, false}, } for _, c := range cases {