Read what a node runs inside its own composition from its haves, not from what the agent applied, so a restarted agent's first composition keeps the bus (issue 1126, review of the fold round 1)
mesh/delivery merged: source.main moved to novox/mesh-host@50c229f90568
mesh/delivery-group group fix/1126-fold merged: every member is merged
mesh/check checking 69b845c0f7b2 on 0d22776b20ef | novox/mesh-host#152 in group fix/1126-fold

A fresh agent composes before it has applied anything (Running empty), so
InOwn of Running folded only mesh-wireguard and the switch would drop nats.
The composer now watches the node's haves: a placed module whose have names no
new-form fingerprint is folded; one with a new-form have whose want is removed
is not folded. The shadow uses the same set, over the candidate's placement.
This commit is contained in:
2026-10-27 09:03:59 +01:00
parent caafd9306e
commit 69b845c0f7
3 changed files with 118 additions and 38 deletions
+29 -8
View File
@@ -112,21 +112,33 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// oldie is placed and no want names it (a module at a build of the old form, nats on novox): the node runs it inside
// its own composition, and composes it there again, even before the agent has applied anything (Running empty).
oldie := `{"module":"oldie","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"oldie.service","state":"running","boot":"enabled"}]}`
odigest := delivery.ManifestDigest([]byte(oldie))
oman, _ := delivery.EncodeManifest(delivery.Manifest{Module: "oldie", JSON: oldie})
okey, _ := delivery.ManifestsKey(odigest)
b.kv[delivery.ManifestsBucket+"/"+okey] = oman
omod, _ := delivery.EncodeModule(delivery.Module{FP2: delivery.SourcePrefix2 + strings.Repeat("c", 64), Manifest: odigest, Repository: "novox/o", Commit: "c"})
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
b.put(t, delivery.PlacementBucket, "placement.n1", place)
b.put(t, delivery.HaveBucket, "n1.node", have)
oldHave, _ := delivery.EncodeHave(delivery.Have{Build: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
b.put(t, delivery.HaveBucket, "n1.oldie", oldHave)
b.put(t, delivery.ModulesBucket, "lemurs", mod)
b.put(t, delivery.ModulesBucket, "oldie", omod)
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket} {
b.ch(bucket) <- agent.Update{Caught: true}
}
got := map[string]bool{}
var own delivery.Composed
for {
u := next(t, out)
if u.Caught {
@@ -139,8 +151,18 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
if c.Node != "n1" {
t.Errorf("%s composed for %q", u.Key, c.Node)
}
if c.Module == "" {
own = c
}
got[u.Key] = true
}
var ownText []string
for _, r := range own.Resources {
ownText = append(ownText, string(r))
}
if !strings.Contains(strings.Join(ownText, " "), `"id":"oldie.service"`) {
t.Errorf("the node's own composition, composed before anything was applied, lacks oldie: %v", ownText)
}
lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fp)
nodeKey, _ := delivery.ComposedNodeKey("n1")
if !got[lemursKey] || !got[nodeKey] || len(got) != 2 {
@@ -196,7 +218,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.mu.Unlock()
kmod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"})
both, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "keeper"}}})
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}, {Module: "keeper"}}})
b.put(t, delivery.ModulesBucket, "keeper", kmod)
b.put(t, delivery.PlacementBucket, "placement.n1", both)
b.put(t, delivery.AssignerWantsBucket, "want.n1.keeper", want)
@@ -448,13 +470,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// Issue 1126, the seed's review: a module n1 runs inside its own composition (nats, at a build of the old form) is
// composed there again, not at the trunk's build as one added; lemurs at the build it runs, keeper (added) at the
// trunk's.
inOwnRunning := []delivery.Composed{{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"nats.server","type":"service"}`)}},
{Node: "n1", Module: "lemurs", FP: fp}}
inOwnRunning := []delivery.Composed{{Node: "n1", Module: "lemurs", FP: fp}}
other := "src2:" + strings.Repeat("9", 64)
gotFps, gotInOwn := shadowBuilds(inOwnRunning, delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
map[string]delivery.Module{"lemurs": {FP2: other}, "nats": {FP2: other}, "keeper": {FP2: other}})
if fmt.Sprint(gotFps) != fmt.Sprint(map[string]string{"lemurs": fp, "keeper": other}) || fmt.Sprint(gotInOwn) != "[nats]" {
t.Errorf("shadow builds %v, in its own %v; want lemurs at what runs, keeper at the trunk's, nats in its own", gotFps, gotInOwn)
gotFps := shadowBuilds(inOwnRunning, delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
map[string]delivery.Module{"lemurs": {FP2: other}, "nats": {FP2: other}, "keeper": {FP2: other}}, []string{"nats"})
if fmt.Sprint(gotFps) != fmt.Sprint(map[string]string{"lemurs": fp, "keeper": other}) {
t.Errorf("shadow builds %v; want lemurs at what runs, keeper at the trunk's, nats at none (folded)", gotFps)
}
}