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
+71 -17
View File
@@ -71,10 +71,13 @@ type Composer struct {
wants map[string]delivery.Want // by module, the node's own under agent.NodeModule
placements map[string]delivery.Placement
records map[string]delivery.NodeRecord
modules map[string]delivery.Module
values map[string]map[string]any // by module, this node's
manifests map[string][]byte // by digest, read once
handed map[string]string // composed key to the digest handed
// inOwn is, by module, whether this node's have of it names no fingerprint of the new form (mesh-wireguard, a module
// at a build of the old form): the node runs it inside its own composition, and composes it there again (issue 1126).
inOwn map[string]bool
modules map[string]delivery.Module
values map[string]map[string]any // by module, this node's
manifests map[string][]byte // by digest, read once
handed map[string]string // composed key to the digest handed
// sealedOwn is each own secret's sealed value as first handed, by module/name and generation: a value sealed again
// is other bytes, so the one first had is kept while the generation stands, and the composition stays the same.
sealedOwn map[string]ownSealed
@@ -90,7 +93,7 @@ func New(env Env) *Composer {
env.Say = func(string) {}
}
return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{},
records: map[string]delivery.NodeRecord{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{},
records: map[string]delivery.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{},
manifests: map[string][]byte{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{}}
}
@@ -105,16 +108,19 @@ type ownSealed struct {
type input struct {
bucket, pattern string
take func(c *Composer, u agent.Update)
// changed, when set, takes the update in take's place and says whether it changed anything composed.
changed func(c *Composer, u agent.Update) bool
}
func (c *Composer) inputs() []input {
node := c.env.Node
return []input{
{delivery.AssignerWantsBucket, "want." + node + ".>", (*Composer).takeWant},
{delivery.PlacementBucket, ">", (*Composer).takePlacement},
{delivery.HaveBucket, "*." + delivery.HaveNodeWord, (*Composer).takeRecord},
{delivery.ModulesBucket, ">", (*Composer).takeModule},
{ResolvedBucket, node + ".>", (*Composer).takeValue},
{delivery.AssignerWantsBucket, "want." + node + ".>", (*Composer).takeWant, nil},
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
// Every node's record, and this node's haves: what it runs inside its own composition.
{delivery.HaveBucket, ">", nil, (*Composer).takeHave},
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
}
}
@@ -193,6 +199,8 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er
dirty = true // a secret changed: its new generation is asked of the vault at the compose
case t.u.Caught:
caught[t.in] = true
case ins[t.in].changed != nil:
dirty = ins[t.in].changed(c, t.u) || dirty
default:
ins[t.in].take(c, t.u)
dirty = true
@@ -302,6 +310,56 @@ func (c *Composer) takeRecord(u agent.Update) {
c.records[node] = *h.Record
}
// takeHave takes a have key: a node's record (always composed again), or one of this node's modules' haves, which
// changes what is composed only when whether it runs inside the node's own composition changes.
func (c *Composer) takeHave(u agent.Update) bool {
if isNodeKey(u.Key) {
c.takeRecord(u)
return true
}
return c.takeModuleHave(u)
}
// takeModuleHave takes one of this node's modules' haves, and says whether it changed which it runs inside its own
// composition: one whose have names no fingerprint of the new form and is not removed (nats on novox: a build-...
// id; mesh-wireguard: none). Read from the haves, which outlast a restart, never from what the agent has applied
// this run: a fresh agent composes before it applies anything (review of the fold, round 1).
func (c *Composer) takeModuleHave(u agent.Update) bool {
node, module, isNode, err := delivery.ParseHaveKey(u.Key)
if err != nil || isNode || node != c.env.Node {
return false
}
in := false
if !u.Deleted {
if h, err := delivery.DecodeHave(u.Value); err == nil {
in = h.Phase != delivery.PhaseRemoved && !strings.HasPrefix(h.FP, delivery.SourcePrefix2)
}
}
c.mu.Lock()
defer c.mu.Unlock()
if c.inOwn[module] == in {
return false
}
if in {
c.inOwn[module] = true
} else {
delete(c.inOwn, module)
}
return true
}
// inOwnOf is, of the modules p places, those the node runs inside its own composition (c.inOwn), sorted; c.mu held.
func (c *Composer) inOwnOf(p delivery.Placement) []string {
var out []string
for _, m := range p.Modules {
if c.inOwn[m.Module] {
out = append(out, m.Module)
}
}
sort.Strings(out)
return out
}
func (c *Composer) takeModule(u agent.Update) {
name, err := delivery.ParseModulesKey(u.Key)
if err != nil {
@@ -359,15 +417,11 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
}
_, nodeWanted := c.wants[agent.NodeModule]
placement := c.placements[c.env.Node]
// What it runs inside its own composition and no want names at a build of the new form stays there, as the
// controller's composed writer folds it (issue 1126); compose folds none a want names.
inOwn := c.inOwnOf(c.placements[c.env.Node])
r := reader{c: c}
c.mu.Unlock()
// What it runs inside its own composition and no want names (mesh-wireguard, a module at a build of the old form)
// stays there, as the controller's composed writer folds it (issue 1126).
var inOwn []string
if c.env.Running != nil {
inOwn = frombus.InOwn(c.env.Running(), placement)
}
sort.Strings(behind)
for _, module := range behind {
digest, err := c.builtManifest(ctx, module, fps[module])