diff --git a/controller/internal/quiesce/quiesce.go b/controller/internal/quiesce/quiesce.go index 8e57eac..5944bef 100644 --- a/controller/internal/quiesce/quiesce.go +++ b/controller/internal/quiesce/quiesce.go @@ -453,24 +453,50 @@ func (l *Loop) quiesceAndPoll(ctx context.Context) error { // `09` §3 decision 156 (R-518 option A): the press makes the local copy with one short stop; the // off-site tier follows at the next scheduled night run. Pinned by TestManualPress_RunsOnlyLocalTier. // -// The primary is the tier the agent FLAGS Primary — not the first in the list. If no kept tier carries -// the flag (the primary's storage reported absent, or an agent that flags none), the first kept tier is -// backed up rather than nothing: a press that silently backs up nothing would be the applied-and-empty -// fault in a different costume. +// The primary is the tier the agent FLAGS Primary — not the first in the list. If the agent flags a primary +// but its storage is reported absent, the press backs up NOTHING and says so at ERROR: the ruling is „the +// local copy only", so the off-site tier is not a stand-in (it still runs at night), and no app is stopped +// (an empty window returns before the stop). Pinned by TestManualRunTiers_NoPrimaryAvailable_BacksUpNothing. +// An agent that flags NO tier primary at all (none advertised it) keeps the old order: the first tier. func (l *Loop) manualRunTiers(ctx context.Context) []dueTier { all := l.allTiersForManualRun(ctx) - if len(all) <= 1 { + if len(all) == 0 { return all } + if len(all) == 1 && all[0].target == "" { + return all // a pre-R-82 agent: the single untargeted tier + } for _, t := range all { if t.primary { return []dueTier{t} } } - l.logger.Printf("[WARN] [quiesce] manual run: no available tier is flagged primary — backing up tier %s (the first available)", tierLabel(all[0].target)) + if l.primaryAdvertised(ctx) { + l.logger.Printf("[ERROR] [quiesce] manual run: the local copy's storage is not available — nothing backed up now; the off-site copy follows at the night run (`09` §3 decision 156)") + return nil + } + l.logger.Printf("[WARN] [quiesce] manual run: the agent flags no tier as the local one — backing up tier %s (the first available)", tierLabel(all[0].target)) return all[:1] } +// primaryAdvertised reports whether the agent flags ANY tier primary, its storage absent or not. +func (l *Loop) primaryAdvertised(ctx context.Context) bool { + tb, ok := l.backend.(TieredBackend) + if !ok { + return false + } + tiers, err := tb.Tiers(ctx) + if err != nil { + return false + } + for _, t := range tiers { + if t.Primary { + return true + } + } + return false +} + // allTiersForManualRun lists every tier the agent can back up now: all advertised tiers whose storage // is not absent on an R-82 agent, or the single untargeted tier otherwise. manualRunTiers picks the // primary from it. diff --git a/controller/internal/quiesce/tier_skip_test.go b/controller/internal/quiesce/tier_skip_test.go index 2dbb76b..fa6a647 100644 --- a/controller/internal/quiesce/tier_skip_test.go +++ b/controller/internal/quiesce/tier_skip_test.go @@ -82,12 +82,24 @@ func TestManualRun_UnknownStorageNotSkipped(t *testing.T) { // `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) { +// `09` §3 decision 156: the press makes the LOCAL copy only. With the local storage absent it makes no copy at +// all — the off-site tier is never its stand-in (it runs at night) — and stops no app. +func TestManualRunTiers_NoPrimaryAvailable_BacksUpNothing(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) + st := &fakeStacks{} + l := newTierLoop(t, be, st, 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) + if len(got) != 0 { + t.Fatalf("want no tier (the local copy's storage is absent), got %+v", got) + } + if err := l.quiesceAndPollTiers(context.Background(), got); err != nil { + t.Fatalf("empty window: %v", err) + } + if len(be.started) != 0 { + t.Fatalf("an off-site backup was started for a manual press: %v", be.started) + } + if n := len(st.stoppedNames()); n != 0 { + t.Fatalf("%d app(s) were stopped for a press that backs up nothing", n) } }