Node-engine: the have, the node's facts, the record, the health report and the data items are the seat's contract types, written through the node-engine client; the interim bridge is gone (Have group, ADR 0634 §1, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
+64
-58
@@ -17,6 +17,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -61,18 +63,18 @@ type Have struct {
|
||||
// waits for a person's yes (ADR 0547 §3).
|
||||
Judgement string
|
||||
Waits string
|
||||
// BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, delivery.Have.BuildAlone).
|
||||
// BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, nodeengine.HaveByNodeModuleValue.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.
|
||||
// §4, nodeengine.HaveByNodeModuleValue.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
|
||||
// slot (nodeengine.HaveByNodeModuleValue.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).
|
||||
Sealed compositions.Seals
|
||||
// PullWait is, on a failed have, the pull from a store its apply failed on and waits on the holder of (issue 1966
|
||||
// ENG-9, delivery.Have.PullWait): read through the have, as node_haves shows it.
|
||||
PullWait *delivery.PullWait
|
||||
// ENG-9, nodeengine.HaveByNodeModuleValue.PullWait): read through the have, as node_haves shows it.
|
||||
PullWait *nodeengine.PullWait
|
||||
}
|
||||
|
||||
// Haves is the node's have bucket as the agent uses it.
|
||||
@@ -220,7 +222,7 @@ type Agent struct {
|
||||
mu sync.Mutex
|
||||
wants map[string]wantNow // by module
|
||||
comps map[string]compNow // by composed key
|
||||
running map[string]delivery.Have
|
||||
running map[string]nodeengine.HaveByNodeModuleValue
|
||||
// applied is the composition each module was last applied from: a revision a want named from the key's history is
|
||||
// not the key's latest value, and what it holds is still held out of the others' applies.
|
||||
applied map[string]compNow
|
||||
@@ -303,7 +305,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{}, keptData: map[string]bool{},
|
||||
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]nodeengine.HaveByNodeModuleValue{}, 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{},
|
||||
pulls: map[string]*pullWait{}, settledAt: map[string]time.Time{}, settledSeq: map[string]int{},
|
||||
retry: make(chan []string, 8), reconcile: make(chan []string, 64), failedAgain: make(chan string, 8), tries: map[string]int{}, again: make(chan againAsk, 64),
|
||||
@@ -495,7 +497,7 @@ const NotComposedDetail = "the node cannot compose it, and keeps running what it
|
||||
|
||||
// composerMark says h is a mark NotComposed wrote: failed, its detail opening with NotComposedDetail.
|
||||
func composerMark(h Have) bool {
|
||||
return h.Phase == string(delivery.PhaseFailed) && strings.HasPrefix(h.Detail, NotComposedDetail)
|
||||
return h.Phase == string(nodeengineclient.PhaseFailed) && strings.HasPrefix(h.Detail, NotComposedDetail)
|
||||
}
|
||||
|
||||
// HavesReadErr is what a Haves may answer besides Read: a have absent (false, nil) told from one that could not be read
|
||||
@@ -590,7 +592,7 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string
|
||||
a.mu.Unlock()
|
||||
back := mark.prior
|
||||
if ran {
|
||||
back = Have{Module: module, FP: r.FP, Composed: r.Composed, Phase: string(delivery.PhaseRunning)}
|
||||
back = Have{Module: module, FP: r.Fp, Composed: r.Composed, Phase: string(nodeengineclient.PhaseRunning)}
|
||||
if r.Phase != "" {
|
||||
back.Phase = string(r.Phase)
|
||||
}
|
||||
@@ -603,9 +605,9 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string
|
||||
}
|
||||
if !marked {
|
||||
if ran {
|
||||
mark.prior = Have{Module: module, FP: r.FP, Composed: r.Composed, Phase: string(r.Phase)}
|
||||
mark.prior = Have{Module: module, FP: r.Fp, Composed: r.Composed, Phase: string(r.Phase)}
|
||||
if mark.prior.Phase == "" {
|
||||
mark.prior.Phase = string(delivery.PhaseRunning)
|
||||
mark.prior.Phase = string(nodeengineclient.PhaseRunning)
|
||||
}
|
||||
} else if h, found, unread := a.readHave(ctx, module); found {
|
||||
mark.prior = h // what its last have said runs: this agent has run nothing of it since its start
|
||||
@@ -624,7 +626,7 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string
|
||||
// A have that already says another cause (an apply that failed, a backup held, an apply under way) keeps it: a
|
||||
// composer's mark over it would hide that cause, and baseline would take the mark for what runs (review of the
|
||||
// switch, round 1).
|
||||
if p := mark.prior.Phase; !marked && p != "" && p != string(delivery.PhaseRunning) && !composerMark(mark.prior) {
|
||||
if p := mark.prior.Phase; !marked && p != "" && p != string(nodeengineclient.PhaseRunning) && !composerMark(mark.prior) {
|
||||
a.mu.Lock()
|
||||
a.sayOnce("unmarked-cause:"+module, fmt.Sprintf("%s is not marked: its have says %s (%s), and the node cannot compose it: %s",
|
||||
module, p, mark.prior.Detail, why))
|
||||
@@ -639,7 +641,7 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string
|
||||
a.notComposed[module] = mark
|
||||
a.mu.Unlock()
|
||||
a.writeHave(ctx, Have{Module: module, FP: mark.prior.FP, Composed: mark.prior.Composed, Sealed: mark.prior.Sealed,
|
||||
Phase: string(delivery.PhaseFailed), Detail: NotComposedDetail + why})
|
||||
Phase: string(nodeengineclient.PhaseFailed), Detail: NotComposedDetail + why})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +698,7 @@ func (a *Agent) KeptNow(ctx context.Context, kept map[string][]string) {
|
||||
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) {
|
||||
if h, ok := a.env.Haves.Read(ctx, m); ok && m != NodeModule && h.Phase != string(nodeengineclient.PhaseWaitingYes) {
|
||||
h.Module = m
|
||||
a.writeHave(ctx, h)
|
||||
}
|
||||
@@ -733,10 +735,10 @@ func (a *Agent) Running() []compositions.Composed {
|
||||
var out []compositions.Composed
|
||||
for _, m := range mods {
|
||||
h := a.running[m]
|
||||
if h.Phase == delivery.PhaseRemoved {
|
||||
if h.Phase == nodeengineclient.PhaseRemoved {
|
||||
continue
|
||||
}
|
||||
if c, ok := a.compositionFor(m, h.FP); ok && c.Err == nil && c.Dig == h.Composed {
|
||||
if c, ok := a.compositionFor(m, h.Fp); ok && c.Err == nil && c.Dig == h.Composed {
|
||||
out = append(out, c.C)
|
||||
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
||||
out = append(out, c.C)
|
||||
@@ -890,10 +892,10 @@ func (a *Agent) others(except string) []compositions.Composed {
|
||||
sort.Strings(mods)
|
||||
for _, m := range mods {
|
||||
h := a.running[m]
|
||||
if h.Phase == delivery.PhaseRemoved {
|
||||
if h.Phase == nodeengineclient.PhaseRemoved {
|
||||
continue
|
||||
}
|
||||
if c, ok := a.compositionFor(m, h.FP); ok && c.Err == nil && c.Dig == h.Composed {
|
||||
if c, ok := a.compositionFor(m, h.Fp); ok && c.Err == nil && c.Dig == h.Composed {
|
||||
out = append(out, c.C)
|
||||
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
||||
out = append(out, c.C)
|
||||
@@ -1091,8 +1093,8 @@ func (a *Agent) keepApplied(module string, comp compNow) {
|
||||
// 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:
|
||||
switch string(h.Phase) {
|
||||
case "", nodeengineclient.PhaseRunning, nodeengineclient.PhaseHeld:
|
||||
return true
|
||||
}
|
||||
return composerMark(h)
|
||||
@@ -1171,7 +1173,7 @@ func (a *Agent) heldOut(except string) []compositions.Composed {
|
||||
if err != nil || isNode || c.Err != nil || m == except || held[m] {
|
||||
continue
|
||||
}
|
||||
if h, ok := a.running[m]; ok && h.Phase != delivery.PhaseRemoved {
|
||||
if h, ok := a.running[m]; ok && h.Phase != nodeengineclient.PhaseRemoved {
|
||||
continue
|
||||
}
|
||||
if w := a.wants[m]; w.Present && w.Want.Removed {
|
||||
@@ -1281,7 +1283,7 @@ func (a *Agent) RetryFailed(module string) error {
|
||||
return fmt.Errorf("%s has no want on this machine, so there is no apply of it to try again", module)
|
||||
case tried && att.Held:
|
||||
return fmt.Errorf("%s is held for a failed backup: the operator's answer releases it", module)
|
||||
case !tried && ran && r.FP == w.Want.FP && r.Phase != delivery.PhaseRemoved:
|
||||
case !tried && ran && r.Fp == w.Want.FP && r.Phase != nodeengineclient.PhaseRemoved:
|
||||
return fmt.Errorf("%s runs what its want names, nothing to retry", module)
|
||||
case !tried:
|
||||
return fmt.Errorf("%s has no failed apply to try again: it waits for its composition, or settles on its own", module)
|
||||
@@ -1314,7 +1316,7 @@ func (a *Agent) HoldsNode() bool {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
h, ok := a.running[NodeModule]
|
||||
return ok && h.Phase != delivery.PhaseRemoved
|
||||
return ok && h.Phase != nodeengineclient.PhaseRemoved
|
||||
}
|
||||
|
||||
func (a *Agent) settleOne(ctx context.Context, module string) {
|
||||
@@ -1370,7 +1372,7 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
|
||||
}
|
||||
a.mu.Lock()
|
||||
r, runs := a.running[module]
|
||||
runsWant := runs && r.FP == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != delivery.PhaseRemoved
|
||||
runsWant := runs && r.Fp == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != nodeengineclient.PhaseRemoved
|
||||
att, tried := a.attempt[module]
|
||||
same := tried && att.FP == w.Want.FP && att.Composed == w.Want.Composed
|
||||
if same && att.Held {
|
||||
@@ -1445,7 +1447,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
a.mu.Lock()
|
||||
a.prior[module] = true
|
||||
a.mu.Unlock()
|
||||
if h.Phase == string(delivery.PhaseHeld) && h.Detail == BackupInterrupted && w.Want.Backup != delivery.BackupSkip {
|
||||
if h.Phase == string(nodeengineclient.PhaseHeld) && h.Detail == BackupInterrupted && w.Want.Backup != delivery.BackupSkip {
|
||||
// Held only because this node agent stopped while the backup ran (postgres on novox, 2026-10-31): no
|
||||
// failure and nothing for the operator to answer. It is backed up again and applied (a.prior makes the
|
||||
// apply back it up first); the want still asking a backup is all the answer needed.
|
||||
@@ -1454,7 +1456,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
a.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
if h.Phase == string(delivery.PhaseHeld) && (w.Want.Backup == delivery.BackupAgain || w.Want.Backup == delivery.BackupSkip) {
|
||||
if h.Phase == string(nodeengineclient.PhaseHeld) && (w.Want.Backup == delivery.BackupAgain || w.Want.Backup == delivery.BackupSkip) {
|
||||
// Held for a failed backup, and the want already carries the operator's answer, written before this engine
|
||||
// started (nats on novox, 2026-11-04: "Back up again" answered, then the engine restarted and took the
|
||||
// answer for the hold's own revision, so it was never acted on; issue 1585). The answer is acted on now:
|
||||
@@ -1467,7 +1469,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
a.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
if h.Phase == string(delivery.PhaseHeld) {
|
||||
if h.Phase == string(nodeengineclient.PhaseHeld) {
|
||||
// Held for a failed backup, and the operator's answer is still to come: the hold is kept, nothing is
|
||||
// backed up again or applied, and the next revision of the want is the answer (ADR 0568 §4). Said, so
|
||||
// why it holds is read from the journal and not guessed (issue 1585: this branch was silent).
|
||||
@@ -1478,7 +1480,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
a.attempt[module] = attempt{FP: w.Want.FP, Composed: w.Want.Composed, WantRev: w.Rev, CompRev: comp.Rev, Held: true,
|
||||
AnsweredRev: w.Rev}
|
||||
if h.Composed != "" {
|
||||
a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning}
|
||||
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed, Phase: nodeengineclient.PhaseRunning}
|
||||
}
|
||||
a.mu.Unlock()
|
||||
a.runsChanged(module + " was taken as held")
|
||||
@@ -1495,18 +1497,18 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
h.Detail = ""
|
||||
}
|
||||
if h.FP == w.Want.FP && h.Composed == w.Want.Composed &&
|
||||
(h.Phase == "" || h.Phase == string(delivery.PhaseRunning) || marked) {
|
||||
(h.Phase == "" || h.Phase == string(nodeengineclient.PhaseRunning) || marked) {
|
||||
if a.inactiveAtStart(ctx, module, comp) {
|
||||
return false
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning}
|
||||
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed, Phase: nodeengineclient.PhaseRunning}
|
||||
a.mu.Unlock()
|
||||
a.runsChanged(module + " was taken from its have")
|
||||
// Said again, so the session holds the module and its key carries the health this engine judges,
|
||||
// not the one an older engine left there (xorg's unknown on g14 and shanks, 2026-10-25).
|
||||
a.writeHave(ctx, Have{Module: module, FP: h.FP, Composed: h.Composed,
|
||||
Phase: string(delivery.PhaseRunning), Detail: h.Detail, Sealed: h.Sealed})
|
||||
Phase: string(nodeengineclient.PhaseRunning), Detail: h.Detail, Sealed: h.Sealed})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -1529,10 +1531,10 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
return false
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.running[module] = delivery.Have{FP: w.Want.FP, Composed: w.Want.Composed, Phase: delivery.PhaseRunning}
|
||||
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: w.Want.FP, Composed: w.Want.Composed, Phase: nodeengineclient.PhaseRunning}
|
||||
a.mu.Unlock()
|
||||
a.runsChanged(module + " was taken from the kept declaration")
|
||||
a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(delivery.PhaseRunning)})
|
||||
a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(nodeengineclient.PhaseRunning)})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1567,10 +1569,10 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
||||
a.mu.Unlock()
|
||||
if !runs {
|
||||
if h, found, _ := a.readHave(ctx, module); found {
|
||||
prev = delivery.Have{FP: h.FP, Composed: h.Composed}
|
||||
prev = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed}
|
||||
}
|
||||
}
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseBackingUp)})
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseBackingUp)})
|
||||
if got := a.env.Backup(ctx, module); !got.Done {
|
||||
words := got.Words
|
||||
if got.Interrupted || ctx.Err() != nil {
|
||||
@@ -1585,12 +1587,12 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
||||
a.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Held: true, AnsweredRev: w.Rev}
|
||||
a.mu.Unlock()
|
||||
// It keeps running what it has: the have says what it holds and why it waits.
|
||||
a.writeHave(ctx, Have{Module: module, FP: prev.FP, Composed: prev.Composed, Phase: string(delivery.PhaseHeld),
|
||||
a.writeHave(ctx, Have{Module: module, FP: prev.Fp, Composed: prev.Composed, Phase: string(nodeengineclient.PhaseHeld),
|
||||
Detail: words})
|
||||
return
|
||||
}
|
||||
}
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseApplying)})
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseApplying)})
|
||||
res := a.env.Apply(ctx, comp.C, others)
|
||||
a.mu.Lock()
|
||||
if !res.Failed || res.Transient {
|
||||
@@ -1607,7 +1609,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
||||
gen := a.gen
|
||||
a.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Transient: true, Gen: gen}
|
||||
a.mu.Unlock()
|
||||
phase, when := delivery.PhaseApplying, "when the bus answers again"
|
||||
phase, when := nodeengineclient.PhaseApplying, "when the bus answers again"
|
||||
if a.env.Backoff != nil {
|
||||
wait := a.env.Backoff(try)
|
||||
when = "in " + wait.String()
|
||||
@@ -1622,7 +1624,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
||||
})
|
||||
}
|
||||
if try > transientFor {
|
||||
phase = delivery.PhaseFailed // still retried, and said as failed: no longer a moment
|
||||
phase = nodeengineclient.PhaseFailed // still retried, and said as failed: no longer a moment
|
||||
}
|
||||
a.env.Say(fmt.Sprintf("applying %s failed because the bus did not answer (try %d), retried %s: %s", what, try, when, res.Detail))
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(phase),
|
||||
@@ -1640,7 +1642,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
||||
wait := a.pullWaitOf(module)
|
||||
a.mu.Unlock()
|
||||
a.env.Say(fmt.Sprintf("applying %s failed: %s", what, res.Detail))
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed), Detail: detail,
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed), Detail: detail,
|
||||
PullWait: wait})
|
||||
if again != "" {
|
||||
a.askPullAgain(module, again)
|
||||
@@ -1650,14 +1652,14 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
||||
delete(a.pulls, module)
|
||||
delete(a.attempt, module)
|
||||
delete(a.notComposed, module) // the apply says what it runs now
|
||||
a.running[module] = delivery.Have{FP: fp, Composed: dig, Phase: delivery.PhaseRunning}
|
||||
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: dig, Phase: nodeengineclient.PhaseRunning}
|
||||
a.applied[module] = comp
|
||||
a.mu.Unlock()
|
||||
a.keepApplied(module, comp)
|
||||
if a.env.Settled != nil {
|
||||
a.env.Settled(module)
|
||||
}
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseRunning), Detail: res.Detail})
|
||||
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseRunning), Detail: res.Detail})
|
||||
}
|
||||
|
||||
func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
|
||||
@@ -1668,7 +1670,7 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if ran && r.Phase == delivery.PhaseRemoved {
|
||||
if ran && r.Phase == nodeengineclient.PhaseRemoved {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -1685,7 +1687,7 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
|
||||
delete(a.attempt, module)
|
||||
delete(a.pulls, module)
|
||||
delete(a.had, module)
|
||||
a.running[module] = delivery.Have{Phase: delivery.PhaseRemoved}
|
||||
a.running[module] = nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRemoved}
|
||||
a.mu.Unlock()
|
||||
if a.env.Applied != nil {
|
||||
if err := a.env.Applied.Drop(module); err != nil {
|
||||
@@ -1766,17 +1768,17 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
||||
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 {
|
||||
if ran && r.Composed == comp.Dig && r.Phase != nodeengineclient.PhaseRemoved {
|
||||
a.mu.Unlock()
|
||||
// Composed back to what runs: a have still waiting for a yes for a move since gone says running again, or it waits
|
||||
// for a yes nobody needs (issue 774, 2026-10-31: four providers stayed waiting-yes once their consumers composed).
|
||||
if h, ok := a.env.Haves.Read(ctx, module); ok && h.Phase == string(delivery.PhaseWaitingYes) {
|
||||
if h, ok := a.env.Haves.Read(ctx, module); ok && h.Phase == string(nodeengineclient.PhaseWaitingYes) {
|
||||
// Its sealed values only when they are of what runs; else writeHave reads them (sealedOf).
|
||||
var sealed compositions.Seals
|
||||
if h.Composed == r.Composed {
|
||||
sealed = h.Sealed
|
||||
}
|
||||
a.writeHave(ctx, Have{Module: module, FP: r.FP, Composed: r.Composed, Phase: string(delivery.PhaseRunning), Sealed: sealed})
|
||||
a.writeHave(ctx, Have{Module: module, FP: r.Fp, Composed: r.Composed, Phase: string(nodeengineclient.PhaseRunning), Sealed: sealed})
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1799,12 +1801,12 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
||||
case known:
|
||||
j = judge.JudgeMove(&from, comp.C)
|
||||
case prior:
|
||||
j = judge.Judged{Judgement: delivery.JudgedCannot, Words: "what " + module + " runs on this node cannot be read"}
|
||||
j = judge.Judged{Judgement: nodeengineclient.JudgedCannot, Words: "what " + module + " runs on this node cannot be read"}
|
||||
default:
|
||||
j = judge.JudgeMove(nil, comp.C)
|
||||
}
|
||||
switch j.Judgement {
|
||||
case delivery.JudgedRecreates, delivery.JudgedMovesData, delivery.JudgedCannot:
|
||||
case nodeengineclient.JudgedRecreates, nodeengineclient.JudgedMovesData, nodeengineclient.JudgedCannot:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -1812,7 +1814,7 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
||||
a.sayOnce("yes:"+module+":"+comp.Dig, fmt.Sprintf("%s at %s waits for a person's yes: %s", module, w.Want.FP, j.Words))
|
||||
prev := a.running[module]
|
||||
a.mu.Unlock()
|
||||
a.writeHave(ctx, Have{Module: module, FP: prev.FP, Composed: prev.Composed, Phase: string(delivery.PhaseWaitingYes),
|
||||
a.writeHave(ctx, Have{Module: module, FP: prev.Fp, Composed: prev.Composed, Phase: string(nodeengineclient.PhaseWaitingYes),
|
||||
Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.FP)})
|
||||
return true
|
||||
}
|
||||
@@ -1820,17 +1822,17 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
||||
// buildAlone says a move of module from what runs to the build want changes the build alone (ADR 0601 §1): another build than
|
||||
// the one it runs, and the build it runs composed with today's settings and secrets is the composition it runs. Anything it
|
||||
// cannot tell is no: the move is then asked of a person.
|
||||
func (a *Agent) buildAlone(ctx context.Context, module string, prev delivery.Have, want string) bool {
|
||||
if prev.FP == "" && prev.Composed == "" {
|
||||
func (a *Agent) buildAlone(ctx context.Context, module string, prev nodeengine.HaveByNodeModuleValue, want string) bool {
|
||||
if prev.Fp == "" && prev.Composed == "" {
|
||||
// Nothing of it run since this agent's start: its last have says what runs, as the have written keeps it.
|
||||
if h, ok := a.env.Haves.Read(ctx, module); ok {
|
||||
prev.FP, prev.Composed = h.FP, h.Composed
|
||||
prev.Fp, prev.Composed = h.FP, h.Composed
|
||||
}
|
||||
}
|
||||
if a.env.ComposedToday == nil || prev.FP == "" || prev.Composed == "" || prev.FP == want {
|
||||
if a.env.ComposedToday == nil || prev.Fp == "" || prev.Composed == "" || prev.Fp == want {
|
||||
return false
|
||||
}
|
||||
dig, ok := a.env.ComposedToday(ctx, module, prev.FP)
|
||||
dig, ok := a.env.ComposedToday(ctx, module, prev.Fp)
|
||||
return ok && dig == prev.Composed
|
||||
}
|
||||
|
||||
@@ -2001,11 +2003,15 @@ func (a *Agent) Settled(holder string) {
|
||||
|
||||
// pullWaitOf is the pull a failed apply of module waits on, as its have carries it (ENG-9): the image, the seat and its
|
||||
// holder, when the pull began, and the last retry with what triggered it; nil when it waits on no holder. a.mu is held.
|
||||
func (a *Agent) pullWaitOf(module string) *delivery.PullWait {
|
||||
func (a *Agent) pullWaitOf(module string) *nodeengine.PullWait {
|
||||
pw := a.pulls[module]
|
||||
if pw == nil {
|
||||
return nil
|
||||
}
|
||||
return &delivery.PullWait{Image: pw.Image, Seat: pw.Seat, Holder: pw.Holder, Began: pw.Started, Retried: pw.last,
|
||||
RetriedAt: pw.lastAt}
|
||||
out := &nodeengine.PullWait{Image: pw.Image, Seat: pw.Seat, Holder: pw.Holder, Began: pw.Started, Retried: pw.last}
|
||||
if !pw.lastAt.IsZero() {
|
||||
at := pw.lastAt
|
||||
out.RetriedAt = &at
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"maps"
|
||||
"slices"
|
||||
@@ -300,7 +302,7 @@ func TestAWantAndItsCompositionApplyTheModuleAndTheHaveNamesBoth(t *testing.T) {
|
||||
eventually(t, "the module was not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||
eventually(t, "the have does not name both", func() bool {
|
||||
h := b.haves.last("web")
|
||||
return h.FP == fp && h.Composed == dig && h.Phase == string(delivery.PhaseRunning)
|
||||
return h.FP == fp && h.Composed == dig && h.Phase == string(nodeengineclient.PhaseRunning)
|
||||
})
|
||||
// Settled is told once what runs holds the module, so a declaration read then names its files (the filling's Knock;
|
||||
// ace, 2026-10-25 21:17).
|
||||
@@ -337,7 +339,7 @@ func TestStartingOnWhatTheKeptDeclarationHoldsAppliesNothing(t *testing.T) {
|
||||
stop := b.run()
|
||||
defer stop()
|
||||
eventually(t, "the have was not written", func() bool { return b.haves.last("web").Composed == dig })
|
||||
if h := b.haves.last("web"); h.FP != fp || h.Phase != string(delivery.PhaseRunning) {
|
||||
if h := b.haves.last("web"); h.FP != fp || h.Phase != string(nodeengineclient.PhaseRunning) {
|
||||
t.Fatalf("the have: %+v", h)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
@@ -357,7 +359,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
||||
fp := fpOf("a")
|
||||
ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1"))
|
||||
wk, wraw := wantFor(t, "avahi", fp, dig, "")
|
||||
b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed),
|
||||
b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed),
|
||||
Detail: "apply: unit failed to start"}}
|
||||
b.comps.before(ck, craw)
|
||||
b.wants.before(wk, wraw)
|
||||
@@ -372,7 +374,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
||||
fp := fpOf("a")
|
||||
ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1"))
|
||||
wk, wraw := wantFor(t, "avahi", fp, dig, "")
|
||||
b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed),
|
||||
b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed),
|
||||
Detail: NotComposedDetail + "frombus: the credentials of ace: context canceled"}}
|
||||
b.comps.before(ck, craw)
|
||||
b.wants.before(wk, wraw)
|
||||
@@ -380,7 +382,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
||||
defer stop()
|
||||
eventually(t, "the mark was not cleared", func() bool {
|
||||
h := b.haves.last("avahi")
|
||||
return h.Phase == string(delivery.PhaseRunning) && h.FP == fp && h.Composed == dig && h.Detail == ""
|
||||
return h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fp && h.Composed == dig && h.Detail == ""
|
||||
})
|
||||
if got := b.appliedList(); len(got) != 0 {
|
||||
t.Fatalf("a module whose mark names what the want names was applied: %v", got)
|
||||
@@ -427,7 +429,7 @@ func testStartingOnItsOwnHaveSaysTheHaveAgainSoItsHealthIsThisEngines(t *testing
|
||||
if len(log) != 1 {
|
||||
t.Fatalf("the have was said %d times, want once: %+v", len(log), log)
|
||||
}
|
||||
if h := log[0]; h.Module != "xorg" || h.FP != fp || h.Composed != dig || h.Phase != string(delivery.PhaseRunning) || h.Detail != "kept" {
|
||||
if h := log[0]; h.Module != "xorg" || h.FP != fp || h.Composed != dig || h.Phase != string(nodeengineclient.PhaseRunning) || h.Detail != "kept" {
|
||||
t.Fatalf("the have said again: %+v", h)
|
||||
}
|
||||
if got := b.appliedList(); len(got) != 0 {
|
||||
@@ -454,7 +456,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||
eventually(t, "the have does not say failed", func() bool {
|
||||
h := b.haves.last("web")
|
||||
return h.Phase == string(delivery.PhaseFailed) && strings.Contains(h.Detail, "no space") && h.FP == fp
|
||||
return h.Phase == string(nodeengineclient.PhaseFailed) && strings.Contains(h.Detail, "no space") && h.FP == fp
|
||||
})
|
||||
// An unrelated key changes: not a retry.
|
||||
ck9, craw9, _ := composition(t, "web", fpOf("9"), false, file("web", "x", "9"))
|
||||
@@ -472,7 +474,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
t.Fatalf("a failed apply was refused a retry: %v", err)
|
||||
}
|
||||
eventually(t, "the asked retry did not apply", func() bool { return len(b.appliedList()) == 2 })
|
||||
eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(delivery.PhaseRunning) })
|
||||
eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(nodeengineclient.PhaseRunning) })
|
||||
for module, words := range map[string]string{"web": "runs what its want names", "ghost": "has no want"} {
|
||||
if err := b.ag.RetryFailed(module); err == nil || !strings.Contains(err.Error(), words) {
|
||||
t.Errorf("a retry of %s: %v, want refused saying %q", module, err, words)
|
||||
@@ -509,7 +511,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||
eventually(t, "the have does not say whom it waits on", func() bool {
|
||||
h := b.haves.last("web")
|
||||
return h.Phase == string(delivery.PhaseFailed) && strings.Contains(h.Detail, "tried again once distribution has settled")
|
||||
return h.Phase == string(nodeengineclient.PhaseFailed) && strings.Contains(h.Detail, "tried again once distribution has settled")
|
||||
})
|
||||
b.idle()
|
||||
if n := len(b.appliedList()); n != 1 {
|
||||
@@ -527,7 +529,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
eventually(t, "not tried again on the holder's settled", func() bool { return len(b.appliedList()) == 2 })
|
||||
eventually(t, "the have does not say what triggered the retry", func() bool {
|
||||
h := b.haves.last("web")
|
||||
return h.Phase == string(delivery.PhaseFailed) && strings.Contains(h.Detail, "last tried again at") && strings.Contains(h.Detail, "distribution settled")
|
||||
return h.Phase == string(nodeengineclient.PhaseFailed) && strings.Contains(h.Detail, "last tried again at") && strings.Contains(h.Detail, "distribution settled")
|
||||
})
|
||||
b.idle()
|
||||
if n := len(b.appliedList()); n != 2 {
|
||||
@@ -536,7 +538,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
// The failed have carries the pull it waits on (ENG-9, read through node_haves): the image, whose settled it waits
|
||||
// on, and what triggered the last retry.
|
||||
if w := b.haves.last("web").PullWait; w == nil || w.Holder != "distribution" || w.Image != "reg/x@sha256:1" ||
|
||||
w.Began.IsZero() || !strings.Contains(w.Retried, "distribution settled") || w.RetriedAt.IsZero() {
|
||||
w.Began.IsZero() || !strings.Contains(w.Retried, "distribution settled") || w.RetriedAt == nil {
|
||||
t.Fatalf("the failed have says it waits on %+v", w)
|
||||
}
|
||||
// The next settled answers once more, and the pull works: the module runs, and waits on nothing.
|
||||
@@ -546,7 +548,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
tick()
|
||||
b.ag.Settled("distribution")
|
||||
eventually(t, "not tried again on the next settled", func() bool { return len(b.appliedList()) == 3 })
|
||||
eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(delivery.PhaseRunning) })
|
||||
eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(nodeengineclient.PhaseRunning) })
|
||||
if w := b.haves.last("web").PullWait; w != nil {
|
||||
t.Fatalf("a running module's have still waits on %+v", w)
|
||||
}
|
||||
@@ -620,11 +622,11 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
b.ag.RetryTransient()
|
||||
}
|
||||
eventually(t, "not applied again without a new revision", func() bool { return len(b.appliedList()) == 2 })
|
||||
eventually(t, "not running", func() bool { return b.haves.last("vault").Phase == string(delivery.PhaseRunning) })
|
||||
eventually(t, "not running", func() bool { return b.haves.last("vault").Phase == string(nodeengineclient.PhaseRunning) })
|
||||
b.haves.mu.Lock()
|
||||
defer b.haves.mu.Unlock()
|
||||
for _, h := range b.haves.log {
|
||||
if h.Phase == string(delivery.PhaseFailed) {
|
||||
if h.Phase == string(nodeengineclient.PhaseFailed) {
|
||||
t.Errorf("a transient failure was written failed: %+v", h)
|
||||
}
|
||||
}
|
||||
@@ -664,7 +666,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
})
|
||||
eventually(t, "the have does not say held", func() bool {
|
||||
h := b.haves.last("db")
|
||||
return h.Phase == string(delivery.PhaseHeld) && strings.Contains(h.Detail, "repository is locked") && h.FP == fp1
|
||||
return h.Phase == string(nodeengineclient.PhaseHeld) && strings.Contains(h.Detail, "repository is locked") && h.FP == fp1
|
||||
})
|
||||
if got := b.appliedList(); len(got) != 0 {
|
||||
t.Fatalf("something was applied behind a failed backup: %v", got)
|
||||
@@ -703,7 +705,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
}
|
||||
eventually(t, "not running at the new fingerprint", func() bool {
|
||||
h := b.haves.last("db")
|
||||
return h.Phase == string(delivery.PhaseRunning) && h.FP == fp2 && h.Composed == dig2
|
||||
return h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fp2 && h.Composed == dig2
|
||||
})
|
||||
|
||||
// The node agent stopped while the backup ran, the module known only from its have: the held have names what runs
|
||||
@@ -713,7 +715,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
ib.comps.before(ck2, craw2)
|
||||
iwk, iwraw := wantFor(t, "db", fp2, dig2, delivery.BackupAgain)
|
||||
ib.wants.before(iwk, iwraw)
|
||||
ib.haves.stored = map[string]Have{"db": {Module: "db", FP: fp1, Composed: dig1, Phase: string(delivery.PhaseRunning)}}
|
||||
ib.haves.stored = map[string]Have{"db": {Module: "db", FP: fp1, Composed: dig1, Phase: string(nodeengineclient.PhaseRunning)}}
|
||||
began := make(chan struct{})
|
||||
ib.env.Backup = func(ctx context.Context, _ string) Backup {
|
||||
close(began)
|
||||
@@ -723,7 +725,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
stopI := ib.run()
|
||||
<-began
|
||||
stopI()
|
||||
if h := ib.haves.last("db"); h.Phase != string(delivery.PhaseHeld) || h.FP != fp1 || h.Composed != dig1 || h.Detail != BackupInterrupted {
|
||||
if h := ib.haves.last("db"); h.Phase != string(nodeengineclient.PhaseHeld) || h.FP != fp1 || h.Composed != dig1 || h.Detail != BackupInterrupted {
|
||||
t.Errorf("held after an interrupted backup: %+v", h)
|
||||
}
|
||||
|
||||
@@ -733,24 +735,24 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
for _, row := range []struct {
|
||||
detail, backup string
|
||||
backups, applied int
|
||||
phase delivery.Phase
|
||||
phase string
|
||||
namesNothing bool // the have names no fp and no composition
|
||||
}{
|
||||
{BackupInterrupted, delivery.BackupAgain, 1, 1, delivery.PhaseRunning, false},
|
||||
{BackupInterrupted, delivery.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
|
||||
// novox's postgres: its held have, written before mesh-host #189, names nothing it runs.
|
||||
{BackupInterrupted, "", 1, 1, delivery.PhaseRunning, true},
|
||||
{BackupInterrupted, "", 1, 1, nodeengineclient.PhaseRunning, true},
|
||||
// A backup that failed, and the want carries the operator's answer, written before this start (nats on novox,
|
||||
// 2026-11-04, issue 1585): again backs it up and applies, skip applies without one; no answer keeps the hold.
|
||||
{"restic: the repository is locked", delivery.BackupAgain, 1, 1, delivery.PhaseRunning, false},
|
||||
{"restic: the repository is locked", delivery.BackupSkip, 0, 1, delivery.PhaseRunning, false},
|
||||
{"restic: the repository is locked", "", 0, 0, delivery.PhaseHeld, false},
|
||||
{"restic: the repository is locked", delivery.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
|
||||
{"restic: the repository is locked", delivery.BackupSkip, 0, 1, nodeengineclient.PhaseRunning, false},
|
||||
{"restic: the repository is locked", "", 0, 0, nodeengineclient.PhaseHeld, false},
|
||||
} {
|
||||
r := newBed(t)
|
||||
r.comps.before(ck1, craw1)
|
||||
r.comps.before(ck2, craw2)
|
||||
wk, wraw := wantFor(t, "db", fp2, dig2, row.backup)
|
||||
r.wants.before(wk, wraw)
|
||||
held := Have{Module: "db", FP: fp1, Composed: dig1, Phase: string(delivery.PhaseHeld), Detail: row.detail}
|
||||
held := Have{Module: "db", FP: fp1, Composed: dig1, Phase: string(nodeengineclient.PhaseHeld), Detail: row.detail}
|
||||
if row.namesNothing {
|
||||
held.FP, held.Composed = "", ""
|
||||
}
|
||||
@@ -800,7 +802,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
return raw
|
||||
}
|
||||
r.wants.before(wk, selfWant(row.atStart))
|
||||
r.haves.stored = map[string]Have{"nats": {Module: "nats", FP: fp2, Composed: dig, Phase: string(delivery.PhaseHeld),
|
||||
r.haves.stored = map[string]Have{"nats": {Module: "nats", FP: fp2, Composed: dig, Phase: string(nodeengineclient.PhaseHeld),
|
||||
Detail: "the backup failed: sh: exit status 1"}}
|
||||
stopR := r.run()
|
||||
defer stopR()
|
||||
@@ -831,7 +833,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
||||
eventually(t, "the answer was not acted on", func() bool { return len(r.appliedList()) == 1 })
|
||||
eventually(t, "not running after the answer", func() bool {
|
||||
h := r.haves.last("nats")
|
||||
return h.Phase == string(delivery.PhaseRunning) && h.Composed == dig
|
||||
return h.Phase == string(nodeengineclient.PhaseRunning) && h.Composed == dig
|
||||
})
|
||||
r.mu.Lock()
|
||||
n := len(r.backups)
|
||||
@@ -959,9 +961,9 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
|
||||
if got := a.Everything(); len(got) != 2 {
|
||||
t.Fatalf("before the agent holds anything: %d compositions, want the 2 kept", len(got))
|
||||
}
|
||||
a.running["web"] = delivery.Have{FP: fpOf("a"), Composed: "sha256:" + strings.Repeat("1", 64), Phase: delivery.PhaseRunning}
|
||||
a.running["web"] = nodeengine.HaveByNodeModuleValue{Fp: fpOf("a"), Composed: "sha256:" + strings.Repeat("1", 64), Phase: nodeengineclient.PhaseRunning}
|
||||
ck, craw, dig := composition(t, "web", fpOf("a"), false, file("web", "x", "new"))
|
||||
a.running["web"] = delivery.Have{FP: fpOf("a"), Composed: dig, Phase: delivery.PhaseRunning}
|
||||
a.running["web"] = nodeengine.HaveByNodeModuleValue{Fp: fpOf("a"), Composed: dig, Phase: nodeengineclient.PhaseRunning}
|
||||
a.takeComposition(Update{Key: ck, Value: craw, Revision: 1})
|
||||
got := a.Everything()
|
||||
if len(got) != 2 {
|
||||
@@ -998,7 +1000,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
|
||||
webDig, _ := compositions.DigestOf(web)
|
||||
store.kept[NodeModule], store.kept["web"] = ownRaw, webRaw
|
||||
b.haves.Write(context.Background(), Have{Module: NodeModule, Composed: ownDig})
|
||||
b.haves.Write(context.Background(), Have{Module: "web", FP: fpOf("a"), Composed: webDig, Phase: string(delivery.PhaseRunning)})
|
||||
b.haves.Write(context.Background(), Have{Module: "web", FP: fpOf("a"), Composed: webDig, Phase: string(nodeengineclient.PhaseRunning)})
|
||||
// The composer's first pass reads what runs when its watch begins, before the agent reads any have.
|
||||
firstCh := make(chan []compositions.Composed, 1)
|
||||
comps := b.comps
|
||||
@@ -1143,7 +1145,7 @@ func TestAFailedApplyIsAppliedAgainAfterARestartWhateverIsKept(t *testing.T) {
|
||||
b.comps.before(ck, craw)
|
||||
b.wants.before(wk, wraw)
|
||||
b.kept = map[string][]json.RawMessage{"web": {file("web", "x", "1")}}
|
||||
b.haves.stored = map[string]Have{"web": {Module: "web", FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed)}}
|
||||
b.haves.stored = map[string]Have{"web": {Module: "web", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed)}}
|
||||
stop := b.run()
|
||||
defer stop()
|
||||
eventually(t, "a failed apply was marked running from the kept declaration", func() bool { return len(b.appliedList()) == 1 })
|
||||
@@ -1425,7 +1427,7 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
|
||||
{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", 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"},
|
||||
Phase: string(nodeengineclient.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 }},
|
||||
@@ -1583,7 +1585,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
// names what runs; composed again, the have is what it runs. web was applied by this agent; idle was not since the
|
||||
// agent's start (the switch and every delivery of the node-engine restart it), so what runs is what its have said.
|
||||
idleFP, idleDig := fpOf("c"), "sha256:"+strings.Repeat("9", 64)
|
||||
b.haves.Write(context.Background(), Have{Module: "idle", FP: idleFP, Composed: idleDig, Phase: string(delivery.PhaseRunning),
|
||||
b.haves.Write(context.Background(), Have{Module: "idle", FP: idleFP, Composed: idleDig, Phase: string(nodeengineclient.PhaseRunning),
|
||||
Sealed: compositions.Seals{"idle.key#sealed": `{"box":"idle"}`}})
|
||||
want("idle", idleFP, "")
|
||||
eventually(t, "idle's want was not taken", func() bool {
|
||||
@@ -1599,7 +1601,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
module, fp, composed, slot, sealed string
|
||||
}{{"web", fp, dig, "web.pw#sealed", "1"}, {"idle", idleFP, idleDig, "idle.key#sealed", `{"box":"idle"}`}} {
|
||||
h := b.haves.last(row.module)
|
||||
if h.Phase != string(delivery.PhaseFailed) || h.FP != row.fp || h.Composed != row.composed ||
|
||||
if h.Phase != string(nodeengineclient.PhaseFailed) || h.FP != row.fp || h.Composed != row.composed ||
|
||||
len(h.Sealed) != 1 || h.Sealed[row.slot] != row.sealed || !strings.Contains(h.Detail, "web's password") {
|
||||
t.Errorf("%s's have while the node cannot compose: %+v", row.module, h)
|
||||
}
|
||||
@@ -1614,7 +1616,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
want("fresh", fpOf("e"), "") // never ran, its have absent: marked, naming nothing it runs
|
||||
// A have that already says another cause keeps it (review of the switch, round 1): an apply that failed.
|
||||
brokenFP, brokenDig := fpOf("f"), "sha256:"+strings.Repeat("8", 64)
|
||||
b.haves.Write(context.Background(), Have{Module: "broken", FP: brokenFP, Composed: brokenDig, Phase: string(delivery.PhaseFailed),
|
||||
b.haves.Write(context.Background(), Have{Module: "broken", FP: brokenFP, Composed: brokenDig, Phase: string(nodeengineclient.PhaseFailed),
|
||||
Detail: "apply: unit failed to start"})
|
||||
want("broken", brokenFP, "")
|
||||
eventually(t, "unread's want was not taken", func() bool {
|
||||
@@ -1626,7 +1628,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
if h := b.haves.last("unread"); h.Module != "" {
|
||||
t.Errorf("a module whose have could not be read was marked: %+v", h)
|
||||
}
|
||||
if h := b.haves.last("fresh"); h.Phase != string(delivery.PhaseFailed) || h.FP != "" {
|
||||
if h := b.haves.last("fresh"); h.Phase != string(nodeengineclient.PhaseFailed) || h.FP != "" {
|
||||
t.Errorf("a module that never ran, its have absent, is not marked: %+v", h)
|
||||
}
|
||||
if h := b.haves.last("broken"); h.Detail != "apply: unit failed to start" || h.Composed != brokenDig {
|
||||
@@ -1642,7 +1644,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
}
|
||||
b.ag.NotComposed(context.Background(), "")
|
||||
for _, row := range []struct{ module, composed string }{{"web", dig}, {"idle", idleDig}} {
|
||||
if h := b.haves.last(row.module); h.Phase != string(delivery.PhaseRunning) || h.Composed != row.composed || len(h.Sealed) != 1 {
|
||||
if h := b.haves.last(row.module); h.Phase != string(nodeengineclient.PhaseRunning) || h.Composed != row.composed || len(h.Sealed) != 1 {
|
||||
t.Errorf("%s's have once the node composes again: %+v", row.module, h)
|
||||
}
|
||||
}
|
||||
@@ -1650,14 +1652,14 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
// novox) is marked alone, naming its own words; web, which composes, keeps running, unmarked. Composed again, idle's
|
||||
// have is what it runs.
|
||||
b.ag.NotComposedEach(context.Background(), "", map[string]string{"idle": "idle says ${config:name}, and nothing sets it"})
|
||||
if h := b.haves.last("idle"); h.Phase != string(delivery.PhaseFailed) || h.Composed != idleDig || !strings.Contains(h.Detail, "${config:name}") {
|
||||
if h := b.haves.last("idle"); h.Phase != string(nodeengineclient.PhaseFailed) || h.Composed != idleDig || !strings.Contains(h.Detail, "${config:name}") {
|
||||
t.Errorf("idle's have while it alone is held: %+v", h)
|
||||
}
|
||||
if h := b.haves.last("web"); h.Phase != string(delivery.PhaseRunning) || h.Composed != dig {
|
||||
if h := b.haves.last("web"); h.Phase != string(nodeengineclient.PhaseRunning) || h.Composed != dig {
|
||||
t.Errorf("web, which composes, is marked beside idle: %+v", h)
|
||||
}
|
||||
b.ag.NotComposedEach(context.Background(), "", nil)
|
||||
if h := b.haves.last("idle"); h.Phase != string(delivery.PhaseRunning) || h.Composed != idleDig {
|
||||
if h := b.haves.last("idle"); h.Phase != string(nodeengineclient.PhaseRunning) || h.Composed != idleDig {
|
||||
t.Errorf("idle's have once it composes again: %+v", h)
|
||||
}
|
||||
// A revision at the same build (a config value changed) is applied.
|
||||
@@ -1682,7 +1684,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
b.comps.put(dk, drawMoved)
|
||||
eventually(t, "db's revision did not wait for a yes", func() bool {
|
||||
h := b.haves.last("db")
|
||||
return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == ddigMoved
|
||||
return h.Phase == string(nodeengineclient.PhaseWaitingYes) && h.Waits == ddigMoved
|
||||
})
|
||||
if h := b.haves.last("db"); h.BuildAlone {
|
||||
t.Errorf("a revision at the build it runs is said to change the build alone: %+v", h)
|
||||
@@ -1692,8 +1694,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
keep(dk, draw)
|
||||
keep(dk, drawMoved)
|
||||
view, err := b.ag.Composition(context.Background(), "db", "", true)
|
||||
if err != nil || view.Have == nil || view.Have.Phase != string(delivery.PhaseWaitingYes) || view.Composition.Digest != ddigMoved ||
|
||||
view.Runs == nil || view.Runs.Composed != ddig || view.Judged.Judgement != string(delivery.JudgedRecreates) ||
|
||||
if err != nil || view.Have == nil || view.Have.Phase != string(nodeengineclient.PhaseWaitingYes) || view.Composition.Digest != ddigMoved ||
|
||||
view.Runs == nil || view.Runs.Composed != ddig || view.Judged.Judgement != string(nodeengineclient.JudgedRecreates) ||
|
||||
!view.Judged.WaitsForYes || len(view.Diff) != 1 || view.Diff[0].ID != "db.server" || view.Diff[0].Change != "changed" ||
|
||||
len(view.Revisions) != 2 {
|
||||
t.Errorf("the composition of db waiting for a yes reads %+v (%v)", view, err)
|
||||
@@ -1731,7 +1733,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
b.comps.put(dk, draw)
|
||||
eventually(t, "db's have stays waiting-yes after its composition went back to what runs", func() bool {
|
||||
h := b.haves.last("db")
|
||||
return h.Phase == string(delivery.PhaseRunning) && h.Composed == ddig && h.Waits == ""
|
||||
return h.Phase == string(nodeengineclient.PhaseRunning) && h.Composed == ddig && h.Waits == ""
|
||||
})
|
||||
if got := len(b.appliedList()); got != 4 {
|
||||
t.Fatalf("a composition equal to what runs was applied again: %d applies", got)
|
||||
@@ -1742,7 +1744,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
// node-engine restart it): its have alone says what runs. Its move waits for a yes as db's does, and its have keeps
|
||||
// what runs (fingerprint, digest, sealed values) rather than naming nothing (review of L2, round 3).
|
||||
recFP, recDig := fpOf("d"), "sha256:"+strings.Repeat("8", 64)
|
||||
b.haves.Write(context.Background(), Have{Module: "rec", FP: recFP, Composed: recDig, Phase: string(delivery.PhaseRunning),
|
||||
b.haves.Write(context.Background(), Have{Module: "rec", FP: recFP, Composed: recDig, Phase: string(nodeengineclient.PhaseRunning),
|
||||
Sealed: compositions.Seals{"rec.key#sealed": `{"box":"rec"}`}})
|
||||
rk, rraw, rdig := unsigned("rec", fpB, container("rec@sha256:"+strings.Repeat("3", 64)))
|
||||
b.comps.put(dk2, draw2)
|
||||
@@ -1757,7 +1759,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
}{{"db", fp, "", ddig2, nil, false}, {"rec", recFP, recDig, rdig, compositions.Seals{"rec.key#sealed": `{"box":"rec"}`}, true}} {
|
||||
eventually(t, row.module+"'s move did not wait for a yes", func() bool {
|
||||
h := b.haves.last(row.module)
|
||||
return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == row.waits
|
||||
return h.Phase == string(nodeengineclient.PhaseWaitingYes) && h.Waits == row.waits
|
||||
})
|
||||
h := b.haves.last(row.module)
|
||||
if h.FP != row.fp || (row.composed != "" && h.Composed != row.composed) || h.Composed == "" ||
|
||||
@@ -1775,8 +1777,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
want("rec", fpB, rdig)
|
||||
eventually(t, "the yes did not apply the move", func() bool {
|
||||
h, r := b.haves.last("db"), b.haves.last("rec")
|
||||
return len(b.appliedList()) == applied+2 && h.Phase == string(delivery.PhaseRunning) && h.FP == fpB &&
|
||||
r.Phase == string(delivery.PhaseRunning) && r.FP == fpB && r.Composed == rdig
|
||||
return len(b.appliedList()) == applied+2 && h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fpB &&
|
||||
r.Phase == string(nodeengineclient.PhaseRunning) && r.FP == fpB && r.Composed == rdig
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1816,7 +1818,7 @@ func testAnEngineStartReconcilesAModuleFromTheMachinesFacts(t *testing.T) {
|
||||
}
|
||||
if c.held {
|
||||
b.haves.stored = map[string]Have{"web": {Module: "web", FP: fpOf("0"), Composed: "sha256:" + strings.Repeat("0", 64),
|
||||
Phase: string(delivery.PhaseHeld), Detail: "restic: the repository is locked"}}
|
||||
Phase: string(nodeengineclient.PhaseHeld), Detail: "restic: the repository is locked"}}
|
||||
}
|
||||
if c.kept {
|
||||
b.kept = map[string][]json.RawMessage{"web": rs}
|
||||
@@ -1867,7 +1869,7 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
||||
stop := b.run()
|
||||
defer stop()
|
||||
eventually(t, "web was not settled", func() bool { return b.haves.last("web").Composed == digW })
|
||||
eventually(t, "db was not held", func() bool { return b.haves.last("db").Phase == string(delivery.PhaseHeld) })
|
||||
eventually(t, "db was not held", func() bool { return b.haves.last("db").Phase == string(nodeengineclient.PhaseHeld) })
|
||||
if got := b.appliedList(); len(got) != 0 {
|
||||
t.Fatalf("applied at the start: %v", got)
|
||||
}
|
||||
@@ -1882,13 +1884,13 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
||||
if got := b.appliedList(); len(got) != 0 {
|
||||
t.Fatalf("a held module and an unwanted one were applied: %v", got)
|
||||
}
|
||||
if h := b.haves.last("db"); h.Phase != string(delivery.PhaseHeld) {
|
||||
if h := b.haves.last("db"); h.Phase != string(nodeengineclient.PhaseHeld) {
|
||||
t.Fatalf("the held module's have changed: %+v", h)
|
||||
}
|
||||
|
||||
b.ag.Reconcile("web")
|
||||
eventually(t, "web was not applied again", func() bool { return len(b.appliedList()) == 1 })
|
||||
eventually(t, "web is not running again", func() bool { return b.haves.last("web").Phase == string(delivery.PhaseRunning) })
|
||||
eventually(t, "web is not running again", func() bool { return b.haves.last("web").Phase == string(nodeengineclient.PhaseRunning) })
|
||||
|
||||
b.mu.Lock()
|
||||
b.failWith = "systemctl start web.service: exit status 1"
|
||||
@@ -1896,7 +1898,7 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
||||
b.ag.Reconcile("web")
|
||||
eventually(t, "the failed re-apply did not write the have failed with its words", func() bool {
|
||||
h := b.haves.last("web")
|
||||
return h.Phase == string(delivery.PhaseFailed) && h.Detail == "systemctl start web.service: exit status 1"
|
||||
return h.Phase == string(nodeengineclient.PhaseFailed) && h.Detail == "systemctl start web.service: exit status 1"
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if got := b.appliedList(); len(got) != 2 {
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-host/internal/judge"
|
||||
)
|
||||
|
||||
@@ -147,7 +147,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
|
||||
have = &h
|
||||
phase := h.Phase
|
||||
if phase == "" {
|
||||
phase = string(delivery.PhaseRunning)
|
||||
phase = string(nodeengineclient.PhaseRunning)
|
||||
}
|
||||
view.Have = &HaveView{FP: h.FP, Composed: h.Composed, Phase: phase, Detail: h.Detail, Judgement: h.Judgement, Waits: h.Waits}
|
||||
}
|
||||
@@ -224,7 +224,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
|
||||
running = &from
|
||||
rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP}
|
||||
if ran {
|
||||
rv.FP, rv.Composed, rv.Phase = r.FP, r.Composed, string(r.Phase)
|
||||
rv.FP, rv.Composed, rv.Phase = r.Fp, r.Composed, string(r.Phase)
|
||||
}
|
||||
view.Runs = rv
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
|
||||
view.Judged = JudgedView{Judgement: string(j.Judgement), Words: j.Words}
|
||||
if a.env.SelfComposed && a.env.Policy != nil && module != NodeModule && (!w.Present || w.Want.Yes != read.Dig) {
|
||||
switch j.Judgement {
|
||||
case delivery.JudgedRecreates, delivery.JudgedMovesData, delivery.JudgedCannot:
|
||||
case nodeengineclient.JudgedRecreates, nodeengineclient.JudgedMovesData, nodeengineclient.JudgedCannot:
|
||||
view.Judged.WaitsForYes = a.env.Policy(ctx, module) == catalogue.PolicyRecord
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user