Write a removal after a restart from the declaration the apply replaced (issue 1369)
mesh/repo-check head 150fadfe8bed merged into trunk 619e14aa8e57 — pass: its merge-check.sh passed
mesh/merge-gate head 150fadfe8bed merged into trunk 619e14aa8e57 — pass: group fix/r3e-before-the-gate composed: every machine composes with the change as …
mesh/delivery delivered
mesh/delivery-group group fix/r3e-before-the-gate delivered: every member is delivered

The set of modules this process wrote lives in memory, so a node-engine restarted before the apply that drops a module never wrote that module's removal. The report now names the modules the kept declaration held before, and they are removal candidates.
This commit is contained in:
2026-10-20 01:26:33 +00:00
parent 6a2fb63bfc
commit 150fadfe8b
4 changed files with 53 additions and 0 deletions
+16
View File
@@ -108,7 +108,23 @@ func (q *Queue) noteApply(r Report) bool {
for _, m := range r.Modules {
next[m.Module] = m
}
// Candidates: what this process wrote, what the declaration kept before this one held (a node-engine that started
// since wrote nothing yet), and what the last apply here held. writeRemoved creates no missing key.
gone := map[string]string{}
for module, build := range s.written {
gone[module] = build
}
for _, module := range r.Before {
if _, known := gone[module]; !known {
gone[module] = ""
}
}
for module, f := range s.modules {
if _, known := gone[module]; !known {
gone[module] = f.Build
}
}
for module, build := range gone {
if _, held := next[module]; !held {
if s.removed == nil {
s.removed = map[string]string{}
+21
View File
@@ -427,3 +427,24 @@ func TestARefusedRemovalIsWrittenByTheNextPass(t *testing.T) {
t.Errorf("b: %+v", b)
}
}
// 1369/F1: a node-engine that started since wrote nothing, and the report names the modules the kept declaration held
// before: one of them the new declaration lacks is written removed, one that is not in the bucket is not created.
func TestARestartedNodeEngineWritesTheRemovalFromTheReportsBefore(t *testing.T) {
l := newHaveLink()
l.put(t, "n1.a", delivery.Have{V: delivery.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.b", delivery.Have{V: delivery.Version, Build: "b-2", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
q, _ := aHaveQueue(l) // fresh: nothing written by this process
r := onlyA()
r.Before = []string{"a", "b", "c"}
q.tell(context.Background(), r, applied)
if b := l.have(t, "n1.b"); b.Phase != delivery.PhaseRemoved {
t.Errorf("b: %+v", b)
}
if _, there := l.kv["n1.c"]; there {
t.Error("c, which has no key, was created")
}
if l.kv["n1.a"].rev != 1 {
t.Errorf("a was written, revision %d", l.kv["n1.a"].rev)
}
}
+5
View File
@@ -188,6 +188,11 @@ type Report struct {
// have. Nil on a report that is not an apply's.
Modules []ModuleFact `json:"-"`
// Before is the names of the modules the declaration kept before this apply held, taken before it was replaced
// (issue 1369): a module of it that Modules lacks has left the node, whether or not this process wrote its have key.
// **Not on the wire.**
Before []string `json:"-"`
// Host is the version of the host that produced this report (novox/hq ADR 0141).
//
// Without it nothing can say a machine is behind, so "every machine current with its source"