What a node keeps of a module as it runs, for a manifest that does not yet say it, is carried on the module's have by module, so the watchman raises its condition (ADR 0601 §4; review round 1, finding 3)

This commit is contained in:
2026-11-03 14:02:01 +01:00
parent b461b005e9
commit 7da4deb3f1
9 changed files with 100 additions and 15 deletions
+10 -3
View File
@@ -120,7 +120,7 @@ func (h sessionHaves) Write(ctx context.Context, x agent.Have) {
}
h.session.NoteModule(ctx, link.ModuleFact{Module: x.Module, FP: x.FP, Composed: x.Composed, Phase: phase,
Failed: phase == string(delivery.PhaseFailed), Detail: shorten(x.Detail), Judgement: x.Judgement, Waits: x.Waits,
BuildAlone: x.BuildAlone, Sealed: x.Sealed})
BuildAlone: x.BuildAlone, Kept: x.Kept, Sealed: x.Sealed})
}
func (h sessionHaves) Removed(ctx context.Context, module string) {
@@ -606,6 +606,12 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
now.NotComposedEach(run, why, held) // a module held alone marks that module, never the others (issue 1126)
}
},
// And what it keeps of a module as it runs, for a manifest that does not say it yet: on the module's have (ADR 0601 §4).
kept: func(kept map[string][]string) {
if now := own.get(); now != nil && run.Err() == nil {
now.KeptNow(run, kept)
}
},
// And the vault's authority beside the controller's, and this node's certificate asked of the vault when a
// module here asks for one (ADR 0550 §2).
vaultAuthority: trust.VaultAuthority,
@@ -616,7 +622,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
// And its answer to every placement candidate, placed or not, applying nothing (novox/hq ADR 0559 §4): written into
// its own key, `<node>.node`, under candidates.
shadowHooks := hooks
shadowHooks.refused, shadowHooks.certify = nil, nil
shadowHooks.refused, shadowHooks.kept, shadowHooks.certify = nil, nil, nil
shadowEnv := composerEnv(session, node, say, shadowHooks)
shadow := selfcompose.NewShadow(shadowEnv,
func(ctx context.Context, answers map[string]delivery.CandidateAnswer) {
@@ -958,6 +964,7 @@ type composerHooks struct {
running func() []delivery.Composed
unseal func(string) ([]byte, error)
refused func(why string, held map[string]string)
kept func(kept map[string][]string)
held func(module string) (delivery.Seals, []string, error)
// vaultAuthority and certify are selfcompose.Env's VaultAuthority and Certify.
vaultAuthority func() string
@@ -1011,6 +1018,6 @@ func composerEnv(session *link.Session, node string, say func(string), hooks com
Hear: session.Hear,
// And a moved module's templates archive, in the artifact store, as every archive is fetched (issue 1126).
Fetch: apply.Fetch,
Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held,
Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held, Kept: hooks.kept,
VaultAuthority: hooks.vaultAuthority, Certify: hooks.certify}
}
+41
View File
@@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"sync"
@@ -60,6 +61,9 @@ type Have struct {
Waits string
// BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, delivery.Have.BuildAlone).
BuildAlone bool
// Kept says, in words, what the node keeps of the module as it runs because its manifest does not yet say it (ADR 0601
// §4, delivery.Have.Kept): the watchman raises a condition from it.
Kept []string
// Sealed is, on a node composing its own, each value the composition it names carries sealed to the node's key, by
// slot (delivery.Have.SealedAt): kept in the have so the composer reuses them after a restart (ADR 0547 §3), each at
// its own slot alone (issue 1126, C1).
@@ -205,6 +209,8 @@ type Agent struct {
// keptData is, for each module of the kept declaration whose manifest could be read, whether it declares data
// (Env.HoldsData): the Data of what runs of it from the kept declaration.
keptData map[string]bool
// kept is, by module, what the composer's last pass keeps of it as it runs (KeptNow, ADR 0601 §4). Under mu.
kept map[string][]string
// attempt is the last apply of a module that failed or is held, with the revisions it was made from.
attempt map[string]attempt
checked map[string]bool
@@ -579,6 +585,11 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string
// said runs, its fingerprint, digest and sealed values, rather than a have naming nothing: what runs is not lost, and
// the boxes the composer reuses after a restart stay (review of L2, rounds 2 and 3).
func (a *Agent) writeHave(ctx context.Context, h Have) {
if h.Module != NodeModule {
a.mu.Lock()
h.Kept = a.kept[h.Module]
a.mu.Unlock()
}
if h.Composed == "" && h.Module != NodeModule {
if last, ok := a.env.Haves.Read(ctx, h.Module); ok && last.Composed != "" {
h.FP, h.Composed = last.FP, last.Composed
@@ -595,6 +606,36 @@ func (a *Agent) writeHave(ctx context.Context, h Have) {
a.env.Haves.Write(ctx, h)
}
// KeptNow takes in what the composer's last pass keeps of each module as it runs because its manifest does not yet say it
// (ADR 0601 §4), and writes again the have of each module whose words changed, so it carries them; a have waiting for a yes
// takes them at its next write, which keeps what it waits for.
func (a *Agent) KeptNow(ctx context.Context, kept map[string][]string) {
a.mu.Lock()
var changed []string
for m := range a.kept {
if _, still := kept[m]; !still {
changed = append(changed, m)
}
}
for m, words := range kept {
if !slices.Equal(a.kept[m], words) {
changed = append(changed, m)
}
}
a.kept = map[string][]string{}
for m, words := range kept {
a.kept[m] = slices.Clone(words)
}
a.mu.Unlock()
slices.Sort(changed)
for _, m := range changed {
if h, ok := a.env.Haves.Read(ctx, m); ok && m != NodeModule && h.Phase != string(delivery.PhaseWaitingYes) {
h.Module = m
a.writeHave(ctx, h)
}
}
}
func (a *Agent) sealedOf(ctx context.Context, module, fp, composed string) delivery.Seals {
a.mu.Lock()
if c, ok := a.applied[module]; ok && c.Err == nil && c.Dig == composed {
+9
View File
@@ -1340,6 +1340,15 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
want("web", fp, "")
eventually(t, "the node's own composition was not applied", func() bool { return len(b.appliedList()) == 1 })
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"}})
if h := b.haves.last("web"); len(h.Kept) != 1 || h.Composed != dig {
t.Errorf("web's have does not carry what is kept as it runs: %+v", h)
}
b.ag.KeptNow(context.Background(), nil)
if h := b.haves.last("web"); len(h.Kept) != 0 || h.Composed != dig {
t.Errorf("web's have still carries what was kept: %+v", h)
}
// The same composition handed again is no move.
b.comps.put(ck, craw)
time.Sleep(50 * time.Millisecond)
+3 -3
View File
@@ -156,7 +156,7 @@ type Scope struct {
SameValue func(a, b string) (same, known bool)
// 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(words string)
Kept func(module, words string)
}
// InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs
@@ -235,9 +235,9 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
in, err := b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted)
if err == nil {
// What runs is kept while a manifest does not yet say what shared code no longer infers (ADR 0601 §4).
for _, words := range keepWhatRuns(&in, s.Node, s.Running) {
for _, k := range keepWhatRuns(&in, s.Node, s.Running) {
if s.Kept != nil {
s.Kept(words)
s.Kept(k.Module, k.Words)
}
}
}
+1 -1
View File
@@ -887,7 +887,7 @@ func whatRunsIsKeptWhileAManifestDoesNotSayIt(t *testing.T) {
in := input(false)
said := keepWhatRuns(&in, "a", running)
if !in.Catalogue["runner-x"].ToolRunner || !in.Catalogue["prov-y"].CoreCredentials || in.Catalogue["app"].ToolRunner ||
in.Catalogue["app"].CoreCredentials || len(said) != 2 {
in.Catalogue["app"].CoreCredentials || len(said) != 2 || said[0].Module != "prov-y" || said[1].Module != "runner-x" {
t.Errorf("what runs was not kept: %+v, said %q", in.Catalogue, said)
}
in = input(true)
+13 -7
View File
@@ -23,9 +23,9 @@ import (
// Known by what runs, never by a module's name.
// keepWhatRuns marks, in in, each module placed on node that what runs shows is the tool runner or carries the core's
// credentials, where its manifest does not say so; it returns, sorted, what it marked, in words. The tool runner is marked
// only where no placed module's manifest says it is one.
func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) []string {
// credentials, where its manifest does not say so; it returns, sorted by module, what it marked, in words. The tool runner is
// marked only where no placed module's manifest says it is one.
func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) []KeptAsRuns {
placed := assignedOn(*in, node)
if len(placed) == 0 || len(running) == 0 {
return nil
@@ -60,7 +60,7 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [
}
}
}
var said []string
var said []KeptAsRuns
saysRunner := false
for _, m := range placed {
if man, ok := manifestOn(*in, node, m); ok && catalogue.IsToolRunner(man) {
@@ -68,6 +68,7 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [
}
}
mark := func(module string, set func(*catalogue.Manifest), words string) {
k := KeptAsRuns{Module: module, Words: words}
for i := range in.Nodes {
if in.Nodes[i].Name != node {
continue
@@ -75,14 +76,14 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [
if man, ok := in.Nodes[i].Kept[module]; ok {
set(&man)
in.Nodes[i].Kept[module] = man
said = append(said, words)
said = append(said, k)
return
}
}
if man, ok := in.Catalogue[module]; ok {
set(&man)
in.Catalogue[module] = man
said = append(said, words)
said = append(said, k)
}
}
if runner != "" && !saysRunner {
@@ -95,6 +96,11 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [
"and its manifest does not say they are the core's credentials (core-credentials): they stay sealed, until its manifest says so", m))
}
}
sort.Strings(said)
sort.SliceStable(said, func(i, j int) bool { return said[i].Module < said[j].Module })
return said
}
// KeptAsRuns is one module composed as what runs shows, and why, in words (ADR 0601 §4): its have carries the words.
type KeptAsRuns struct {
Module, Words string
}
+1
View File
@@ -374,6 +374,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
f := s.modules[module]
h := delivery.Have{V: delivery.Version, FP: f.FP, Composed: f.Composed, Build: f.Build,
Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown, Detail: f.Detail}
h.Kept = f.Kept
if delivery.Phase(f.Phase) == delivery.PhaseWaitingYes {
h.Judgement, h.Waits, h.BuildAlone = delivery.Judgement(f.Judgement), f.Waits, f.BuildAlone
}
+2
View File
@@ -641,6 +641,8 @@ type ModuleFact struct {
Waits string
// BuildAlone says, in phase waiting-yes, the move changes the build alone (ADR 0601 §1, delivery.Have.BuildAlone).
BuildAlone bool
// Kept is what the node keeps of the module as it runs, for a manifest that does not say it yet (delivery.Have.Kept).
Kept []string
// Sealed is the values the composition it names carries sealed to this node's key, by slot (delivery.Have.SealedAt).
Sealed delivery.Seals
}
+20 -1
View File
@@ -78,6 +78,9 @@ type Env struct {
// only within its own module.
// An error is a have the bus did not answer for, never one absent: the pass is then tried again (BusFailed).
Held func(module string) (slots delivery.Seals, legacy []string, err error)
// Kept is told, after each pass that composed, what the node keeps of each module as it runs because its manifest does
// not yet say it (ADR 0601 §4), by module, so each module's have carries it (agent.KeptNow). Nil tells nobody.
Kept func(kept map[string][]string)
// Fetch reads an archive at a URL in the artifact store: a moved module's templates archive, whose texts its built
// manifest carries (issue 1126). Read once per digest. Nil reads none, and a moved module does not compose.
Fetch func(ctx context.Context, url string) ([]byte, error)
@@ -844,7 +847,18 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
// Its own certificate, asked of the vault when a module here asks for one and the record holds none it trusts.
c.certifyIfWanted(ctx, r, own)
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Kept: c.sayOnceEach}
// What it keeps of a module as it runs, for a manifest that does not say it yet (ADR 0601 §4): said, and carried on the
// module's have, from which the watchman raises its condition.
keptRuns := map[string][]string{}
var keptMu sync.Mutex
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Kept: func(module, words string) {
keptMu.Lock()
defer keptMu.Unlock()
if !slices.Contains(keptRuns[module], words) {
keptRuns[module] = append(keptRuns[module], words)
}
c.sayOnceEach(words)
}}
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()
@@ -906,6 +920,11 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
done := c.waiting("the agent marking each held module's have")
c.refused("", held)
done()
if c.env.Kept != nil {
keptMu.Lock()
c.env.Kept(keptRuns)
keptMu.Unlock()
}
if len(held) == 0 {
c.sayOnce("")
} else {