The node-engine says why it composes a wanted module at no composition, and hands again one its agent lacks (issue 1966, recovery) #216

Merged
mesh-tools merged 4 commits from fix/1966-views-page-time into main 2026-11-09 08:13:15 +00:00
7 changed files with 347 additions and 8 deletions
+1
View File
@@ -659,6 +659,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
machinesNow.Store(composer) // the machines a failing network part points at, by the assignments (issue 774)
composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone
env.Composed, env.SelfComposed, env.Policy, env.ComposedToday = composer, true, composer.Policy, composer.ComposedToday
env.WhyUnmade, env.Waiting = composer.WhyUnmade, composer.Waiting
env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does
session.NoteComposes(linked, nodeengineclient.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere")
ag = agent.New(env)
+41 -1
View File
@@ -125,6 +125,11 @@ type Env struct {
Wants Watcher
Composed Watcher
Haves Haves
// WhyUnmade is why the node's composer composed nothing of a module at its want's fingerprint, from its last pass; empty
// when it says nothing. Nil says nothing. Waiting tells the composer the agent waits for the composition of a module at
// a fingerprint it holds none of, so a composer that believes it handed it hands it again. Nil tells nobody.
WhyUnmade func(module string) string
Waiting func(module, fp string)
// Apply applies one module's composition (scope NodeModule for the node's own), given the compositions the node's
// other modules run so a path, unit or group they hold is not removed or taken. It is called with one module at a
// time.
@@ -241,6 +246,8 @@ type Agent struct {
// prior is a module the node ran before this agent started, so it has something to back up.
prior map[string]bool
said map[string]string
// waited is, by module, the want (fingerprint and revision) the agent said it waits for the composer for.
waited map[string]string
// notComposed is the modules whose have says the node could not compose them: why, and the have before (NotComposed).
notComposed map[string]notComposedMark
// heldAlone is the modules the composer holds alone (NotComposedEach): what runs of each in the node's own
@@ -306,7 +313,7 @@ func New(env Env) *Agent {
env.Say = func(string) {}
}
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]nodeengine.HaveByNodeModuleValue{}, applied: map[string]compNow{}, had: map[string]compNow{}, keptData: map[string]bool{},
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, notComposed: map[string]notComposedMark{},
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, waited: map[string]string{}, notComposed: map[string]notComposedMark{},
pulls: map[string]*pullWait{}, settledAt: map[string]time.Time{}, settledSeq: map[string]int{},
retry: make(chan []string, 8), reconcile: make(chan []string, 64), failedAgain: make(chan string, 8), tries: map[string]int{}, again: make(chan againAsk, 64),
transient: make(chan struct{}, 1), stopped: make(chan struct{}), quiet: make(chan chan struct{})}
@@ -1319,6 +1326,31 @@ func (a *Agent) HoldsNode() bool {
return ok && h.Phase != nodeengineclient.PhaseRemoved
}
// waitsForComposer says once per want and reason that the agent waits for the node's own composition of module at fp, and
// why the composer made none (Env.WhyUnmade), and tells the composer (Env.Waiting) so it hands again what it believes it
// handed. Called without a.mu held.
func (a *Agent) waitsForComposer(module, fp string, rev uint64) {
why := "the composer has made none yet"
if a.env.WhyUnmade != nil {
if w := a.env.WhyUnmade(module); w != "" {
why = w
}
}
line := fmt.Sprintf("%s waits for the composer: no composition of it at %s: %s", module, fp, why)
// Said and told once per want (its fingerprint and revision), whatever the reason says; forgotten once composed.
at := fmt.Sprintf("%s@%d", fp, rev)
a.mu.Lock()
first := a.waited[module] != at
if first {
a.waited[module] = at
a.env.Say(line)
}
a.mu.Unlock()
if first && a.env.Waiting != nil {
a.env.Waiting(module, fp)
}
}
func (a *Agent) settleOne(ctx context.Context, module string) {
a.mu.Lock()
w := a.wants[module]
@@ -1334,7 +1366,15 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
comp, have := a.compositionFor(module, w.Want.Fp)
a.mu.Unlock()
if a.env.SelfComposed {
if have {
a.mu.Lock()
delete(a.waited, module)
a.mu.Unlock()
}
if !have || comp.Err != nil {
if !have {
a.waitsForComposer(module, w.Want.Fp, w.Rev)
}
return // waits for the node's own composition of the want's build
}
// The composition the node composed for the want's build is the one it applies (ADR 0547 §2).
+40
View File
@@ -1556,6 +1556,11 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
history[key] = append(history[key], Update{Key: key, Value: raw, Revision: uint64(len(history[key]) + 1)})
}
b.env.Held = func() map[string]string { return map[string]string{"mesh-build-go": "its binding is unset"} }
// Issue 1966 (novox, 2026-11-09): a want the composer made no composition of is said once, with why, and the composer told.
var toldMu sync.Mutex
var told []string
b.env.WhyUnmade = func(string) string { return "no manifest in the composer's picture" }
b.env.Waiting = func(module, fp string) { toldMu.Lock(); told = append(told, module+"@"+fp); toldMu.Unlock() }
stop := b.run()
defer stop()
unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) {
@@ -1583,6 +1588,41 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
b.comps.put(ck, craw)
want("web", fp, "")
eventually(t, "the node's own composition was not applied", func() bool { return len(b.appliedList()) == 1 })
gk := meshwants.WantsByWantNodeModuleKey(node, "ghost")
graw, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fpOf("9"), Self: true})
b.wants.put(gk, graw)
line := "ghost waits for the composer: no composition of it at " + fpOf("9") + ": no manifest in the composer's picture"
saidCount := func() (n int) {
b.mu.Lock()
defer b.mu.Unlock()
for _, l := range b.said {
if l == line {
n++
}
}
return n
}
eventually(t, "the wait for the composer was not said", func() bool { return saidCount() == 1 })
// Settled again by another want's change, it is neither said nor told again.
other, oraw, _ := unsigned("ghost", fpOf("8"), file("ghost", "x", "1"))
b.comps.put(other, oraw)
eventually(t, "ghost's other composition was not taken", func() bool {
b.ag.mu.Lock()
defer b.ag.mu.Unlock()
_, ok := b.ag.comps[other]
return ok
})
// What is settled after it: web's want moves on, so ghost's settle, queued before it, has run.
want("web", fp, "")
eventually(t, "the composer was not told once", func() bool { toldMu.Lock(); defer toldMu.Unlock(); return slices.Contains(told, "ghost@"+fpOf("9")) })
if _, err := b.ag.Composition(context.Background(), "ghost", "", false); err == nil || !strings.Contains(err.Error(), "waits for the composer: no manifest in the composer's picture") {
t.Errorf("the composition read of ghost says %v, want why the composer made none", err)
}
toldMu.Lock()
if n := slices.Index(told, "ghost@"+fpOf("9")); saidCount() != 1 || n < 0 || slices.Contains(told[n+1:], "ghost@"+fpOf("9")) {
t.Errorf("said %d times, told %q; want once and ghost told once", saidCount(), told)
}
toldMu.Unlock()
eventually(t, "the have does not name the composed digest", func() bool { return b.haves.last("web").Composed == dig })
// ADR 0601 §4: what the composer keeps of web as it runs is carried on its have, and goes once it keeps none.
b.ag.KeptNow(context.Background(), map[string][]string{"web": {"web runs the tool runner's process here"}})
+5
View File
@@ -209,6 +209,11 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
case digest != "" && read == nil:
return view, fmt.Errorf("no revision of %s the bucket keeps has the digest %s; revisions lists those it keeps", key, digest)
case read == nil && !haveLatest:
if a.env.SelfComposed && a.env.WhyUnmade != nil && module != NodeModule {
if why := a.env.WhyUnmade(module); why != "" {
return view, fmt.Errorf("the bucket holds no composition of %s at %s; the agent waits for the composer: %s", module, fp, why)
}
}
return view, fmt.Errorf("the bucket holds no composition of %s at %s", module, fp)
case read == nil:
read = &latest
+14
View File
@@ -167,6 +167,10 @@ type Scope struct {
// Kept is told, in words, each module composed as what runs shows because its manifest does not yet say it (the tool
// runner, the core's credentials; ADR 0601 §4), so the node says it. Nil: said nowhere.
Kept func(module, words string)
// Unmade is told, in words, each module placed on the node that is composed from the indexer's manifest and not from a
// build, with why: the indexer's modules hold no build of the new form for it, or the builder holds none done at that
// fingerprint. Nil: said nowhere.
Unmade func(module, why string)
// takeInHeld, set for the input a node answers a check with (ComposingInput), takes in the build of each module placed
// on the node that its manifest's verdict or its decoder holds, into compose.Input.Holds, never into what the node composes: the
// check's composer, judging the change's manifest itself, composes it at that build (issue 1921).
@@ -930,6 +934,9 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
}
for _, pm := range p.Modules {
m, ok := modules[pm.Module]
if (!ok || m.Fp2 == "") && s.Unmade != nil && wanted[pm.Module] != "" {
s.Unmade(pm.Module, "the indexer's modules hold no build of the new form for it (fingerprint "+m.Fp2+"), so it is composed from the manifest")
}
if !ok || m.Fp2 == "" || keptAt[pm.Module] != "" || compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) {
// A module it runs inside its own composition is composed there as before while it is folded: no build of the
// new form is what it runs (Scope.InOwn). Wanted at a build of the new form it is no longer folded (compose.Folds,
@@ -975,6 +982,13 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m
stub(pm.Module, fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build yet; it "+
"keeps running what it runs", pm.Module, m.Fp2))
}
if s.Unmade != nil && wanted[pm.Module] != "" {
state := "none"
if b != nil {
state = string(b.State)
}
s.Unmade(pm.Module, "the builder holds no done build of it at "+m.Fp2+" (build: "+state+"), so it is composed from the indexer's manifest, which names no artifacts")
}
continue // nothing of it runs: composed from the indexer's manifest, as before
}
resolved, err := built(pm.Module, m.Fp2, b)
+180 -7
View File
@@ -154,6 +154,14 @@ type Composer struct {
// the data items after it (publishData) do not read each placed module's build again over the bus (ADR 0577).
builds map[string]builds.BuilderBuild
handed map[string]string // composed key to the digest handed
// notTaken is, by module, why a want of this node's was not taken (it could not be decoded, or names no fingerprint).
notTaken map[string]string
// unmadeSaid is, by module, the stable part of the reason its last journal line said (without where the picture stands).
unmadeSaid map[string]string
// modulesRev is the revision of the last update of the indexer's modules taken: where the picture of them stands.
modulesRev uint64
// rehand asks for a pass that hands again what the agent waits for (Waiting).
rehand chan struct{}
// sealedOwn is each own secret's sealed value as first handed, by module/name and generation: a value sealed again
// is other bytes, so the one first had is kept while the generation stands, and the composition stays the same.
sealedOwn map[string]ownSealed
@@ -238,6 +246,11 @@ type Pass struct {
// holds is what holds each module Held names by its manifest's verdict or its decoder, with the build it would be
// composed at (frombus.Scope.Holds), for LastHeld's words.
holds map[string]compose.Hold
// Unmade is each module wanted that the pass composed no composition of at its want's fp, with why: the want not
// taken, the module not assigned here, no manifest in the composer's picture of the indexer's modules (and where that
// picture stands), or what the composition left it out for. A module held alone is in Held, not here. Said once per
// change as a journal line, so a want that waits for the composer says what for.
Unmade map[string]string `json:"unmade,omitempty"`
// Handed is how many compositions the pass handed the agent that differ from what it handed before.
Handed int `json:"handed"`
// Again is when the node composes again on its own, after a seat or a read that did not answer; zero when it does
@@ -340,7 +353,7 @@ func New(env Env) *Composer {
}
return &Composer{env: env, wants: map[string]meshwants.WantsByWantNodeModuleValue{}, assignments: map[string]meshmoduleassigner.Assignments{}, assignmentKeys: meshmoduleassignerclient.NewKeys(),
records: map[string]nodeengine.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]meshcatalog.ModulesByModuleValue{}, values: map[string]map[string]any{},
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]builds.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{},
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]builds.BuilderBuild{}, handed: map[string]string{}, notTaken: map[string]string{}, unmadeSaid: map[string]string{}, rehand: make(chan struct{}, 1), sealedOwn: map[string]ownSealed{},
pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)}
}
@@ -544,6 +557,8 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er
again = nil
dirty = true
c.env.Say(c.againLine())
case <-c.rehand:
dirty = true
case reply := <-c.asked:
// Asked by hand (the verb compose): composed now; the growing bound is not restarted, so a pass that
// fails again waits longer, not less.
@@ -699,6 +714,13 @@ func (c *Composer) entry(bucket, key string) (v any, has, known bool) {
}
func (c *Composer) takeWant(u agent.Update) {
if line := c.takeWantLocked(u); line != "" {
c.sayOnceEach(line)
}
}
// takeWantLocked takes the want in and answers a line to say when it is a want of this node's that is not taken.
func (c *Composer) takeWantLocked(u agent.Update) string {
c.mu.Lock()
defer c.mu.Unlock()
if _, ok := meshwants.ParseWantsByWantNodeNodeKey(u.Key); ok {
@@ -707,18 +729,30 @@ func (c *Composer) takeWant(u agent.Update) {
} else {
c.wants[agent.NodeModule] = meshwants.WantsByWantNodeModuleValue{}
}
return
return ""
}
node, module, ok := meshwants.ParseWantsByWantNodeModuleKey(u.Key)
if !ok || node != c.env.Node {
return
return ""
}
delete(c.notTaken, module)
w, err := meshwants.DecodeWantsByWantNodeModuleValue(u.Value)
if u.Deleted || err != nil || w.Removed || w.Fp == "" {
delete(c.wants, module)
return
switch {
case u.Deleted || (err == nil && w.Removed):
case err != nil:
c.notTaken[module] = "its want cannot be decoded: " + err.Error()
default:
c.notTaken[module] = "its want names no fingerprint"
}
if why := c.notTaken[module]; why != "" {
return "the node composes nothing of " + module + ": " + why
}
return ""
}
c.wants[module] = w
return ""
}
func (c *Composer) takeAssignments(u agent.Update) {
@@ -844,6 +878,7 @@ func (c *Composer) takeModule(u agent.Update) {
return
}
c.modules[name] = m
c.modulesRev = u.Revision
}
func (c *Composer) takeValue(u agent.Update) {
@@ -946,6 +981,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
keptRuns := map[string][]string{}
var keptMu sync.Mutex
var holds map[string]compose.Hold
fromManifest := map[string]string{} // by module, why it is composed from the indexer's manifest and not a build
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Kept: func(module, words string) {
keptMu.Lock()
defer keptMu.Unlock()
@@ -953,12 +989,16 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
keptRuns[module] = append(keptRuns[module], words)
}
c.sayOnceEach(words)
}, Holds: func(h map[string]compose.Hold) { holds = h }}
}, Holds: func(h map[string]compose.Hold) { holds = h }, Unmade: func(module, why string) {
keptMu.Lock()
defer keptMu.Unlock()
fromManifest[module] = why
}}
c.mu.Lock()
c.dataScope = frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn} // the data items are worked out as composed
c.mu.Unlock()
missed := c.missed.Load()
compositions, _, held, err := frombus.Compose(ctx, r, c.env.Mesh, scope, fps)
compositions, skipped, held, err := frombus.Compose(ctx, r, c.env.Mesh, scope, fps)
err = c.missedIn(missed, err)
// A bus that does not answer for a moment (a reload of its accounts pauses it a few seconds) is no refusal: the pass is
// tried again after a growing wait before anything is marked (issue 1126).
@@ -970,7 +1010,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
if ctx.Err() == nil {
missed = c.missed.Load()
compositions, _, held, err = frombus.Compose(ctx, r, c.env.Mesh, scope, fps)
compositions, skipped, held, err = frombus.Compose(ctx, r, c.env.Mesh, scope, fps)
err = c.missedIn(missed, err)
}
}
@@ -1013,6 +1053,9 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
c.paid()
pass.Held, pass.holds = held, holds
var unmadeKeys map[string]string
pass.Unmade, unmadeKeys = c.unmadeOf(fps, compositions, held, skipped, own, inOwn, fromManifest, &keptMu)
c.sayUnmade(pass.Unmade, unmadeKeys)
// The provisioner client's account the node runs with, dropped by this pass, is said: every filled write is refused
// under a declaration that names none (filling.JudgeAccount), and no module is handed to it (issue 774, ace,
// 2026-11-04 01:29). A placed client keeps its account while it is held (compose.Node.RunningProvisioner).
@@ -2028,3 +2071,133 @@ func (c *Composer) seatsOf(ctx context.Context, r reader, module string) []strin
}
return seats
}
// unmadeOf is each module wanted at a fingerprint (fps) of which the pass made no composition at it, with why, apart from
// the modules held alone (Pass.Unmade), and each want of this node's that was not taken.
func (c *Composer) unmadeOf(fps map[string]string, comps map[string]engcomp.Composed, held map[string]string, skipped []string,
own meshmoduleassigner.Assignments, inOwn []string, fromManifest map[string]string, mu *sync.Mutex) (map[string]string, map[string]string) {
made := map[string]string{}
for _, comp := range comps {
if comp.Module != "" {
made[comp.Module] = comp.FP
}
}
placed := map[string]bool{}
for _, m := range own.Modules {
placed[m.Module] = true
}
mu.Lock()
defer mu.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
out, keys := map[string]string{}, map[string]string{}
for module, fp := range fps {
if _, isHeld := held[module]; isHeld {
continue
}
if made[module] == fp {
// Composed, but from the indexer's manifest and not a build: said, since what it names no artifacts of is no module
// that runs.
if w, ok := fromManifest[module]; ok {
out[module], keys[module] = w, fp+" "+w
}
continue
}
var why, key string
switch m, in := c.modules[module]; {
case compose.Folds(slices.Contains(inOwn, module), fp):
why = "it is composed inside the node's own composition at " + fp
key = why
case !placed[module]:
why = fmt.Sprintf("the seat's assignment places no %s on this node (%d modules placed)", module, len(own.Modules))
key = "not placed"
case !in:
why = fmt.Sprintf("no manifest in the composer's picture of the indexer's modules (%d modules, last update at revision %d)",
len(c.modules), c.modulesRev)
key = "no manifest"
case slices.Contains(skipped, module):
why = "the composition skipped it: the declaration gives it no fingerprint (indexer's picture: " + m.Fp2 + ")"
key = why
default:
if at := made[module]; at != "" {
why = fmt.Sprintf("the pass composed it at %s and the want names %s (indexer's picture: %s, revision %d)", at, fp, m.Fp2, c.modulesRev)
key = "composed at " + at
} else {
why = fmt.Sprintf("the composition made nothing of it at %s (indexer's picture: %s, revision %d)", fp, m.Fp2, c.modulesRev)
key = "made nothing at " + m.Fp2
}
}
if w, ok := fromManifest[module]; ok {
why += "; " + w
key += "; " + w
}
out[module], keys[module] = why, fp+" "+key
}
for module, why := range c.notTaken {
if _, wanted := fps[module]; !wanted {
out[module] = "want not taken: " + why
keys[module] = out[module]
}
}
if len(out) == 0 {
return nil, keys
}
return out, keys
}
// sayUnmade says what the pass composed none of when a module's reason changed (keys: its reason without where the
// picture of the indexer's modules stands, so a stuck want is not said again at every update of any module), and forgets a
// module once it is composed.
func (c *Composer) sayUnmade(unmade, keys map[string]string) {
c.mu.Lock()
changed := false
for m := range c.unmadeSaid {
if _, still := unmade[m]; !still {
delete(c.unmadeSaid, m)
}
}
for m, k := range keys {
if c.unmadeSaid[m] != k {
c.unmadeSaid[m], changed = k, true
}
}
c.mu.Unlock()
if !changed {
return
}
var lines []string
for _, m := range heldModules(unmade) {
lines = append(lines, m+": "+unmade[m])
}
c.env.Say("the node composes nothing of " + strings.Join(lines, "; "))
}
// Waiting is the agent's word that it waits for the composition of module at fp, which it holds none of. When the composer
// believes it handed that composition, it forgets so and composes again, so the agent is handed it again: what was handed
// is no more trusted over what the agent holds (issue 1966, novox: handed zero, the agent waiting).
func (c *Composer) Waiting(module, fp string) {
key, err := engcomp.Key(c.env.Node, module, fp)
if err != nil {
return
}
c.mu.Lock()
_, believed := c.handed[key]
delete(c.handed, key)
c.mu.Unlock()
if !believed {
return
}
c.env.Say(fmt.Sprintf("the agent waits for the composition of %s at %s, which the node believed it handed: handed again", module, fp))
select {
case c.rehand <- struct{}{}:
default:
}
}
// WhyUnmade is why the last pass composed nothing of module at its want's fingerprint (Pass.Unmade); empty when it said
// none, or no pass was made.
func (c *Composer) WhyUnmade(module string) string {
c.mu.Lock()
defer c.mu.Unlock()
return c.last.Unmade[module]
}
+66
View File
@@ -280,6 +280,72 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.mu.Unlock()
t.Fatalf("handed %v, want %s and %s", got, lemursKey, nodeKey)
}
// Issue 1966 (novox, 2026-11-09): every pass names each wanted module it composed nothing of, or composed from the indexer's
// manifest for want of a done build, with why, and a want it did not take; said once; the agent's word that it waits
// hands a composition the node believes handed again.
tfp := meshcatalogclient.SourcePrefix2 + strings.Repeat("7", 64)
tool := `{"module":"tool","version":"1","capabilities":["systemd"],"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
`"resources":[{"id":"service","type":"service","unit":"tool.service","state":"running","boot":"enabled"}]}`
tdigest := meshcatalogclient.ManifestDigest([]byte(tool))
tman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "tool", Manifest: tool})
tkey, _ := meshcatalogclient.ManifestsKey(tdigest)
b.mu.Lock()
b.kv[buckets.Manifests+"/"+tkey] = tman
b.mu.Unlock()
tmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: tfp, Manifest: tdigest, Repository: "novox/t", Commit: "c"})
c.takeModule(agent.Update{Key: "tool", Value: tmod, Revision: 9})
for module, w := range map[string]meshwants.WantsByWantNodeModuleValue{"tool": {Fp: tfp, Self: true}, "ghost": {Fp: fp, Self: true}, "nofp": {Self: true}} {
raw, _ := bucketstest.Want(w)
c.takeWant(agent.Update{Key: "want.n1." + module, Value: raw})
}
c.mu.Lock()
placed := c.assignments["n1"]
placed.Modules = append(slices.Clone(placed.Modules), meshmoduleassigner.PlacedModule{Module: "tool"})
c.assignments["n1"] = placed
c.mu.Unlock()
said := func(sub string) (n int) {
b.mu.Lock()
defer b.mu.Unlock()
for _, l := range b.said {
if strings.Contains(l, sub) {
n++
}
}
return n
}
c.compose(ctx)
why := map[string]string{"tool": c.WhyUnmade("tool"), "ghost": c.WhyUnmade("ghost"), "nofp": c.WhyUnmade("nofp"), "lemurs": c.WhyUnmade("lemurs")}
if !strings.Contains(why["tool"], "the builder holds no done build of it at "+tfp) || !strings.Contains(why["ghost"], "places no ghost") ||
!strings.Contains(why["nofp"], "want not taken: its want names no fingerprint") || why["lemurs"] != "" {
t.Errorf("the pass says it composed nothing of %q", why)
}
// An update of another module moves the picture's revision: the same reasons are not said again.
c.takeModule(agent.Update{Key: "lemurs", Value: mod, Revision: 10})
c.compose(ctx)
if n := said("the node composes nothing of ghost"); n != 1 {
t.Errorf("the journal says why ghost is not composed %d times, want once", n)
}
// The state of this step is taken away again, and the pass that takes back what it handed is made here.
for _, module := range []string{"tool", "ghost", "nofp"} {
c.takeWant(agent.Update{Key: "want.n1." + module, Deleted: true})
}
c.takeModule(agent.Update{Key: "tool", Deleted: true})
c.mu.Lock()
placed.Modules = placed.Modules[:len(placed.Modules)-1]
c.assignments["n1"] = placed
c.mu.Unlock()
c.compose(ctx)
// The agent waits for what the node believes it handed: handed again; for what it never handed, nothing is.
c.Waiting("ghost", fp)
select {
case u := <-out:
t.Errorf("handed %s for a wait of a composition never handed", u.Key)
default:
}
c.Waiting("lemurs", fp)
if u := next(t, out); !strings.Contains(u.Key, "lemurs") {
t.Errorf("handed %s, want lemurs's composition again", u.Key)
}
// ADR 0601 §1: the module composed today at the build it runs, as the agent compares it with what runs. The same inputs give
// the digest handed; a changed input (here its manifest's service, as a changed setting would change a file) another; a
// build that cannot be composed (no done build or manifest of it: held) gives none.