Node-engine: a held own composition that was handed stays handed, as a held module's does (ADR 0636 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -1029,8 +1029,11 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
out = append(out, agent.Update{Key: key, Value: raw, Revision: c.rev})
|
||||
}
|
||||
for key := range c.handed {
|
||||
if _, module, _, _, err := engcomp.ParseKey(key); err == nil && module != "" {
|
||||
if _, isHeld := held[module]; isHeld {
|
||||
if _, module, _, isNode, err := engcomp.ParseKey(key); err == nil {
|
||||
if isNode {
|
||||
module = agent.NodeModule // the node's own composition is held under its scope
|
||||
}
|
||||
if _, isHeld := held[module]; isHeld && module != "" {
|
||||
continue // held: what was handed of it stays, so the node keeps running it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/novox/mesh-host/internal/buckets"
|
||||
"github.com/novox/mesh-host/internal/buckets/bucketstest"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"maps"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
@@ -1851,6 +1852,66 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHeldOwnCompositionThatWasHandedIsNotDropped(t *testing.T) {
|
||||
b := newFakeBus()
|
||||
fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64)
|
||||
digest := meshcatalogclient.ManifestDigest([]byte(lemurs))
|
||||
man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: lemurs})
|
||||
key, _ := meshcatalogclient.ManifestsKey(digest)
|
||||
b.kv[buckets.Manifests+"/"+key] = man
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
env := b.env()
|
||||
g := &gate{}
|
||||
changed := make(chan struct{}, 1)
|
||||
env.SeatList, env.SeatListChanged = g, changed
|
||||
passes := make(chan map[string]string, 8)
|
||||
env.Refused = func(_ string, held map[string]string) { passes <- maps.Clone(held) }
|
||||
c := New(env)
|
||||
out, err := c.Watch(ctx, "n1.>")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
// The list is read: the node's own composition is handed. Then the reader cannot act again (it restarted and has
|
||||
// not read): the composition is held, and what was handed of it stays, as a held module's does.
|
||||
g.open.Store(true)
|
||||
changed <- struct{}{}
|
||||
for u := next(t, out); !(u.Key == "n1.node" && !u.Deleted); u = next(t, out) {
|
||||
}
|
||||
g.open.Store(false)
|
||||
changed <- struct{}{}
|
||||
for h := range passes {
|
||||
if h[agent.NodeModule] == WaitsForTheSeatListOwn {
|
||||
break
|
||||
}
|
||||
}
|
||||
deadline := time.After(100 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case u := <-out:
|
||||
if u.Key == "n1.node" && u.Deleted {
|
||||
t.Fatalf("the held own composition that was handed is deleted, and the node drops what it runs")
|
||||
}
|
||||
case <-deadline:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0636 §1, the cycle of the node's own composition: while the list is unread the node's own composition is held, but
|
||||
// a node whose new own composition must start the holder of the seat mesh-catalog composes of it, whatever the list says,
|
||||
// the holder's resources and its account and nothing else, over what the node's own composition ran before; and the rest
|
||||
|
||||
Reference in New Issue
Block a user