Read this node's placement from its node and module keys, or its placement.<node> before the holder converts it, so a write of the holder's is composed only once its node key lands (issue 774, research 072 row 37)

This commit is contained in:
2026-10-31 09:37:23 +01:00
parent bb8383b065
commit a4c0cee251
8 changed files with 176 additions and 42 deletions
+37 -13
View File
@@ -1015,7 +1015,7 @@ type composerHooks struct {
}
func selfComposer(ctx context.Context, session *link.Session, node string, say func(string), hooks composerHooks) (*selfcompose.Composer, string) {
key, err := delivery.PlacementKey(node)
key, err := delivery.PlacementNodeKey(node)
if err != nil {
say("this node applies the controller's compositions: " + err.Error())
return nil, err.Error()
@@ -1079,31 +1079,55 @@ func composerEnv(session *link.Session, node string, say func(string), hooks com
VaultAuthority: hooks.vaultAuthority, Certify: hooks.certify}
}
// placementKeysOf is a node's node key and the pattern of the keys that say it is placed: its node key and its legacy
// key `placement.<node>`, read while the holder may not have converted it (research 072 row 37). Its module keys say
// nothing of whether it is placed.
func placementKeysOf(node string) (key, keys string, err error) {
key, err = delivery.PlacementNodeKey(node)
if err != nil {
return "", "", err
}
legacy, _ := delivery.LegacyPlacementKey(node)
return key, link.WatchPatterns(key, legacy), nil
}
// anyHeld says some key holds a value.
func anyHeld(held map[string]bool) bool {
for _, h := range held {
if h {
return true
}
}
return false
}
// endWhenUnplaced calls stop once this node's placement key is deleted from the seat's placement (after the watch
// caught up, or gone already), so the agent begins again and applies the controller's compositions, as a node the
// catalogue does not place does from its start: the way back (issue 1126, the seed's revert). A watch that cannot be
// made is said; the node then returns at its next start.
func endWhenUnplaced(ctx context.Context, session *link.Session, node string, stop func(), say link.Announce) {
key, err := delivery.PlacementKey(node)
key, keys, err := placementKeysOf(node)
if err != nil {
return
}
updates, err := bucketWatch{session, delivery.PlacementBucket}.Watch(ctx, key)
updates, err := bucketWatch{session, delivery.PlacementBucket}.Watch(ctx, keys)
if err != nil {
say("whether this node's key in the seat's placement state still exists is not followed (" + err.Error() + "): it returns to the controller's compositions at its next start")
return
}
placed := false
// Its node key or, before the holder converted it, its legacy key: placed while either holds a value (research
// 072 row 37); the holder deletes the legacy key before the node key, so the node key's delete is the last.
held, caught := map[string]bool{}, false
for u := range updates {
switch {
case u.Caught && placed:
continue
case u.Caught || u.Deleted || len(u.Value) == 0:
if u.Caught {
caught = true
} else {
held[u.Key] = !u.Deleted && len(u.Value) > 0
}
if caught && !anyHeld(held) {
say("this node's key in the seat's placement state is gone (" + key + "): the node agent begins again, applying the controller's compositions")
stop()
return
default:
placed = true
}
}
if ctx.Err() == nil {
@@ -1115,7 +1139,7 @@ func endWhenUnplaced(ctx context.Context, session *link.Session, node string, st
// 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, note func(string)) {
key, err := delivery.PlacementKey(node)
key, keys, err := placementKeysOf(node)
if err != nil {
return
}
@@ -1124,7 +1148,7 @@ func endWhenPlaced(ctx context.Context, session *link.Session, node string, stop
// (no timer), says so once in the journal and in the node's own have, where the watchman reads it, and stays on the
// controller's compositions until its key is written. A watch the account may not make is said the same way, and the
// node stays until its next start.
updates, err := bucketWatch{session, delivery.PlacementBucket}.WatchAwait(ctx, key, func(err error) {
updates, err := bucketWatch{session, delivery.PlacementBucket}.WatchAwait(ctx, keys, func(err error) {
why := "this node's key in the seat's placement state is waited for, the state is not on the bus yet (" + err.Error() + "): it stays on the controller's compositions and is watched when the bus says the state was created"
say(why)
note(why)
@@ -1137,7 +1161,7 @@ func endWhenPlaced(ctx context.Context, session *link.Session, node string, stop
}
for u := range updates {
if !u.Caught && !u.Deleted && len(u.Value) > 0 {
say("this node's key in the seat's placement state is written now (" + key + "): the node agent begins again, composing its own modules")
say("this node's key in the seat's placement state is written now (" + u.Key + "): the node agent begins again, composing its own modules")
stop()
return
}
+9 -3
View File
@@ -498,7 +498,7 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
strings.Contains(a.Refused, "web: the controller holds no composition")
})
ran := b.have("web").Composed
pkey, _ := delivery.PlacementKey(itNode)
pkey, _ := delivery.PlacementNodeKey(itNode)
// A pass in flight at the switch (issue 1126, 2026-10-28: the last pass of the own run, failing on its run's end,
// marked the next agent's modules failed): a config value changed starts one, and the key goes at once.
if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut","source":"module"}`)); err != nil {
@@ -587,7 +587,10 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode, Capabilities: []string{"service-manager"}}})
pkey, _ := delivery.PlacementKey(itNode)
// it1 in one key per module and node (research 072 row 37), it2 in the form before: a node reads both.
pkey, _ := delivery.PlacementNodeKey(itNode)
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
place = places[pkey]
hkey, _ := delivery.HaveNodeKey(itNode)
// A provider on it2 serving from its own config value: a mail domain, which it1's composition settles.
mailer := `{"module":"mailer","version":"1","config":{"domain":{"type":"string","description":"the mail domain","required":true}},` +
@@ -598,7 +601,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
mlmod, _ := delivery.EncodeModule(delivery.Module{FP2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)})
place2, _ := delivery.EncodePlacement(delivery.Placement{Node: "it2", Modules: []delivery.PlacedModule{{Module: "mailer"}}})
rec2, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}})
pkey2, _ := delivery.PlacementKey("it2")
pkey2, _ := delivery.LegacyPlacementKey("it2")
hkey2, _ := delivery.HaveNodeKey("it2")
buildKey, _ := delivery.BuilderBuildKey("web", fp)
done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest,
@@ -632,6 +635,9 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
if b.file("web") != "" {
t.Fatal("a node the catalogue does not place composed its own")
}
if _, err := placement.Put(itNode+".web", places[itNode+".web"]); err != nil {
t.Fatal(err)
}
if _, err := placement.Put(pkey, place); err != nil {
t.Fatal(err)
}
+33 -5
View File
@@ -5,6 +5,7 @@ import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -239,15 +240,19 @@ func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, s
// bus: then the kept declaration is read no more (keptRetired). A node the seat does not place, or whose placement or haves
// cannot be read, keeps reading it.
func placedAllHave(ctx context.Context, session *link.Session, node string) (bool, string) {
key, err := delivery.PlacementKey(node)
key, err := delivery.PlacementNodeKey(node)
if err != nil {
return false, err.Error()
}
h, err := session.KVHistory(ctx, delivery.PlacementBucket, key)
if err != nil || len(h) == 0 || h[len(h)-1].Deleted {
return false, "its placement is not read"
legacy, _ := delivery.LegacyPlacementKey(node)
// Its node key names every module placed (research 072 row 37); before the holder converted it, its legacy key does.
values := map[string][]byte{}
for _, k := range []string{key, legacy} {
if h, err := session.KVHistory(ctx, delivery.PlacementBucket, k); err == nil && len(h) > 0 && !h[len(h)-1].Deleted {
values[k] = h[len(h)-1].Value
}
}
p, err := delivery.DecodePlacement(h[len(h)-1].Value)
p, err := placedModules(node, values)
if err != nil {
return false, err.Error()
}
@@ -256,6 +261,29 @@ func placedAllHave(ctx context.Context, session *link.Session, node string) (boo
})
}
// placedModules is the modules node's node key names (a placement of their names alone), or its legacy key's placement
// where it has no node key; an error when neither reads.
func placedModules(node string, values map[string][]byte) (delivery.Placement, error) {
key, _ := delivery.PlacementNodeKey(node)
if raw, ok := values[key]; ok {
n, err := delivery.DecodePlacementNode(raw)
if err != nil {
return delivery.Placement{}, err
}
p := delivery.Placement{Node: node}
for _, r := range n.Modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: r.Module})
}
return p, nil
}
legacy, _ := delivery.LegacyPlacementKey(node)
raw, ok := values[legacy]
if !ok {
return delivery.Placement{}, errors.New("its placement is not read")
}
return delivery.DecodePlacement(raw)
}
// allHave says whether has finds, for every module p places, a have that names a fingerprint of the new form, and why
// 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