Merge pull request 'The node-engine says why it composes a wanted module at no composition, and hands again one its agent lacks (issue 1966, recovery)' (#216) from fix/1966-views-page-time into main
This commit was merged in pull request #216.
This commit is contained in:
@@ -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
@@ -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).
|
||||
|
||||
@@ -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"}})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user