From 1d8d7267584e83fd6740cf02c0dd01cdbf9ea157 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 01:13:11 +0100 Subject: [PATCH] Leave a record the node's own carried for a module that composes its own, and let that module's apply remove it, so the node's own is not refused for ever (issue 774) On the bus node restic's manifest replaced its process measure by awake (mesh-catalog 35ff67bd). The record of restic.measure was the node's, made while restic was folded, so restic's own apply never saw it, and the guard against removing a placed module's resources refused the node's own apply whole every two minutes from 2026-10-30 19:42: nothing of the node's own applied. Quick fix considered: forget the one record by hand (a hand act on the node; refused). Thorough fix (this): the node's own apply keeps such a record and says so, never refusing or removing it; a module's own apply owns a node record whose id names it, so it alone removes it once neither composition declares it. --- internal/apply/apply.go | 49 ++++++++++++++++++++++++++++- internal/apply/module_scope_test.go | 41 ++++++++++++++++++++---- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 8eb83d8..fd1432e 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -347,8 +347,19 @@ func (sc *Scope) placedOwner(a store.Applied) string { } // owns reports whether a record belongs to the scope's module. +// +// In a module's own apply, a record the node's own composition made for the module while it was folded (recorded as the +// node's, its id naming the module) is the module's too: it is the only apply that may remove it once neither +// composition declares it (issue 774). One the node's own still declares stays, as another's held resource does. func (sc *Scope) owns(a store.Applied) bool { - return store.OwnerOf(a, declaration.ModuleGuess) == sc.Module + if store.OwnerOf(a, declaration.ModuleGuess) == sc.Module { + return true + } + if sc.Module == store.NodeModule || a.Module != store.NodeModule { + return false + } + m, ok := declaration.ModuleGuess(a.ID) + return ok && m == sc.Module } // isNode is whether the scope is the node's own composition, the one that carries identities, groups, libraries and @@ -473,6 +484,29 @@ func applyScoped( } } + // **A resource the node's own carried for a module that now has a composition of its own is that module's to remove** + // (issue 774, 2026-10-30 on the bus node): recorded as the node's while the module was folded, it is no longer in the + // node's own composition and no longer in the module's own either (restic's measure, replaced by awake). The node's own + // apply leaves it, record and all, and says so; the module's own apply sees it as its own (Scope.owns) and removes it. + // Refused instead, the node's own applied nothing at all, every two minutes, for as long as the record stayed. + takenByItsModule := map[string]string{} + if scope != nil && scope.Module == store.NodeModule { + composes := map[string]bool{} + for _, r := range scope.Others { + if m := r.DeclaredModule(); m != "" && m != store.NodeModule { + composes[m] = true + } + } + for _, orphan := range known.Orphans(declared, origin) { + if orphan.Module != store.NodeModule { + continue + } + if m, ok := declaration.ModuleGuess(orphan.ID); ok && composes[m] { + takenByItsModule[orphan.ID] = m + } + } + } + // **A module a want still places here is never removed by another's apply** (issue 774): its resources go only with // its own apply or its want's removal. One that would is refused whole, before anything is changed, naming them: a // composition that leaves them out lost what runs, and removing what runs is the worst a node can do with that. @@ -485,6 +519,9 @@ func applyScoped( if _, kept := heldByOthers[orphan.ID]; kept { continue } + if _, kept := takenByItsModule[orphan.ID]; kept { + continue + } if m := scope.placedOwner(orphan); m != "" { refused = append(refused, orphan.ID+" of "+m) } @@ -513,6 +550,16 @@ func applyScoped( log(fmt.Sprintf(" kept %s (%s): %s", orphan.ID, orphan.Target, detail)) return nil } + if module, taken := takenByItsModule[orphan.ID]; taken { + detail := "no longer declared here; " + module + " has a composition of its own here, whose apply removes it, " + + "so it was left as it is" + report.Outcomes = append(report.Outcomes, Outcome{ + ID: orphan.ID, Type: orphan.Type, Target: orphan.Target, + Action: "kept", Detail: detail, + }) + log(fmt.Sprintf(" kept %s (%s): %s", orphan.ID, orphan.Target, detail)) + return nil + } if by, held := heldFiles[fileKey(orphan)]; held && by != orphan.ID { if scope != nil && !declared[by] { // Another module declares the path: it is that module's, so this record is let go and the file is diff --git a/internal/apply/module_scope_test.go b/internal/apply/module_scope_test.go index 51e42a5..1bdb768 100644 --- a/internal/apply/module_scope_test.go +++ b/internal/apply/module_scope_test.go @@ -125,12 +125,15 @@ func TestARecordWithoutAnOwnerBelongsToTheModuleItsIdNames(t *testing.T) { // want's removal lets it go. func TestAResourceThatMovedToItsModulesCompositionIsLeftByTheNodesOwnApply(t *testing.T) { for _, tc := range []struct { - name string - id string // the resource the node's own composition carried, and no longer does - moved bool // its module's own composition now declares it (Others) - placed bool // a want still places its module here - refused bool // the node's own apply is refused whole - stays bool // the resource is still on the machine and recorded + name string + id string // the resource the node's own composition carried, and no longer does + moved bool // its module's own composition now declares it (Others) + // dropped: its module has a composition of its own here that no longer declares it (Others holds another of its + // resources); the node's own apply leaves it, and the module's own apply removes it. + dropped bool + placed bool // a want still places its module here + refused bool // the node's own apply is refused whole + stays bool // the resource is still on the machine and recorded // wants names the modules whose want says removed (Scope.RemovedByWant); nil leaves the guard to Placed alone. wants []string }{ @@ -141,6 +144,11 @@ func TestAResourceThatMovedToItsModulesCompositionIsLeftByTheNodesOwnApply(t *te // it; in the node's own apply only a module a want removed loses its resources. {name: "a folded module with no want", id: "nats.server-conf", wants: []string{}, refused: true, stays: true}, {name: "a folded module a want removed", id: "nats.server-conf", wants: []string{"nats", "old"}}, + // 2026-10-30 19:42 on the bus node: restic's manifest replaced its process measure by awake. Its record was the + // node's, carried when restic was folded, so restic's own apply never saw it and the node's own was refused + // whole every two minutes, applying nothing of the node's own. + {name: "its module's own composition dropped it", id: "restic.measure", dropped: true, placed: true, + wants: []string{"old"}, stays: true}, } { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() @@ -159,6 +167,11 @@ func TestAResourceThatMovedToItsModulesCompositionIsLeftByTheNodesOwnApply(t *te moved := declare(t, `{"id":"`+tc.id+`","type":"file","path":"`+fact+`","content":"mesh\n","module":"`+module+`"}`) scope.Others = moved.Resources } + if tc.dropped { + later := declare(t, `{"id":"`+module+`.awake","type":"file","path":"`+filepath.Join(dir, "awake")+ + `","content":"a\n","module":"`+module+`"}`) + scope.Others = later.Resources + } if tc.placed { scope.Placed = map[string]bool{module: true} } @@ -188,6 +201,22 @@ func TestAResourceThatMovedToItsModulesCompositionIsLeftByTheNodesOwnApply(t *te if exists(gone) == !tc.refused { t.Fatalf("a resource no composition holds any more exists %v, with the apply refused %v", exists(gone), tc.refused) } + if tc.dropped { + if o := outcomeOf(report, tc.id); o.Action != "kept" { + t.Fatalf("the node's own apply did not say it left it: %+v", o) + } + later := declare(t, `{"id":"`+module+`.awake","type":"file","path":"`+filepath.Join(dir, "awake")+ + `","content":"a\n","module":"`+module+`"}`) + _, last, err := ApplyModule(context.Background(), archHost(t), later, Scope{Module: module}, after, + store.OriginDeclared, noServices, nil, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + if _, still := last.Find(tc.id); still || exists(fact) { + t.Fatalf("the module's own apply left what it no longer declares: recorded %v, on the machine %v", + still, exists(fact)) + } + } }) } } -- 2.54.0