R-518 option A: one stop per backup tier (09 §3 decision 156)

Each quiesce window now backs up ONLY the first due tier that starts and
resumes the apps at its snapshotted; the upload finishes with the apps
running. Any other due tier stays due and a later cycle (the next poll)
takes it in its own short window - never straight after the first.
This reverses R-82's 'ONE quiesce window for both due tiers'.

A manual press (TriggerNow) backs up the primary (local) tier only, picked
by the agent's Primary flag; the off-site tier follows at the next
scheduled night run. If no available tier is flagged primary, the first
available tier is backed up rather than nothing.

Unchanged: marker written before any stop, one unquiesce per window, the
max-quiesce bound, BUSY/start-error handling (the next tier is still tried
in the same window when a tier does not start), breaker and contention.

Tests: TestBothTiersDue_ExactlyOneQuiesceWindow ->
TestBothTiersDue_FirstCycleRunsOnlyFirstTier; TestNonLastTierSnapshot_
DoesNotResumeApp -> TestFirstTierSnapshot_ResumesAppWhileUploadContinues
(red on old code); new TestManualPress_RunsOnlyLocalTier (red on old
code), TestLeftoverTier_RunsInNextCycleInItsOwnWindow,
TestManualRunTiers_NoPrimaryAvailable_BacksUpFirstAvailable;
TestNotify_BothFailingTiersAreReported now runs two cycles.

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:
2026-10-06 14:59:48 +02:00
parent 31242786af
commit d8c2634472
5 changed files with 269 additions and 102 deletions
@@ -79,3 +79,15 @@ func TestManualRun_UnknownStorageNotSkipped(t *testing.T) {
t.Fatalf("a tier without an absent verdict was dropped: %v", got)
}
}
// `09` §3 decision 156: a press backs up ONE tier. When no available tier is flagged primary (the
// primary's storage is absent), the first available tier is backed up — never nothing.
func TestManualRunTiers_NoPrimaryAvailable_BacksUpFirstAvailable(t *testing.T) {
be := newTierBackend()
be.tiers = []BackupTier{{Target: "local", Primary: true, StorageAbsent: true}, {Target: "felhom-pbs"}, {Target: "other"}}
l := newTierLoop(t, be, &fakeStacks{}, nil)
got := l.manualRunTiers(context.Background())
if len(got) != 1 || got[0].target != "felhom-pbs" {
t.Fatalf("want exactly the first available tier, got %+v", got)
}
}