A put without a hold ends once the node runs its fingerprint healthy #192

Merged
mesh-tools merged 1 commits from fix/1966-put-ends into main 2026-11-09 19:37:53 +00:00
4 changed files with 53 additions and 2 deletions
@@ -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)
@@ -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
}
@@ -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 {
@@ -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 {