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 {