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)
mesh/delivery merged: source.main moved to novox/mesh-host@62b211bb6040
mesh/check checking 1d8d7267584e on 62b211bb6040

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.
This commit is contained in:
2026-10-31 01:13:11 +01:00
parent 91fb323ce0
commit 1d8d726758
2 changed files with 83 additions and 7 deletions
+48 -1
View File
@@ -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
+35 -6
View File
@@ -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))
}
}
})
}
}