What runs from the kept declaration says Data as the module's manifest does, and only a have that runs counts at the start (issue 924)
The kept-declaration fallback built a composition without Data, so after a restart a data module (mongodb, mssql
on novox) was judged to move its data against its first composition and the operator was asked a false yes. Now
every such composition takes Data from the manifest (agent.Env.HoldsData, the composer's DataItems), and a module
whose manifest cannot be read is said to be unjudgeable rather than taken to hold none. The composition a have
names comes before the kept declaration in the judgement too.
loadHad takes a have only in the phases baseline takes as running (running, held, a composer's mark): one backing
up, applying, checking, failed or waiting names a composition not applied (review of 031e106). Two rows added to
the same table test; the count stays.
This commit is contained in:
@@ -618,6 +618,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
if composer != nil {
|
||||
composes.set(delivery.ComposesOwn)
|
||||
env.Composed, env.SelfComposed, env.Trusted, env.Policy = composer, true, nil, composer.Policy
|
||||
env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does
|
||||
session.NoteComposes(linked, delivery.ComposesOwn, why)
|
||||
// And once the catalogue no longer places it, the agent begins again on the controller's compositions: the
|
||||
// placement key deleted ends this run, the way back (issue 1126, the seed's revert).
|
||||
|
||||
+76
-10
@@ -151,6 +151,11 @@ type Env struct {
|
||||
// move of a module of policy record that recreates a container or moves its data waits for a person's yes in its
|
||||
// want (ADR 0547 §3). Nil judges no move.
|
||||
Policy func(ctx context.Context, module string) string
|
||||
// HoldsData says whether a module's manifest declares data (catalogue.Manifest.DataItems), and whether its manifest
|
||||
// could be read: what a composition's Data is, for what runs of a module from the kept declaration, which carries no
|
||||
// Data of its own. A composition says Data, so without it a data module from the kept declaration is judged to move
|
||||
// its data against its first composition after a restart (mongodb and mssql on novox, issue 924). Nil reads none.
|
||||
HoldsData func(ctx context.Context, module string) (data, known bool)
|
||||
}
|
||||
|
||||
// transientFor is how many transient retries say the phase applying before the have says failed: past them the bus has
|
||||
@@ -172,6 +177,9 @@ type Agent struct {
|
||||
// composition, read once at the start (loadHad): what runs of a module the agent has not taken yet, ahead of the kept
|
||||
// declaration (issue 924).
|
||||
had map[string]compNow
|
||||
// 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
|
||||
// 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
|
||||
@@ -230,7 +238,7 @@ func New(env Env) *Agent {
|
||||
if env.Say == nil {
|
||||
env.Say = func(string) {}
|
||||
}
|
||||
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, applied: map[string]compNow{}, had: map[string]compNow{},
|
||||
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, 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{},
|
||||
retry: make(chan []string, 8), reconcile: make(chan []string, 64), tries: map[string]int{}, again: make(chan againAsk, 64),
|
||||
transient: make(chan struct{}, 1), stopped: make(chan struct{})}
|
||||
@@ -808,10 +816,7 @@ func (a *Agent) others(except string) []delivery.Composed {
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, m := range names {
|
||||
c := delivery.Composed{Node: a.env.Node, Module: m, Resources: kept[m]}
|
||||
if a.env.KeptFilled != nil {
|
||||
c.Filled = a.env.KeptFilled()[m]
|
||||
}
|
||||
c, _ := a.keptComposed(m, kept[m])
|
||||
if m == NodeModule {
|
||||
c.Module = ""
|
||||
}
|
||||
@@ -846,12 +851,13 @@ func (a *Agent) loadHad(ctx context.Context) {
|
||||
names = append(names, m)
|
||||
}
|
||||
sort.Strings(names)
|
||||
a.readKeptData(ctx, names...)
|
||||
for _, m := range names {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
h, ok := a.env.Haves.Read(ctx, m)
|
||||
if !ok || h.Composed == "" || h.Phase == string(delivery.PhaseRemoved) {
|
||||
if !ok || h.Composed == "" || !haveRuns(h) {
|
||||
continue
|
||||
}
|
||||
a.mu.Lock()
|
||||
@@ -871,6 +877,17 @@ func (a *Agent) loadHad(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// haveRuns says a have names what runs, in the phases baseline takes as running: running (or empty), held for a failed
|
||||
// backup, or a composer's mark over what still runs. A have backing up, applying, checking, failed or waiting names a
|
||||
// composition not applied, or not yet (issue 924, review of 031e106).
|
||||
func haveRuns(h Have) bool {
|
||||
switch delivery.Phase(h.Phase) {
|
||||
case "", delivery.PhaseRunning, delivery.PhaseHeld:
|
||||
return true
|
||||
}
|
||||
return composerMark(h)
|
||||
}
|
||||
|
||||
// keptHas says whether the kept declaration has resources for the module. Called holding a.mu or not: Kept takes no lock
|
||||
// of the agent's.
|
||||
func (a *Agent) keptHas(module string) ([]json.RawMessage, bool) {
|
||||
@@ -1009,7 +1026,8 @@ func (a *Agent) heldOut(except string) []delivery.Composed {
|
||||
}
|
||||
}
|
||||
if len(rs) > 0 {
|
||||
out = append(out, delivery.Composed{Node: a.env.Node, Module: m, Resources: rs})
|
||||
// Held out, never judged: its Data from its manifest where known, as every other composition says it.
|
||||
out = append(out, delivery.Composed{Node: a.env.Node, Module: m, Resources: rs, Data: a.keptData[m]})
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -1397,6 +1415,7 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
||||
if module == NodeModule || a.env.Policy == nil || a.env.Policy(ctx, module) != catalogue.PolicyRecord || w.Want.Yes == comp.Dig {
|
||||
return false
|
||||
}
|
||||
a.readKeptData(ctx, module) // the manifest may read now though it did not at the start
|
||||
a.mu.Lock()
|
||||
r, ran := a.running[module]
|
||||
if ran && r.Composed == comp.Dig && r.Phase != delivery.PhaseRemoved {
|
||||
@@ -1442,14 +1461,61 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
||||
|
||||
// runningComposition is what the module runs now: the composition last applied, else its resources in the declaration
|
||||
// the node kept from before the switch. Called with a.mu held.
|
||||
//
|
||||
// The composition its have named at the start comes before the kept declaration (issue 924). From the kept declaration,
|
||||
// what runs carries Data as its manifest declares it; a module whose manifest could not be read cannot be judged against
|
||||
// it (false), and that is said, rather than taking it to hold no data and judging a false move of its data.
|
||||
func (a *Agent) runningComposition(module string) (delivery.Composed, bool) {
|
||||
if c, ok := a.applied[module]; ok && c.Err == nil {
|
||||
return c.C, true
|
||||
}
|
||||
if a.env.Kept != nil {
|
||||
if rs, ok := a.env.Kept()[module]; ok {
|
||||
return delivery.Composed{Node: a.env.Node, Module: module, Resources: rs}, true
|
||||
if c, ok := a.had[module]; ok {
|
||||
return c.C, true
|
||||
}
|
||||
if rs, ok := a.keptHas(module); ok {
|
||||
c, known := a.keptComposed(module, rs)
|
||||
if !known {
|
||||
a.sayOnce("kept-data:"+module, fmt.Sprintf("%s runs from the kept declaration, and whether it holds data cannot be "+
|
||||
"read from its manifest: a move of it cannot be judged", module))
|
||||
}
|
||||
return c, known
|
||||
}
|
||||
return delivery.Composed{}, false
|
||||
}
|
||||
|
||||
// keptComposed is what runs of a module from the kept declaration: its resources there, the paths it names as filled,
|
||||
// and Data as the module's manifest declares it; false when the manifest could not be read, so Data is not known. Called
|
||||
// with a.mu held.
|
||||
func (a *Agent) keptComposed(module string, rs []json.RawMessage) (delivery.Composed, bool) {
|
||||
c := delivery.Composed{Node: a.env.Node, Module: module, Resources: rs}
|
||||
if a.env.KeptFilled != nil {
|
||||
c.Filled = a.env.KeptFilled()[module]
|
||||
}
|
||||
data, known := a.keptData[module]
|
||||
c.Data = data
|
||||
return c, known || module == NodeModule
|
||||
}
|
||||
|
||||
// readKeptData reads, outside the agent's lock, whether each module of the kept declaration not known yet declares data
|
||||
// (Env.HoldsData), and keeps the answers it could read.
|
||||
func (a *Agent) readKeptData(ctx context.Context, modules ...string) {
|
||||
if a.env.HoldsData == nil {
|
||||
return
|
||||
}
|
||||
for _, m := range modules {
|
||||
if m == NodeModule {
|
||||
continue
|
||||
}
|
||||
a.mu.Lock()
|
||||
_, done := a.keptData[m]
|
||||
a.mu.Unlock()
|
||||
if _, kept := a.keptHas(m); done || !kept {
|
||||
continue
|
||||
}
|
||||
if data, known := a.env.HoldsData(ctx, m); known {
|
||||
a.mu.Lock()
|
||||
a.keptData[m] = data
|
||||
a.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,6 +1031,9 @@ func TestTheRevisionAWantNamesIsAppliedFromHistory(t *testing.T) {
|
||||
// for claude-code and screen-lock, 2026-10-25); from the composition its have names once it has one, the kept
|
||||
// declaration being older, read at the start before any module settles and told to the filling then, so a file the
|
||||
// newer composition fills is accepted on the first fill (power's three drop-ins on ace, g14 and shanks, 2026-10-29).
|
||||
// Only a have in a phase baseline takes as running names what runs; and what runs carries Data, from the composition
|
||||
// or, from the kept declaration, from the module's manifest, so a data module is not judged to move its data against
|
||||
// its first composition after a restart (mongodb and mssql on novox).
|
||||
func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
|
||||
const old, added = "/tmp/r6/power/settings", "/tmp/r6/power/bound"
|
||||
filledFile := func(id string) json.RawMessage {
|
||||
@@ -1039,7 +1042,7 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
|
||||
}
|
||||
fp := fpOf("a")
|
||||
ck, _ := delivery.ComposedKey(node, "power", fp)
|
||||
applied := delivery.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added},
|
||||
applied := delivery.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added}, Data: true,
|
||||
Resources: []json.RawMessage{filledFile("settings"), filledFile("bound")}}
|
||||
_, appliedRaw, appliedDig := signedBy(t, applied, ck, controllerKey)
|
||||
newer := applied
|
||||
@@ -1053,13 +1056,21 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
|
||||
bucket []byte
|
||||
history []Update
|
||||
filled, told string
|
||||
// data is what runs' Data; holdsData the manifest's answer (nil: none read).
|
||||
data bool
|
||||
holdsData func(context.Context, string) (bool, bool)
|
||||
}{
|
||||
{name: "no have: the kept declaration's", filled: old},
|
||||
{name: "a have naming the bucket's value", have: &Have{Module: "power", FP: fp, Composed: appliedDig},
|
||||
bucket: appliedRaw, filled: old + "," + added, told: "the haves were read at the start"},
|
||||
bucket: appliedRaw, filled: old + "," + added, told: "the haves were read at the start", data: true},
|
||||
{name: "a have applying names nothing that runs yet", have: &Have{Module: "power", FP: fp, Composed: appliedDig,
|
||||
Phase: string(delivery.PhaseApplying)}, bucket: appliedRaw, filled: old, told: "the haves were read at the start"},
|
||||
{name: "a data module from the kept declaration says Data as its manifest does", filled: old,
|
||||
told: "the haves were read at the start", data: true,
|
||||
holdsData: func(context.Context, string) (bool, bool) { return true, true }},
|
||||
{name: "a have naming a revision replaced since", have: &Have{Module: "power", FP: fp, Composed: appliedDig},
|
||||
bucket: newerRaw, history: []Update{{Key: ck, Value: appliedRaw, Revision: 3}, {Key: ck, Value: newerRaw, Revision: 7}},
|
||||
filled: old + "," + added, told: "the haves were read at the start"},
|
||||
filled: old + "," + added, told: "the haves were read at the start", data: true},
|
||||
{name: "a have naming a composition the bucket no longer keeps", have: &Have{Module: "power", FP: fp, Composed: appliedDig},
|
||||
bucket: newerRaw, filled: old, told: "the haves were read at the start"},
|
||||
} {
|
||||
@@ -1074,12 +1085,16 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
|
||||
b.comps.before(ck, tc.bucket)
|
||||
}
|
||||
b.env.Revisions = func(_ context.Context, key string) ([]Update, error) { return tc.history, nil }
|
||||
b.env.HoldsData = tc.holdsData
|
||||
var told []string
|
||||
var mu sync.Mutex
|
||||
b.env.RunsChanged = func(why string) { mu.Lock(); told = append(told, why); mu.Unlock() }
|
||||
filledNow := func() string {
|
||||
for _, c := range b.ag.Everything() {
|
||||
if c.Module == "power" {
|
||||
if c.Data != tc.data {
|
||||
t.Errorf("what runs says Data %v, want %v", c.Data, tc.data)
|
||||
}
|
||||
return strings.Join(c.Filled, ",")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,6 +757,27 @@ func (c *Composer) Policy(ctx context.Context, module string) string {
|
||||
return policy
|
||||
}
|
||||
|
||||
// HoldsData says whether a module's manifest declares data (catalogue.Manifest.DataItems), as a composition's Data says
|
||||
// it, and whether the manifest could be read: the agent's Data for what runs of a module from the kept declaration
|
||||
// (agent.Env.HoldsData, issue 924).
|
||||
func (c *Composer) HoldsData(ctx context.Context, module string) (data, known bool) {
|
||||
c.mu.Lock()
|
||||
m, ok := c.modules[module]
|
||||
c.mu.Unlock()
|
||||
if !ok || m.Manifest == "" {
|
||||
return false, false
|
||||
}
|
||||
raw, err := reader{c: c}.Manifest(ctx, m.Manifest)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
parsed, err := catalogue.ParseManifestForInputs(raw)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return len(parsed.DataItems()) > 0, true
|
||||
}
|
||||
|
||||
// reader is the composer's view of the bus as frombus reads it.
|
||||
type reader struct{ c *Composer }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user