R3-1, R3-2, R3-3: the node-engine reads no build of the builder; the shadow builds from the entry it holds

The composer's Build is made from the catalog half's entry it holds and the parts of the want that names it (buildOf); the
inputs of the indexer's modules and the builder's builds, takeBuildDone, takeModule and the builder read are gone, and mesh-host
names no BuilderBuildsBucket. The shadow takes the entries of the builds it composes from (askBuildsEntries), builds a module a
candidate adds from its entry and the candidate's `added` parts (A-316), and holds, with the reason, a module whose entry
has no part given, whose fingerprint the verb module does not give, or for which it errors (A-312). Its composer is given the
entry call as the node's own is. Rows: a built module added with and without parts, the verb module answering no fingerprint
and erroring.

Builds: design 47 G §421, §628, A-308, A-312, A-316
This commit is contained in:
2026-11-12 23:05:06 +01:00
parent e558510a54
commit 23799a30ed
12 changed files with 279 additions and 359 deletions
+12 -116
View File
@@ -180,10 +180,7 @@ type Composer struct {
values map[string]map[string]any // by module, this node's
manifests map[string][]byte // by digest, read once
archives map[string][]byte // templates archives by digest, read once
// builds are the builder's done builds by key, read once: a done build at a fingerprint never changes, so a pass and
// 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
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).
@@ -203,8 +200,6 @@ type Composer struct {
forCandidates bool
// addedFps are the builds of modules a candidate adds that the shadow asked the entry of (addedModule), kept beside the wants' under mu.
addedFps map[string]bool
// modulesRev is the revision of the last update of the indexer's modules taken: where the picture of them stands.
modulesRev uint64
// heard is, by node and module, what each node's event running last said it runs, kept from the seat's stream by this
// node's own consumer (NoteRunning), under mu; heardRead is whether the first read of the stream ended (RunningRead).
heard map[string]map[string]heardRun
@@ -418,7 +413,7 @@ func New(env Env) *Composer {
func newComposer(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{}, unmadeSaid: map[string]string{}, rehand: make(chan struct{}, 1), sealedOwn: map[string]ownSealed{},
manifests: map[string][]byte{}, archives: map[string][]byte{}, 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)}
}
@@ -478,22 +473,9 @@ type input struct {
changed func(c *Composer, u agent.Update) bool
}
// inputs are the buckets the composer watches: it watches neither the indexer's modules nor the builder's builds, but reads the
// catalog half's entry of each build its wants name (§421, §628).
func (c *Composer) inputs() []input {
ins := c.allInputs()
if !c.byEntries() {
return ins
}
// Reading the catalog half by fingerprint, it watches neither the indexer's modules nor the builder's builds (§421, §628).
out := ins[:0:0]
for _, in := range ins {
if in.bucket != buckets.Modules && in.bucket != builds.BuilderBuildsBucket {
out = append(out, in)
}
}
return out
}
func (c *Composer) allInputs() []input {
node := c.env.Node
return []input{
{buckets.Wants, "want." + node + ".>", (*Composer).takeWant, nil},
@@ -503,50 +485,10 @@ func (c *Composer) allInputs() []input {
// A watch of every key is refused by the bus (issue 1126, 2026-10-27 09:21: the fold's `>` stopped every agent).
{buckets.Have, "*." + nodeengineclient.NodeWord, nil, (*Composer).takeRecordChanged},
{buckets.Have, node + ".>", nil, (*Composer).takeModuleHave},
{buckets.Modules, ">", (*Composer).takeModule, nil},
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
// The builder's builds: a pass that held a module for a build not yet done is made again when it lands (issue
// 774, 2026-10-30 19:30: node-tools' build landed and ace, g14 and shanks stayed refused until another input moved).
{builds.BuilderBuildsBucket, ">", nil, (*Composer).takeBuildDone},
}
}
// takeBuildDone says whether a build that landed is one this node composes from: done, of a module it wants at that
// build, or wanted or placed here whose trunk's build it is; or failed, of a module placed here whose trunk's build it
// is, which the node publishes as a refusal (ADR 0593 §6). Every other build moves nothing.
func (c *Composer) takeBuildDone(u agent.Update) bool {
// Any write or delete of a build's key forgets the build kept as done: the builder claims a done key again as a new
// revision (a gone step, issue 1937), which lands done with other artifacts.
c.mu.Lock()
delete(c.builds, u.Key)
c.mu.Unlock()
if u.Deleted {
return false
}
module, fp, err := builds.ParseBuilderBuildKey(u.Key)
if err != nil {
return false
}
c.mu.Lock()
w, wanted := c.wants[module]
m := c.modules[module]
placedHere := false
for _, pm := range c.assignments[c.env.Node].Modules {
placedHere = placedHere || pm.Module == module
}
c.mu.Unlock()
// A module placed here is composed at its trunk's build once that is done, for the composition state, before its
// want moves (ADR 0593 §6).
b, err := builds.DecodeBuilderBuild(u.Value)
if err != nil {
return false
}
if b.State == builds.BuildFailed {
return placedHere && m.Fp2 == fp
}
return b.State == builds.BuildDone && (wanted && w.Fp == fp || (wanted || placedHere) && m.Fp2 == fp)
}
// Watch starts the composer and answers the compositions it makes: each as a key of `composed-<node>` would be, then a
// Caught update once every input was read and the node composed once, then every change. It ends when ctx ends or an
// input's watch ends.
@@ -794,13 +736,6 @@ func (c *Composer) entry(bucket, key string) (v any, has, known bool) {
}
p, ok := c.assignments[node]
return p, ok, true
case buckets.Modules:
name, err := meshcatalogclient.ParseModulesKey(key)
if err != nil {
return nil, false, true
}
m, ok := c.modules[name]
return m, ok, true
case ResolvedBucket:
rest, ok := strings.CutPrefix(key, c.env.Node+".")
if !ok {
@@ -969,22 +904,6 @@ func (c *Composer) inOwnOf(p meshmoduleassigner.Assignments) []string {
return out
}
func (c *Composer) takeModule(u agent.Update) {
name, err := meshcatalogclient.ParseModulesKey(u.Key)
if err != nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
m, err := meshcatalogclient.DecodeModule(u.Value)
if u.Deleted || err != nil {
delete(c.modules, name)
return
}
c.modules[name] = m
c.modulesRev = u.Revision
}
func (c *Composer) takeValue(u agent.Update) {
rest, ok := strings.CutPrefix(u.Key, c.env.Node+".")
if !ok {
@@ -1659,33 +1578,10 @@ func (r reader) Manifest(ctx context.Context, digest string) ([]byte, error) {
return []byte(m.Manifest), nil
}
func (r reader) Build(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) {
if r.c.byEntries() && !r.c.forCandidates {
return r.c.buildOf(module, fp), nil
}
key, err := builds.BuilderBuildKey(module, fp)
if err != nil {
return nil, err
}
r.c.mu.Lock()
done, ok := r.c.builds[key]
r.c.mu.Unlock()
if ok {
return &done, nil
}
defer r.c.waiting("the build of " + module + " at " + fp + " (" + builds.BuilderBuildsBucket + ")")()
raw, err := r.c.get(ctx, builds.BuilderBuildsBucket, key)
if err != nil || raw == nil {
return nil, err
}
b, err := builds.DecodeBuilderBuild(raw)
if err != nil {
return nil, fmt.Errorf("the builder's %s: %w", key, err)
}
if b.State == builds.BuildDone {
r.c.keepRead(func(k *Composer) { k.builds[key] = b })
}
return &b, nil
// Build is the build a want names, made from the catalog half's entry the composer holds (§421, §628): the builder's builds are
// not read.
func (r reader) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) {
return r.c.buildOf(module, fp), nil
}
// archiveWithin bounds one read of a templates archive in the artifact store.
@@ -2313,18 +2209,18 @@ func (c *Composer) unmadeOf(fps map[string]string, comps map[string]engcomp.Comp
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)
why = fmt.Sprintf("no manifest in the composer's picture of the indexer's modules (%d modules)",
len(c.modules))
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)
why = fmt.Sprintf("the pass composed it at %s and the want names %s (indexer's picture: %s)", at, fp, m.Fp2)
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)
why = fmt.Sprintf("the composition made nothing of it at %s (indexer's picture: %s)", fp, m.Fp2)
key = "made nothing at " + m.Fp2
}
}