v0.295.0: a box that was off at its backup time catches up once (R-871, decision 109); the missed-backup banner (decision 110); a late daily timer after a host suspend is skipped
gates / gates (push) Successful in 31s
gates / gates (push) Successful in 31s
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:
@@ -100,6 +100,9 @@ type Loop struct {
|
||||
windowStartFn func() string
|
||||
cadence time.Duration
|
||||
updateLegFn func() (active, stepRunning bool)
|
||||
// catchUpMu/catchUpFn (R-871, v0.295.0): a running catch-up (internal/nightchain) holds the scheduled cycle.
|
||||
catchUpMu sync.Mutex
|
||||
catchUpFn func() bool
|
||||
// mu single-flights the quiesce cycle across the scheduled loop AND the manual trigger, so the
|
||||
// two can never stop the same stacks concurrently (the persisted marker covers crash-safety across
|
||||
// restarts; this covers concurrency within the process — which a manual trigger introduces).
|
||||
@@ -235,6 +238,15 @@ func (l *Loop) runOnce(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// R-871 (v0.295.0): a catch-up of a missed night is making its database dumps and copies right now — the
|
||||
// whole-guest backup would stop the apps under it. SCHEDULED path only (TriggerNow is the household's press);
|
||||
// the catch-up waits for a quiesce the same way (nightchain.CatchUp.QuiesceBusy), so neither starves: the
|
||||
// catch-up is minutes, and this is re-asked at the next poll. Pinned by TestR871_ScheduledCycleWaitsForCatchUp.
|
||||
if l.catchUpRunning() {
|
||||
l.logger.Printf("[INFO] [quiesce] full-system backup due, but a catch-up of a missed night is running — deferring to the next poll (R-871)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Window gate (Part 3) — SCHEDULED path only. TriggerNow calls quiesceAndPoll directly and is
|
||||
// never gated. Disabled when no window fn is wired (pre-v0.168.0 behavior).
|
||||
//
|
||||
@@ -842,3 +854,17 @@ func updateLegDefers(fn func() (bool, bool), now time.Time, windowStart string)
|
||||
|
||||
// legStepGraceMin bounds how long past W+5h the gate waits for a step already in flight.
|
||||
const legStepGraceMin = 30
|
||||
|
||||
// SetCatchUpFn wires the catch-up's "running" state (R-871). Safe to call after Run has started.
|
||||
func (l *Loop) SetCatchUpFn(fn func() bool) {
|
||||
l.catchUpMu.Lock()
|
||||
defer l.catchUpMu.Unlock()
|
||||
l.catchUpFn = fn
|
||||
}
|
||||
|
||||
func (l *Loop) catchUpRunning() bool {
|
||||
l.catchUpMu.Lock()
|
||||
fn := l.catchUpFn
|
||||
l.catchUpMu.Unlock()
|
||||
return fn != nil && fn()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package quiesce
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-871 (v0.295.0): a scheduled whole-guest cycle waits while a catch-up of a missed night runs (it would stop the
|
||||
// apps under the catch-up's database dumps), and runs at the next poll once the catch-up has ended.
|
||||
// COMPANION RED-PROOF: drop the catchUpRunning() check in runOnce → "the backup started while a catch-up ran".
|
||||
func TestR871_ScheduledCycleWaitsForCatchUp(t *testing.T) {
|
||||
be := &fakeBackend{due: true, dueAge: i64(20 * 3600), phases: []string{"done"}}
|
||||
st := &fakeStacks{running: []string{"nextcloud"}}
|
||||
l := windowLoop(t, be, st, "02:30", atBudapest(5, 0)) // inside [W+2h, W+6h)
|
||||
running := true
|
||||
l.SetCatchUpFn(func() bool { return running })
|
||||
if err := l.runOnce(context.Background()); err != nil {
|
||||
t.Fatalf("runOnce: %v", err)
|
||||
}
|
||||
if be.startCalls != 0 || len(st.stoppedNames()) != 0 {
|
||||
t.Fatalf("the backup started while a catch-up ran: start=%d stopped=%v", be.startCalls, st.stoppedNames())
|
||||
}
|
||||
running = false
|
||||
if err := l.runOnce(context.Background()); err != nil {
|
||||
t.Fatalf("runOnce: %v", err)
|
||||
}
|
||||
if be.startCalls != 1 {
|
||||
t.Fatalf("after the catch-up the backup must run at the next poll; start=%d", be.startCalls)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user