Keep reading the kept declaration while a placed module runs at a build of the old form, so the node's own never drops it (issue 774)

At 19:43:01 on 2026-10-30 shanks' new engine said "the kept declaration is no longer read: each of the 77 modules
placed here has a have of its own", and from 19:43:57 every apply of the node's own was refused: it would remove
systemd-resolved's resources (novox the same for nats and restic). systemd-resolved's have names only a controller
build (build-1791404191702719760), no fingerprint, so the assigner wants it nowhere and it runs folded in the node's
own, of which the kept declaration is the one record; retired, the folded module was composed as nothing.

Quick fix: never retire the kept declaration. Thorough fix: allHave counts only a have naming a fingerprint of the new
form (or removed); a module at a build of the old form keeps the read, named with why, until it has one. Chose
thorough: the read retires by itself once every placed module is wanted.
This commit is contained in:
2026-10-30 20:27:12 +01:00
parent 33131dcbfe
commit d5e8458340
2 changed files with 21 additions and 10 deletions
+12 -6
View File
@@ -251,25 +251,31 @@ func placedAllHave(ctx context.Context, session *link.Session, node string) (boo
if err != nil {
return false, err.Error()
}
return allHave(p, func(module string) (bool, error) {
_, ok, err := session.ReadHaveErr(ctx, module)
return ok, err
return allHave(p, func(module string) (delivery.Have, bool, error) {
return session.ReadHaveErr(ctx, module)
})
}
// allHave says whether has finds a have for every module p places, and why not.
func allHave(p delivery.Placement, has func(module string) (bool, error)) (bool, string) {
// allHave says whether has finds, for every module p places, a have that names a fingerprint of the new form, and why
// not. A module whose have names only a build of the old form runs inside the node's own composition, and the kept
// declaration is the one record of what runs of it: retired then, the node's own composed it as nothing and refused to
// remove what runs (issue 774, 2026-10-30 19:43: systemd-resolved on shanks, nats and restic on novox, each node's own
// apply refused from then on).
func allHave(p delivery.Placement, has func(module string) (delivery.Have, bool, error)) (bool, string) {
if len(p.Modules) == 0 {
return false, "the seat mesh-placement places no module here"
}
for _, m := range p.Modules {
ok, err := has(m.Module)
h, ok, err := has(m.Module)
if err != nil {
return false, "the have of " + m.Module + " is not read: " + err.Error()
}
if !ok {
return false, m.Module + " has no have yet"
}
if h.Phase != delivery.PhaseRemoved && !strings.HasPrefix(h.FP, delivery.SourcePrefix2) {
return false, m.Module + " runs at a build of the old form (" + h.Build + "), which only the kept declaration records"
}
}
return true, fmt.Sprintf("each of the %d modules placed here has a have of its own", len(p.Modules))
}
+9 -4
View File
@@ -116,15 +116,20 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
t.Fatalf("before the read is retired: %+v, %v", d, err)
}
placed := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}}
// Issue 774, 2026-10-30 19:43: a have naming only a build of the old form (systemd-resolved on shanks, folded in the
// node's own) keeps the kept declaration read, since it is the one record of what runs of it.
newForm := delivery.Have{FP: delivery.SourcePrefix2 + strings.Repeat("a", 64), Phase: delivery.PhaseRunning}
oldForm := delivery.Have{Build: "build-1791404191702719760", Phase: delivery.PhaseRunning}
for _, row := range []struct {
name string
has map[string]bool
has map[string]delivery.Have
want bool
}{
{"every placed module has a have", map[string]bool{"zsh": true, "lemurs": true}, true},
{"one has none yet", map[string]bool{"zsh": true}, false},
{"every placed module has a have of the new form", map[string]delivery.Have{"zsh": newForm, "lemurs": newForm}, true},
{"one has none yet", map[string]delivery.Have{"zsh": newForm}, false},
{"one runs at a build of the old form", map[string]delivery.Have{"zsh": newForm, "lemurs": oldForm}, false},
} {
if got, why := allHave(placed, func(m string) (bool, error) { return row.has[m], nil }); got != row.want {
if got, why := allHave(placed, func(m string) (delivery.Have, bool, error) { h, ok := row.has[m]; return h, ok, nil }); got != row.want {
t.Errorf("%s: %v (%s)", row.name, got, why)
}
}