The node's words for a want it composes nothing of are said once per reason, and proved on the live paths (issue 1966)
The said-once keys no longer hold where the indexer's picture stands; a module composed from the manifest for want of a done build is named in Pass.Unmade; the agent's wait is told once per want and again after the composition was there; the two tests are folded into existing ones. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -156,6 +156,8 @@ type Composer struct {
|
||||
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).
|
||||
@@ -351,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{}, notTaken: map[string]string{}, rehand: make(chan struct{}, 1), 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)}
|
||||
}
|
||||
|
||||
@@ -1051,8 +1053,9 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
}
|
||||
c.paid()
|
||||
pass.Held, pass.holds = held, holds
|
||||
pass.Unmade = c.unmadeOf(fps, compositions, held, skipped, own, inOwn, fromManifest, &keptMu)
|
||||
c.sayUnmade(pass.Unmade)
|
||||
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).
|
||||
@@ -2072,7 +2075,7 @@ func (c *Composer) seatsOf(ctx context.Context, r reader, module string) []strin
|
||||
// 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 {
|
||||
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 != "" {
|
||||
@@ -2087,51 +2090,86 @@ func (c *Composer) unmadeOf(fps map[string]string, comps map[string]engcomp.Comp
|
||||
defer mu.Unlock()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
out := map[string]string{}
|
||||
out, keys := map[string]string{}, map[string]string{}
|
||||
for module, fp := range fps {
|
||||
if _, isHeld := held[module]; isHeld || made[module] == fp {
|
||||
if _, isHeld := held[module]; isHeld {
|
||||
continue
|
||||
}
|
||||
var why string
|
||||
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:
|
||||
why = fmt.Sprintf("the composition made nothing of it at %s (indexer's picture: %s, revision %d)", fp, m.Fp2, c.modulesRev)
|
||||
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] = why
|
||||
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
|
||||
return nil, keys
|
||||
}
|
||||
return out
|
||||
return out, keys
|
||||
}
|
||||
|
||||
// sayUnmade says each change of what the pass composed none of, once.
|
||||
func (c *Composer) sayUnmade(unmade map[string]string) {
|
||||
if len(unmade) == 0 {
|
||||
// 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.sayOnceEach("the node composes nothing of " + strings.Join(lines, "; "))
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user