Reconcile a module from the machine's facts at engine start and at each edge of a declared-running resource going down (issue 1957, ADR 0568)
mesh/delivery merged: source.main moved to novox/mesh-host@58b4f8b6bc07
mesh/check being carried to bf667a602d21 on 58b4f8b6bc07 | from 9a8b3870bad0
mesh/delivery merged: source.main moved to novox/mesh-host@58b4f8b6bc07
mesh/check being carried to bf667a602d21 on 58b4f8b6bc07 | from 9a8b3870bad0
A reboot left the have running, so a unit that failed to start at boot was never started again and nothing said so. Baseline now asks the machine; the judge's look calls Agent.Reconcile once per down edge, leaving held and unwanted modules alone.
This commit is contained in:
+61
-1
@@ -118,6 +118,11 @@ type Env struct {
|
||||
// Recorded says the engine holds a record of every resource of the composition, so it was applied and not removed
|
||||
// since; nil says yes. A composition the kept declaration or a have says runs is applied again when it says no.
|
||||
Recorded func(ctx context.Context, c delivery.Composed) bool
|
||||
// Inactive says a resource the composition declares running is not active on the machine now (a unit stopped, a
|
||||
// container not running), read from the machine and not from any record (ADR 0568 §1). A have or a kept declaration
|
||||
// that says the module runs is believed at the agent's start only when this says no; nil says no, and an unreadable
|
||||
// machine says no (unknown is not inactive).
|
||||
Inactive func(ctx context.Context, c delivery.Composed) bool
|
||||
// Revisions are a composition key's revisions the bucket keeps, oldest first; nil reads none. The composition a want
|
||||
// names is taken from them when a newer one replaced it under the same key (ADR 0539 clause 1).
|
||||
Revisions func(ctx context.Context, key string) ([]Update, error)
|
||||
@@ -167,6 +172,8 @@ type Agent struct {
|
||||
// composition is held out of the node's own apply until a composition of its own is handed (heldOut).
|
||||
heldAlone map[string]bool
|
||||
retry chan []string
|
||||
// reconcile carries the judge's edges: a resource found not active (ADR 0568 §2).
|
||||
reconcile chan []string
|
||||
// tries counts the transient failures in a row of each module; again carries a retry whose wait ran out, and
|
||||
// transient asks for every transiently failed module at once. stopped is closed when Run returns.
|
||||
tries map[string]int
|
||||
@@ -213,7 +220,7 @@ func New(env Env) *Agent {
|
||||
}
|
||||
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, applied: map[string]compNow{},
|
||||
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), tries: map[string]int{}, again: make(chan againAsk, 64),
|
||||
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{})}
|
||||
}
|
||||
|
||||
@@ -245,6 +252,17 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
dirty[m] = true
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case modules := <-a.reconcile:
|
||||
a.mu.Lock()
|
||||
for _, m := range modules {
|
||||
if w := a.wants[m]; !w.Present || w.Want.Removed || a.attempt[m].Held {
|
||||
continue
|
||||
}
|
||||
delete(a.running, m)
|
||||
delete(a.attempt, m)
|
||||
dirty[m] = true
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case ask := <-a.again:
|
||||
// The wait of a transiently failed apply ran out: tried again from the keys it has, if nothing tried it since.
|
||||
a.mu.Lock()
|
||||
@@ -303,6 +321,16 @@ func (a *Agent) Retry(modules ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile asks for the modules to be applied again because a resource each declares running was found not active, once
|
||||
// per edge (the judge's look, ADR 0568 §2). It is Retry, but a module whose apply is held (a failed backup) is left
|
||||
// alone, as is one the node wants no more (ADR 0568 §4). It never blocks.
|
||||
func (a *Agent) Reconcile(modules ...string) {
|
||||
select {
|
||||
case a.reconcile <- modules:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// RetryTransient asks for every module whose last apply failed transiently to be applied again now: the engine says it
|
||||
// when it knows the bus answers again (after it wrote the bus's user list and the bus reloaded its accounts). It never
|
||||
// blocks.
|
||||
@@ -1010,6 +1038,18 @@ 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) {
|
||||
// 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).
|
||||
a.mu.Lock()
|
||||
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.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
if h.Composed != "" {
|
||||
// This node-engine wrote it: what it says is what runs, and the kept declaration says nothing more. A
|
||||
// failed apply or a different composition is applied again.
|
||||
@@ -1022,6 +1062,9 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
}
|
||||
if h.FP == w.Want.FP && h.Composed == w.Want.Composed &&
|
||||
(h.Phase == "" || h.Phase == string(delivery.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.mu.Unlock()
|
||||
@@ -1047,6 +1090,9 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
if !ok || !SameResources(kept, comp.C.Resources) {
|
||||
return false
|
||||
}
|
||||
if a.inactiveAtStart(ctx, module, comp) {
|
||||
return false
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.running[module] = delivery.Have{FP: w.Want.FP, Composed: w.Want.Composed, Phase: delivery.PhaseRunning}
|
||||
a.mu.Unlock()
|
||||
@@ -1054,6 +1100,20 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
||||
return true
|
||||
}
|
||||
|
||||
// inactiveAtStart says the machine shows a resource the module declares running not active, so what a have or the kept
|
||||
// declaration says runs is not believed and the module is applied once, as for a changed want (ADR 0568 §1).
|
||||
func (a *Agent) inactiveAtStart(ctx context.Context, module string, comp compNow) bool {
|
||||
if a.env.Inactive == nil || !a.env.Inactive(ctx, comp.C) {
|
||||
return false
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.prior[module] = true
|
||||
a.sayOnce("inactive:"+module+":"+comp.Dig, fmt.Sprintf("%s is applied again: a resource it declares running is not "+
|
||||
"active on this machine", module))
|
||||
a.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []delivery.Composed) {
|
||||
fp, dig := w.Want.FP, w.Want.Composed
|
||||
a.mu.Lock()
|
||||
|
||||
@@ -342,6 +342,7 @@ func TestStartingOnWhatTheKeptDeclarationHoldsAppliesNothing(t *testing.T) {
|
||||
|
||||
// What the node-engine itself last applied is what runs after a restart, not the kept declaration.
|
||||
func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
||||
t.Run("1957: an engine start reads the machine's facts", testAnEngineStartReconcilesAModuleFromTheMachinesFacts)
|
||||
t.Run("says the have again", testStartingOnItsOwnHaveSaysTheHaveAgainSoItsHealthIsThisEngines)
|
||||
// An apply that failed is not what runs, though its have names the want's build and digest: the agent starting on
|
||||
// it applies again (review of the switch, round 1).
|
||||
@@ -431,6 +432,7 @@ func testStartingOnItsOwnHaveSaysTheHaveAgainSoItsHealthIsThisEngines(t *testing
|
||||
// A module whose kept resources differ from the composition is applied, and a failed apply is retried only by a change
|
||||
// of either key.
|
||||
func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
||||
t.Run("1957: the judge's edge reconciles, and a failed re-apply is failed", testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone)
|
||||
// A real failure is not retried by the clock, however short the backoff: only by a change of either key.
|
||||
t.Run("a real failure", func(t *testing.T) {
|
||||
b := newBed(t)
|
||||
@@ -1233,3 +1235,122 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
r.Phase == string(delivery.PhaseRunning) && r.FP == fpB && r.Composed == rdig
|
||||
})
|
||||
}
|
||||
|
||||
// 1957/AC1 and AC4 (unit; ADR 0568 §1, §4): an engine start takes a module from what the machine shows, not only from the
|
||||
// have or the kept declaration it kept. Every resource up applies nothing; one declared running and not active applies the
|
||||
// module once, from a have or from the kept declaration; a module with no want is not looked at.
|
||||
func testAnEngineStartReconcilesAModuleFromTheMachinesFacts(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
have bool // the engine's own have says running at the want
|
||||
held bool // else its have says held, from a failed backup
|
||||
kept bool // else the kept declaration says it runs
|
||||
inactive bool
|
||||
unwanted bool
|
||||
applied int
|
||||
asked bool // the machine was looked at
|
||||
}{
|
||||
{"all up: nothing is applied", true, false, false, false, false, 0, true},
|
||||
{"a unit not active: applied once", true, false, false, true, false, 1, true},
|
||||
{"kept declaration, a unit not active: applied once", false, false, true, true, false, 1, true},
|
||||
{"kept declaration, all up: nothing is applied", false, false, true, false, false, 0, true},
|
||||
{"a held module applies nothing at start", false, true, false, true, false, 0, false},
|
||||
{"no want: nothing is looked at or applied", true, false, false, true, true, 0, false},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
b := newBed(t)
|
||||
fp := fpOf("a")
|
||||
rs := []json.RawMessage{file("web", "x", "1")}
|
||||
ck, craw, dig := composition(t, "web", fp, false, rs...)
|
||||
wk, wraw := wantFor(t, "web", fp, dig, "")
|
||||
b.comps.before(ck, craw)
|
||||
if !c.unwanted {
|
||||
b.wants.before(wk, wraw)
|
||||
}
|
||||
if c.have {
|
||||
b.haves.stored = map[string]Have{"web": {Module: "web", FP: fp, Composed: dig, Phase: "running"}}
|
||||
}
|
||||
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"}}
|
||||
}
|
||||
if c.kept {
|
||||
b.kept = map[string][]json.RawMessage{"web": rs}
|
||||
}
|
||||
var mu sync.Mutex
|
||||
asked := false
|
||||
b.env.Inactive = func(context.Context, delivery.Composed) bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
asked = true
|
||||
return c.inactive
|
||||
}
|
||||
stop := b.run()
|
||||
defer stop()
|
||||
if !c.unwanted && !c.held {
|
||||
eventually(t, "the module was not settled", func() bool { return b.haves.last("web").Composed == dig })
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if got := b.appliedList(); len(got) != c.applied {
|
||||
t.Fatalf("applied %v, want %d", got, c.applied)
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if asked != c.asked {
|
||||
t.Fatalf("the machine was asked: %v, want %v", asked, c.asked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 1957/AC2, AC3 and AC4 (unit; ADR 0568 §2, §3, §4): the judge's edge, Reconcile, applies a running module again; one held
|
||||
// for a failed backup and one with no want are left alone; and an apply that fails writes the have `failed` with the
|
||||
// apply's own words.
|
||||
func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(t *testing.T) {
|
||||
b := newBed(t)
|
||||
fp := fpOf("a")
|
||||
ckW, crawW, digW := composition(t, "web", fp, false, file("web", "x", "1"))
|
||||
wkW, wrawW := wantFor(t, "web", fp, digW, "")
|
||||
ckD, crawD, digD := composition(t, "db", fp, true, file("db", "x", "1"))
|
||||
wkD, wrawD := wantFor(t, "db", fp, digD, "")
|
||||
b.comps.before(ckW, crawW)
|
||||
b.wants.before(wkW, wrawW)
|
||||
b.comps.before(ckD, crawD)
|
||||
b.wants.before(wkD, wrawD)
|
||||
b.haves.stored = map[string]Have{"web": {Module: "web", FP: fp, Composed: digW, Phase: "running"},
|
||||
"db": {Module: "db", FP: fpOf("0"), Composed: "sha256:" + strings.Repeat("0", 64), Phase: "running"}}
|
||||
b.backup = func(string) Backup { return Backup{Words: "restic: the repository is locked"} }
|
||||
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) })
|
||||
if got := b.appliedList(); len(got) != 0 {
|
||||
t.Fatalf("applied at the start: %v", got)
|
||||
}
|
||||
|
||||
b.ag.Reconcile("db", "ghost")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
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) {
|
||||
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) })
|
||||
|
||||
b.mu.Lock()
|
||||
b.failWith = "systemctl start web.service: exit status 1"
|
||||
b.mu.Unlock()
|
||||
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"
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if got := b.appliedList(); len(got) != 2 {
|
||||
t.Fatalf("a failed apply was tried again with no edge: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user