Node agent: follow the placement key at start only and say why when it cannot; test the reached candidate and the switched rule; word the log by the seat's state (issue 1126)

This commit is contained in:
2026-10-28 18:43:49 +01:00
parent 54aee345fc
commit ab22efd8b1
5 changed files with 89 additions and 55 deletions
+31 -47
View File
@@ -594,7 +594,9 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, sess
session.NoteComposes(linked, delivery.ComposesController, why)
// 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)
go endWhenPlaced(run, session, node, stopRun, say, func(reason string) {
session.NoteComposes(linked, delivery.ComposesController, why+"; "+reason)
})
}
ag = agent.New(env)
own.set(ag)
@@ -896,38 +898,29 @@ func selfComposer(ctx context.Context, session *link.Session, node string, say f
return nil, err.Error()
}
env := composerEnv(session, node, say, hooks)
placed, err := selfcompose.Placed(ctx, env.Get, node, link.Backoff, say, composedItsOwn(ctx, env.Get, node))
unreadable := ""
placed, err := selfcompose.Placed(ctx, env.Get, node, link.Backoff, func(m string) {
if strings.Contains(m, "cannot be read") {
unreadable = m
}
say(m)
}, selfcompose.ComposedItsOwn(ctx, env.Get, node))
if err != nil {
return nil, "its placement could not be read before the link ended: " + err.Error()
}
if !placed {
why := "the catalogue does not place it"
why := "the seat's placement state holds no key for it"
if unreadable != "" {
why = "its placement cannot be read, and it never composed its own: " + unreadable
}
say("this node applies the controller's compositions: " + why)
return nil, why
}
why := "the catalogue places it (" + key + ")"
why := "the seat's placement state holds its key (" + key + ")"
say("this node composes its own modules: " + why)
return selfcompose.New(env), why
}
// composedItsOwn says whether the node's own have, kept on the bus, says it composed from its placement key at its last
// start. A have that cannot be read says yes: the safe side, a node waits and does not fall back.
func composedItsOwn(ctx context.Context, get func(ctx context.Context, bucket, key string) ([]byte, error), node string) bool {
key, err := delivery.HaveNodeKey(node)
if err != nil {
return true
}
raw, err := get(ctx, delivery.HaveBucket, key)
if err != nil {
return true
}
if len(raw) == 0 {
return false
}
n, err := delivery.DecodeHaveNode(raw)
return err != nil || n.Composes == delivery.ComposesOwn
}
// composerEnv is what the node's composers read of the bus, the vault and the agent: its own composer and its answer
// to placement candidates (selfcompose.Shadow) alike.
func composerEnv(session *link.Session, node string, say func(string), hooks composerHooks) selfcompose.Env {
@@ -966,7 +959,7 @@ func endWhenUnplaced(ctx context.Context, session *link.Session, node string, st
}
updates, err := bucketWatch{session, delivery.PlacementBucket}.Watch(ctx, key)
if err != nil {
say("whether the catalogue still places this node is not followed (" + err.Error() + "): it returns to the controller's compositions at its next start")
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
@@ -975,7 +968,7 @@ func endWhenUnplaced(ctx context.Context, session *link.Session, node string, st
case u.Caught && placed:
continue
case u.Caught || u.Deleted || len(u.Value) == 0:
say("the catalogue no longer places this node (" + key + "): the node agent begins again, applying the controller's compositions")
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:
@@ -983,46 +976,37 @@ func endWhenUnplaced(ctx context.Context, session *link.Session, node string, st
}
}
if ctx.Err() == nil {
say("the watch of " + key + " ended while the link stays: whether the catalogue still places this node is followed again at the node agent's next start")
say("the watch of " + key + " ended while the link stays: whether this node's key in the seat's placement state still exists is followed again at the node agent's next start")
}
}
// endWhenPlaced calls stop once this node's placement key appears in the seat'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) {
func endWhenPlaced(ctx context.Context, session *link.Session, node string, stop func(), say link.Announce, note func(string)) {
key, err := delivery.PlacementKey(node)
if err != nil {
return
}
// The seat's placement state appears when its holder first writes it (ADR 0567): until then the watch is refused, as a
// read of it finds no bucket (Placed), and the node stays on the controller's compositions. The watch is tried again,
// so the node switches when its key is written, not only at its next start.
var updates <-chan agent.Update
said := ""
for try := 1; ; try++ {
updates, err = bucketWatch{session, delivery.PlacementBucket}.Watch(ctx, key)
if err == nil {
break
}
if why := err.Error(); why != said {
say("whether the catalogue places this node is not followed yet (" + why + "): the node stays on the controller's compositions and the watch is tried again")
said = why
}
select {
case <-ctx.Done():
return
case <-time.After(link.Backoff(try)):
}
// Tried at the start only: the delivery brings the grants part and the holder first, and the holder makes its states
// at its own start, so the state exists and the read is granted by the time the node-engine starts. A watch still
// refused is said once in the journal and in the node's own have, where the watchman reads it, and the node stays
// on the controller's compositions until its next start.
updates, err := bucketWatch{session, delivery.PlacementBucket}.Watch(ctx, key)
if err != nil {
why := "this node's key in the seat's placement state cannot be followed (" + err.Error() + "): it stays on the controller's compositions until its next start"
say(why)
note(why)
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")
say("this node's key in the seat's placement state is written now (" + key + "): the node agent begins again, composing its own modules")
stop()
return
}
}
if ctx.Err() == nil {
say("the watch of " + key + " ended while the link stays: whether the catalogue places this node is followed again at the node agent's next start")
say("the watch of " + key + " ended while the link stays: whether this node's key in the seat's placement state is written is followed again at the node agent's next start")
}
}
+1 -1
View File
@@ -529,7 +529,7 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
b.mu.Lock()
said := strings.Join(b.said, "\n")
b.mu.Unlock()
return strings.Contains(said, "the catalogue no longer places this node") && node().Composes == delivery.ComposesController
return strings.Contains(said, "key in the seat's placement state is gone") && node().Composes == delivery.ComposesController
})
time.Sleep(3 * time.Second) // what a pass in flight would mark lands within it
select {