The way back: a node whose placement key is deleted returns to the controller's compositions without a restart, says so in its journal and its have, and answers such a candidate as returning; pin mesh-sdk 472d5f3 (issue 1126, the seed's revert)
The seed's revert (mesh-catalog #526) was refused on 'would stop running node', while a self-composing node would have refused its placement's absence, kept what runs and marked every have failed until a restart. endWhenUnplaced ends the agent's run when the key goes, so it begins again on the controller's compositions, as at a start without a key; the composer refuses nothing while unplaced; the node's have says whose compositions it applies (Composes); the shadow answers a deleted placement Returns, verdict same.
This commit is contained in:
@@ -52,6 +52,9 @@ type haveState struct {
|
||||
nodeSealed []string
|
||||
// nodeCandidates is the node's answer to each placement candidate group (novox/hq ADR 0559 §4), written into its key.
|
||||
nodeCandidates map[string]delivery.CandidateAnswer
|
||||
// nodeComposes and nodeComposesWhy are whose compositions the node applies and why (HaveNode.Composes), written into
|
||||
// its key.
|
||||
nodeComposes, nodeComposesWhy string
|
||||
// written is the build of each module this node-engine wrote a have key for and that the declaration it applied
|
||||
// still holds; removed is each such module the latest declaration no longer holds, with the build its key last
|
||||
// said, until the key is written once with the phase removed (issue 1369, novox/hq ADR 0408 §4: never deleted).
|
||||
@@ -423,6 +426,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
|
||||
facts.Composed = s.nodeComposed
|
||||
facts.Sealed = s.nodeSealed
|
||||
facts.Candidates = s.nodeCandidates
|
||||
facts.Composes, facts.ComposesWhy = s.nodeComposes, s.nodeComposesWhy
|
||||
if raw, err = delivery.EncodeHaveNode(facts); err == nil {
|
||||
want := facts
|
||||
if !q.putHave(write, bucket, key, raw, func(current []byte) bool {
|
||||
@@ -752,6 +756,26 @@ func (q *Session) NoteCandidates(ctx context.Context, answers map[string]deliver
|
||||
q.writeHaves(ctx, bus)
|
||||
}
|
||||
|
||||
// NoteComposes says, in the node's own key, whose compositions the node applies (delivery.ComposesOwn or
|
||||
// delivery.ComposesController) and why: written at each start of its agent, so a node returning to the controller's
|
||||
// compositions says so where every have is read (issue 1126).
|
||||
func (q *Session) NoteComposes(ctx context.Context, composes, why string) {
|
||||
q.init()
|
||||
s := &q.haves
|
||||
s.mu.Lock()
|
||||
changed := s.nodeComposes != composes || s.nodeComposesWhy != why
|
||||
s.nodeComposes, s.nodeComposesWhy = composes, why
|
||||
known := s.node != nil
|
||||
s.mu.Unlock()
|
||||
if !known || !changed {
|
||||
return // said with the node's first facts
|
||||
}
|
||||
q.mu.Lock()
|
||||
bus := q.bus
|
||||
q.mu.Unlock()
|
||||
q.writeHaves(ctx, bus)
|
||||
}
|
||||
|
||||
// ReadNodeHave is the node's own key as the bucket holds it, and whether it holds one: what the node's own composition
|
||||
// it last ran is and carries sealed, read after a restart (ADR 0547 §3).
|
||||
func (q *Session) ReadNodeHave(ctx context.Context) (delivery.HaveNode, bool) {
|
||||
|
||||
@@ -429,9 +429,16 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
_, nodeWanted := c.wants[agent.NodeModule]
|
||||
// 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])
|
||||
own, placed := c.placements[c.env.Node]
|
||||
inOwn := c.inOwnOf(own)
|
||||
r := reader{c: c}
|
||||
c.mu.Unlock()
|
||||
if !placed {
|
||||
// The catalogue no longer places it: the agent returns to the controller's compositions (endWhenUnplaced), and
|
||||
// this is no refusal of what it runs (issue 1126, the seed's revert).
|
||||
c.sayOnce("the catalogue no longer places this node: it composes nothing of its own, and keeps running what it runs")
|
||||
return nil
|
||||
}
|
||||
// Composed from the build its want names (frombus.Scope.KeptAt, issue 1126).
|
||||
for _, module := range behind {
|
||||
kept[module] = fps[module]
|
||||
|
||||
@@ -477,6 +477,15 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
candidate("adds", 4, "lemurs", "keeper")
|
||||
candidate("drops", 5)
|
||||
candidate("ghost", 6, "lemurs", "ghost")
|
||||
// A head deleting n1's placement file (the seed's revert, mesh-catalog #526): n1 returns to the controller's
|
||||
// compositions, answered as such, not as every module removed.
|
||||
if raw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Group: "deletes",
|
||||
Pull: delivery.Head{Repository: delivery.CatalogueRepository, Number: 1, Head: "h-deletes"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
k, _ := delivery.PlacementCandidateKey("deletes", "n1")
|
||||
sb.put(delivery.PlacementCandidatesBucket, k, raw, 7)
|
||||
}
|
||||
sb.caught()
|
||||
var answers map[string]delivery.CandidateAnswer
|
||||
waitFor := func(why string, ok func(map[string]delivery.CandidateAnswer) bool) {
|
||||
@@ -490,7 +499,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
waitFor("every group answered", func(a map[string]delivery.CandidateAnswer) bool { return len(a) == 4 })
|
||||
waitFor("every group answered", func(a map[string]delivery.CandidateAnswer) bool { return len(a) == 5 })
|
||||
// Every watch and get the shadow makes is one the node's grant allows (issue 1126, 2026-10-27 09:21).
|
||||
sb.mu.Lock()
|
||||
for _, w := range sb.watches {
|
||||
@@ -520,6 +529,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
"ghost": func(a delivery.CandidateAnswer) bool {
|
||||
return a.Verdict == delivery.CandidateRefused && a.Refused != ""
|
||||
},
|
||||
"deletes": func(a delivery.CandidateAnswer) bool {
|
||||
return a.Verdict == delivery.CandidateSame && a.Returns == delivery.ReturnsToController && a.Revision == 7 &&
|
||||
len(a.Removed) == 0
|
||||
},
|
||||
} {
|
||||
if !want(answers[group]) {
|
||||
t.Errorf("%s: %+v", group, answers[group])
|
||||
@@ -534,7 +547,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
}
|
||||
k, _ := delivery.PlacementCandidateKey("same", "n1")
|
||||
sb.del(delivery.PlacementCandidatesBucket, k)
|
||||
waitFor("the deleted group's answer stays", func(a map[string]delivery.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 3 })
|
||||
waitFor("the deleted group's answer stays", func(a map[string]delivery.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 4 })
|
||||
|
||||
// Review of L2b, round 2: a watch ends while the link stays; a group's key is deleted before the shadow watches
|
||||
// again. The new run reads afresh, so that group is no longer answered.
|
||||
@@ -546,7 +559,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
sb.missing(delivery.PlacementCandidatesBucket, false)
|
||||
waitFor("a group deleted while the shadow was down is still answered", func(a map[string]delivery.CandidateAnswer) bool {
|
||||
_, ok := a["drops"]
|
||||
return !ok && len(a) == 2
|
||||
return !ok && len(a) == 3
|
||||
})
|
||||
|
||||
// Issue 1126, the seed's review: a module n1 runs inside its own composition (nats, at a build of the old form) is
|
||||
|
||||
@@ -292,18 +292,11 @@ func (s *Shadow) answerGroup(ctx context.Context, group string) (delivery.Candid
|
||||
running = c.env.Running()
|
||||
}
|
||||
if p, ok := given[node]; ok && p == nil {
|
||||
// The head removes this node's placement file: everything it runs is removed.
|
||||
a := delivery.CandidateAnswer{Head: head, Revision: rev}
|
||||
for _, r := range running {
|
||||
if r.Module == "" {
|
||||
a.Removed = append(a.Removed, delivery.HaveNodeWord)
|
||||
} else {
|
||||
a.Removed = append(a.Removed, r.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(a.Removed)
|
||||
a.Verdict = delivery.VerdictOf(a)
|
||||
return a, true
|
||||
// The head deletes this node's placement file: the node then returns to the controller's compositions, as one the
|
||||
// catalogue does not place (endWhenUnplaced), and keeps what runs; nothing is removed (issue 1126, the seed's
|
||||
// revert, whose check read "would stop running node").
|
||||
return delivery.CandidateAnswer{Head: head, Revision: rev, Returns: delivery.ReturnsToController,
|
||||
Verdict: delivery.CandidateSame}, true
|
||||
}
|
||||
|
||||
placement := given[node]
|
||||
|
||||
Reference in New Issue
Block a user