From 21d6583b5e23f5d9f3dffcde161c3d0dd0ed63bd Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 19:34:24 +0100 Subject: [PATCH] =?UTF-8?q?The=20node=20agent=20begins=20again=20composing?= =?UTF-8?q?=20its=20own=20when=20the=20catalogue=20places=20its=20node,=20?= =?UTF-8?q?with=20no=20restart=20(issue=201126,=20ADR=200549=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mesh-host/nodeagent.go | 30 ++++++++++++++++++++- cmd/mesh-host/nodeagent_integration_test.go | 17 +++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 689a6be..09aca00 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -496,12 +496,18 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, sess } // The node composes its own modules once the catalogue places it (novox/hq ADR 0547 §2, ADR 0549, issue 1126): its // placement key is read at the agent's start; until then it applies the controller's compositions as before. + run, stopRun := context.WithCancel(linked) if composer := selfComposer(linked, session, node, say); composer != nil { env.Composed, env.SelfComposed, env.Trusted, env.Policy = composer, true, nil, composer.Policy + } else { + // And once the catalogue places it, the agent begins again composing its own: the placement key appearing ends + // this run, so a node does not wait for a restart to follow the seed of its placement (ADR 0549 §4). + go endWhenPlaced(run, session, node, stopRun, say) } ag = agent.New(env) current.set(ag) - err = ag.Run(linked) + err = ag.Run(run) + stopRun() cancel() current.set(nil) if ctx.Err() != nil { @@ -818,3 +824,25 @@ func selfComposer(ctx context.Context, session *link.Session, node string, say f // And the vault's word that a secret changed, on which it composes again (ADR 0547 §2). Hear: session.Hear}) } + +// endWhenPlaced calls stop once this node's placement key appears in the indexer's placement (a value after the watch +// caught up, or one already there), so the agent begins again and composes its own. A watch that cannot be made is said; +// the node then switches at its next start. +func endWhenPlaced(ctx context.Context, session *link.Session, node string, stop func(), say link.Announce) { + key, err := delivery.PlacementKey(node) + if err != nil { + return + } + updates, err := bucketWatch{session, delivery.PlacementBucket}.Watch(ctx, key) + if err != nil { + say("whether the catalogue places this node is not followed (" + err.Error() + "): it composes its own from its next start") + return + } + for u := range updates { + if !u.Caught && !u.Deleted && len(u.Value) > 0 { + say("the catalogue places this node now (" + key + "): the node agent begins again, composing its own modules") + stop() + return + } + } +} diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index 4d299a8..ffc9813 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -313,8 +313,8 @@ func TestNatsTheHandPathAppliesACompositionTheOperatorSigned(t *testing.T) { }) } -// Issue 1126 (ADR 0547 §2, the milestone's H8): the node agent started whole against a real bus, on a node the catalogue -// places, composes a module from its want, the indexer's manifest, its record and its mesh-config value, and applies it, +// Issue 1126 (ADR 0547 §2, the milestone's H8): the node agent started whole against a real bus, once the catalogue +// places its node (the placement written after it started), composes a module from its want, the indexer's manifest, its record and its mesh-config value, and applies it, // with no key in the controller's compositions; a changed config value is composed again on its event and applied as a // revision of what runs. func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { @@ -345,7 +345,7 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { kv nats.KeyValue key string value []byte - }{{manifests, mkey, mraw}, {modules, "web", modv}, {placement, pkey, place}, {b.haves, hkey, rec}, + }{{manifests, mkey, mraw}, {modules, "web", modv}, {b.haves, hkey, rec}, {resolved, itNode + ".web.greeting", []byte(`{"value":"bonjour"}`)}} { if _, err := w.kv.Put(w.key, w.value); err != nil { t.Fatal(err) @@ -359,7 +359,16 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { stop := b.start(false) defer stop() - within(t, 3*time.Second, "the node did not compose and apply its own module", func() bool { return b.file("web") == "greeting=bonjour\n" }) + // Not placed yet: the agent applies the controller's compositions, of which there are none. The catalogue's seed then + // places the node, and the agent begins again composing its own (ADR 0549 §4), with no restart of the engine. + time.Sleep(300 * time.Millisecond) + if b.file("web") != "" { + t.Fatal("a node the catalogue does not place composed its own") + } + if _, err := placement.Put(pkey, place); err != nil { + t.Fatal(err) + } + within(t, 5*time.Second, "the node did not compose and apply its own module once placed", func() bool { return b.file("web") == "greeting=bonjour\n" }) within(t, 2*time.Second, "the have does not name the build", func() bool { h := b.have("web") return h.FP == fp && h.Composed != "" && h.Phase == delivery.PhaseRunning