G §683: a start composes the modules whose kept want and entry it holds before its other inputs are read

The node's own record comes from the node-engine's statement of itself, taken in memory with no bus.

Builds: design 47 G A-117, A-120, §683, A-95
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 01:12:06 +01:00
parent 054ca5a6b3
commit c6df8fa20f
4 changed files with 127 additions and 2 deletions
+10 -1
View File
@@ -125,10 +125,19 @@ func composeFromEntries(t *testing.T) {
mu.Unlock()
env2 := b.env()
env2.Entry, env2.ComposedState = EntryCall(ask), state
// The node's own record, stated by the node-engine of itself in memory (A-120).
env2.NodeRecord = func() (nodeengine.NodeRecord, bool) {
return nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}, true
}
c2 := New(env2)
if _, err := c2.Watch(ctx, "n1.>"); err != nil {
out2, err := c2.Watch(ctx, "n1.>")
if err != nil {
t.Fatal(err)
}
// And composes the module it kept a want and an entry of before any input is read, nothing of the others (design 47 G §683, A-117).
if u := next(t, out2); u.Key != lemursKey || u.Deleted {
t.Errorf("the early pass handed %q, want the composition %s of lemurs", u.Key, lemursKey)
}
c2.mu.Lock()
kept, keptWant := c2.keptEnt[fp], c2.wants["lemurs"].Fp
c2.mu.Unlock()
+97
View File
@@ -61,6 +61,9 @@ const ResolvedBucket = "mesh-config_resolved"
// Env is what the composer needs of the node.
type Env struct {
Node string
// NodeRecord is the node's own record as the node-engine states it of itself, held in memory; the early pass of a start composes
// with it before the have bucket is read (design 47 G A-120). Nil, or false, has none.
NodeRecord func() (nodeengine.NodeRecord, bool)
// EntryNotFound is told when the catalog half answered it holds no entry of a fingerprint (notFound), and when that is no longer so:
// the entry answered, or nothing the node holds names it (design 47 G §614, A-117). Nil tells nobody.
EntryNotFound func(fp string, notFound bool)
@@ -241,6 +244,8 @@ type Composer struct {
asked chan chan Pass
// passing is set while a pass is made.
passing atomic.Bool
// early is true while the early pass of a start runs (design 47 G §683, A-117).
early atomic.Bool
// last is how the last pass went, under mu.
last Pass
// watching says the composer's loop runs (Watch), so an ask of a pass can be answered.
@@ -611,6 +616,20 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er
// input moves (issue 1126, 2026-10-28).
var again <-chan time.Time
var replies []chan Pass
// At a start, until its assignments, haves and resolved values are read, the node composes the modules whose kept want and
// kept entry it holds, so the bus module and the indexer start while both are down (design 47 G §683, A-95, A-117).
if c.standing() {
c.passing.Store(true)
updates := c.composeEarly(ctx)
c.passing.Store(false)
for _, u := range updates {
select {
case out <- u:
case <-ctx.Done():
return
}
}
}
for {
// Drain whatever came, then compose once for all of it.
select {
@@ -975,6 +994,7 @@ func (c *Composer) takeValue(u agent.Update) {
// compose composes the node once and answers the updates to hand: each composition whose digest differs from the one
// handed, and a deletion for each handed one no want names any more.
func (c *Composer) compose(ctx context.Context) []agent.Update {
early := c.early.Load()
pass := Pass{}
defer func() {
pass.At = time.Now()
@@ -1013,6 +1033,9 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
if module == agent.NodeModule {
continue
}
if early && !c.stands(module, w) {
continue
}
fps[module] = w.Fp
if m, ok := c.modules[module]; ok && m.Fp2 != w.Fp {
behind = append(behind, module)
@@ -1113,6 +1136,11 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
} else {
c.paid()
}
if early {
// Inputs not yet read may refuse it: nothing is marked, and the full pass follows when they are read.
c.env.Say("the node's early composition of the modules it kept is refused, and the full pass follows: " + why)
return nil
}
pass.Refused = why
// Said before the agent marks the modules: the marks read each have first, which a paused bus delays.
c.sayOnce("the node cannot compose its modules, and keeps running what it runs: " + why)
@@ -1232,6 +1260,9 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
pubComps[key] = comp
}
if early {
return out // what the early pass handed is composed from kept state alone: nothing is published from it
}
c.pubAt = &published{scope: scope, fps: maps.Clone(fps), comps: pubComps, held: maps.Clone(held), placed: placedMods,
ran: ranOf(running)}
c.pubDue = true
@@ -2431,3 +2462,69 @@ func (c *Composer) EngineModule() string {
defer c.mu.Unlock()
return c.engine
}
// stands says the want of a module stands for its placement in the early pass of a start: its want is a kept one the live read has
// not answered, and the entry of its build is kept (design 47 G A-117).
func (c *Composer) stands(module string, w meshwants.WantsByWantNodeModuleValue) bool {
if !c.byEntries() || !c.keptWants[module] {
return false
}
k, ok := c.keptEnt[w.Fp]
return ok && k.entry.Module != ""
}
// standing says the composer holds a kept want and the kept entry of its build, so an early pass has something to compose.
func (c *Composer) standing() bool {
c.mu.Lock()
defer c.mu.Unlock()
for module, w := range c.wants {
if module != agent.NodeModule && c.stands(module, w) {
return true
}
}
return false
}
// composeEarly is the pass of a start before the assignments, haves and resolved values are read: it composes only the modules whose
// kept want and kept entry it holds, a kept want standing for their placement, and leaves every other module as it runs; the full
// pass follows once the inputs are read (design 47 G §683, A-95, A-117).
func (c *Composer) composeEarly(ctx context.Context) []agent.Update {
node := c.env.Node
c.mu.Lock()
_, hadRecord := c.records[node]
if !hadRecord && c.env.NodeRecord != nil {
if rec, ok := c.env.NodeRecord(); ok {
c.records[node] = rec
} else {
hadRecord = true // nothing laid, nothing to take away
}
}
_, had := c.assignments[node]
if !had {
p := meshmoduleassigner.Assignments{Node: node}
for module, w := range c.wants {
if module != agent.NodeModule && c.stands(module, w) {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: module})
}
}
sort.Slice(p.Modules, func(i, j int) bool { return p.Modules[i].Module < p.Modules[j].Module })
c.assignments[node] = p
}
c.mu.Unlock()
c.early.Store(true)
defer func() {
c.early.Store(false)
if !hadRecord {
c.mu.Lock()
delete(c.records, node)
c.mu.Unlock()
}
if !had {
c.mu.Lock()
delete(c.assignments, node)
c.mu.Unlock()
}
}()
c.env.Say("the node composes the modules it kept a want and an entry of, before its other inputs are read")
return c.compose(ctx)
}