controller (unreleased): R-921 check-first pre-check (no stop while another tier's job is in flight); R-922 email_cleared on a household's deliberate clear
gates / gates (push) Successful in 1m0s
gates / gates (push) Successful in 1m0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package quiesce
|
||||
|
||||
import "context"
|
||||
|
||||
// R-921 — CHECK FIRST, STOP SECOND.
|
||||
//
|
||||
// MEASURED 2026-10-08 night on demo-hp (felhom.eu documentation/audits/dooplex-survival-2026-10-09/partE/
|
||||
// R-518.txt): the controller stopped every app for the off-site tier, the agent refused the backup
|
||||
// („a heavy operation is already in flight", busy=backup:local — the night OS step that follows the local
|
||||
// copy), and the apps were down about a minute for no copy.
|
||||
//
|
||||
// What the agent lets the controller see BEFORE a stop (felhom-agent v0.154.0, read 2026-10-09): the agent
|
||||
// refuses POST /backup for two reasons. (1) A job of ANOTHER tier of this guest is in flight
|
||||
// (localapi otherTierInFlight) — that IS readable, per tier, from GET /backup/status?target=…. (2) Its
|
||||
// host-wide heavy-operation gate is held (backup.InFlight: the OS step after the night's local copy, a
|
||||
// restore-test, fstrim) — that is served by NO endpoint (InFlight.Busy() exists and nothing exposes it).
|
||||
// So the pre-check below covers reason (1) only. Reason (2) — the measured instance — is met by the
|
||||
// BUSY path in quiesceAndPollTiers, which resumes the apps at once (pinned by
|
||||
// TestR921_BusyRefusalResumesAtOnce); closing it before the stop needs the agent to serve its gate.
|
||||
//
|
||||
// The race (free at the check, busy at the start) needs nothing new: it is the BUSY path again.
|
||||
|
||||
// InFlightProber is the OPTIONAL pre-check surface (R-921). An adapter that does not implement it keeps
|
||||
// the pre-R-921 behaviour exactly: stop, ask, and on refusal resume at once.
|
||||
type InFlightProber interface {
|
||||
// BackupJobsInFlight returns the tiers whose backup job the agent holds in flight right now
|
||||
// (phase running or snapshotted). An agent without per-tier jobs (pre-R-82) answers nil, nil.
|
||||
BackupJobsInFlight(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// anotherTierInFlight reports whether the agent would refuse the window's first tier because a job of a
|
||||
// DIFFERENT tier is in flight — so the window must not stop any app. The window's OWN tier in flight is
|
||||
// not a refusal (the agent answers its start with the running job, 202), and that path is left as it was.
|
||||
//
|
||||
// Fail toward backing up: an unanswerable pre-check, an untargeted window (pre-R-82 agent) or an adapter
|
||||
// without the surface all return false — the window runs as before.
|
||||
func (l *Loop) anotherTierInFlight(ctx context.Context, first string) bool {
|
||||
if first == "" {
|
||||
return false
|
||||
}
|
||||
p, ok := l.backend.(InFlightProber)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
busy, err := p.BackupJobsInFlight(ctx)
|
||||
if err != nil {
|
||||
l.logger.Printf("[WARN] [quiesce] pre-check: could not ask the agent which backup jobs are in flight (%v) — stopping the apps and asking as before (R-921)", err)
|
||||
return false
|
||||
}
|
||||
for _, t := range busy {
|
||||
if t != first {
|
||||
l.logger.Printf("[INFO] [quiesce] tier %s is due, but the agent still holds a backup job on tier %s — no app is stopped; the tier stays due and is asked again at the next poll (R-921)",
|
||||
tierLabel(first), tierLabel(t))
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user