Merge pull request 'Keep the kept declaration read while a module runs at an old-form build, so novox and shanks apply their own composition again (issue 774)' (#182) from fix/kept-read-and-foreseen-account into main

This commit was merged in pull request #182.
This commit is contained in:
2026-10-30 19:49:17 +00:00
5 changed files with 59 additions and 13 deletions
+14 -6
View File
@@ -251,25 +251,33 @@ func placedAllHave(ctx context.Context, session *link.Session, node string) (boo
if err != nil { if err != nil {
return false, err.Error() return false, err.Error()
} }
return allHave(p, func(module string) (bool, error) { return allHave(p, func(module string) (delivery.Have, bool, error) {
_, ok, err := session.ReadHaveErr(ctx, module) return session.ReadHaveErr(ctx, module)
return ok, err
}) })
} }
// allHave says whether has finds a have for every module p places, and why not. // allHave says whether has finds, for every module p places, a have that names a fingerprint of the new form, and why
func allHave(p delivery.Placement, has func(module string) (bool, error)) (bool, string) { // 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 { if len(p.Modules) == 0 {
return false, "the seat mesh-placement places no module here" return false, "the seat mesh-placement places no module here"
} }
for _, m := range p.Modules { for _, m := range p.Modules {
ok, err := has(m.Module) h, ok, err := has(m.Module)
if err != nil { if err != nil {
return false, "the have of " + m.Module + " is not read: " + err.Error() return false, "the have of " + m.Module + " is not read: " + err.Error()
} }
if !ok { if !ok {
return false, m.Module + " has no have yet" return false, m.Module + " has no have yet"
} }
// A module the mesh provides (mesh-wireguard) never has a fingerprint: the node's own composes it from the manifest
// mesh-sdk provides (delivery.ProvidedManifest), never from the kept declaration, so it keeps no read.
if h.Phase != delivery.PhaseRemoved && !strings.HasPrefix(h.FP, delivery.SourcePrefix2) && !delivery.ProvidedByMesh(m.Module) {
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)) return true, fmt.Sprintf("each of the %d modules placed here has a have of its own", len(p.Modules))
} }
+20 -4
View File
@@ -12,6 +12,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/liveness" "github.com/novox/mesh-host/internal/liveness"
@@ -116,18 +117,33 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
t.Fatalf("before the read is retired: %+v, %v", d, err) t.Fatalf("before the read is retired: %+v, %v", d, err)
} }
placed := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}} 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 { for _, row := range []struct {
name string name string
has map[string]bool has map[string]delivery.Have
want bool want bool
}{ }{
{"every placed module has a have", map[string]bool{"zsh": true, "lemurs": true}, true}, {"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]bool{"zsh": true}, false}, {"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) t.Errorf("%s: %v (%s)", row.name, got, why)
} }
} }
// The module the mesh provides never has a fingerprint, and keeps no read (review of issue 774).
withWireguard := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}}
if got, why := allHave(withWireguard, func(m string) (delivery.Have, bool, error) {
if m == overlay.Name {
return oldForm, true, nil
}
return newForm, true, nil
}); !got {
t.Errorf("%s without a fingerprint beside zsh at the new form keeps the read: %s", overlay.Name, why)
}
if got, _ := allHave(delivery.Placement{Node: "n1"}, nil); got { if got, _ := allHave(delivery.Placement{Node: "n1"}, nil); got {
t.Error("a node with nothing placed retires the read") t.Error("a node with nothing placed retires the read")
} }
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-host
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030184532-9685990a15ed git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030193354-4e9621edd664
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030184532-9685990a15ed h1:0h1S5YXrMdkjhB7gWzugqdLxb/HgDHWDmaVCNbEDAzc= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030193354-4e9621edd664 h1:K23Dwp+MXwji+BIBUG5Rl6hvzI2FgggwPHWCVWxkSh4=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030184532-9685990a15ed/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030193354-4e9621edd664/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+22
View File
@@ -20,6 +20,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/compose"
"git.novox.be/novox/mesh-sdk/go/compose/frombus" "git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/agent"
@@ -398,6 +399,27 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
if len(*told4) != 1 || !strings.HasPrefix((*told4)[0], "held power: ") || !strings.Contains((*told4)[0], `"shows" is none of them`) { if len(*told4) != 1 || !strings.HasPrefix((*told4)[0], "held power: ") || !strings.Contains((*told4)[0], `"shows" is none of them`) {
t.Errorf("the agent was told %q, want power held alone, naming its item", *told4) t.Errorf("the agent was told %q, want power held alone, naming its item", *told4)
} }
// Review of issue 774 (the kept read): mesh-wireguard, which the mesh provides and which never has a fingerprint, is
// folded in the node's own and composed from the manifest mesh-sdk provides (delivery.ProvidedManifest), not from
// what runs nor the kept declaration: with nothing running (the kept read retired, Running empty) the node's own
// still carries its resources, so retiring the read removes nothing of it.
c5, told5 := fresh(env.Get)
p5 := c5.placements["n1"]
p5.Modules = append(append([]delivery.PlacedModule{}, p5.Modules...), delivery.PlacedModule{Module: overlay.Name})
p5.Decided.Endpoint = "n1.example:51820"
c5.placements["n1"] = p5
c5.inOwn[overlay.Name] = true
nodeKey5, _ := delivery.ComposedNodeKey("n1")
var own5 string
for _, u := range c5.compose(ctx) {
if u.Key == nodeKey5 {
own5 = string(u.Value)
}
}
if len(*told5) != 0 || !strings.Contains(own5, overlay.Name+".") {
t.Errorf("the node's own with %s folded and nothing running: told %q, carries %s: %.300s", overlay.Name, *told5,
overlay.Name, own5)
}
// A read the bus did not answer, timed out or not, is tried again later; the run's own end and a refusal are not. // A read the bus did not answer, timed out or not, is tried again later; the run's own end and a refusal are not.
if !BusFailed(fmt.Errorf("the revisions of it2.mailer.domain: %w", context.DeadlineExceeded)) || !BusFailed(fmt.Errorf("x: %w", nats.ErrNoResponders)) || if !BusFailed(fmt.Errorf("the revisions of it2.mailer.domain: %w", context.DeadlineExceeded)) || !BusFailed(fmt.Errorf("x: %w", nats.ErrNoResponders)) ||