v0.98.0 — R-82 Slice A fix: per-tier vzdump wait bound (the 30-minute false failure)
Found by live validation on demo-felhom, not by review. The first real PBS-targeted backup ran past the runner's hard-coded 30-minute WaitTask bound. The agent stopped waiting and recorded success=false WHILE THE VZDUMP KEPT RUNNING (still running 72 min later, 2.4 GB uploaded). Consequences: the tier stays permanently due, the next attempt collides with the guest lock the live vzdump holds, and the hub sees a DR tier that never succeeds — R-82's 'applied and empty' fault re-created by a timeout. Measured: ~33 MB/min over wg to Hetzner, so a first FULL ~10 GB snapshot projects to ~5h. - BackupTargetConfig.WaitTimeoutSeconds: per-tier bound. Primary 30m UNCHANGED (a local vzdump hanging 30m IS a real fault); additional tier 6h, sized from the measurement. - backup.NewBackupRunnerWithWait: per-instance (per-tier) bound. NewBackupRunner keeps its signature, so restore-test/selftest are untouched. - localapi.BackupTier.WaitTimeout: the fire-and-forget context is sized from the tier, not a fixed 2h. BOTH bounds had to move — a 6h runner bound under a 2h outer context reproduces the same false failure four hours later. Same direction as restore_test_pbs_restore_timeout_seconds: when in doubt wait LONGER. A slow backup is a slow backup; a false timeout is a corrupt status plus lock contention. Red-proof observed and restored; full suite green.
This commit is contained in:
@@ -391,13 +391,43 @@ type BackupTargetConfig struct {
|
||||
// (the fail-safe default, and the current behaviour for every PBS target). A PBS tier is never
|
||||
// pruned by the per-run flag regardless — see BackupRunner.localPruneSpec.
|
||||
KeepLast int `json:"keep_last"`
|
||||
// WaitTimeoutSeconds bounds how long the agent WAITS for this tier's vzdump task. 0/unset →
|
||||
// defaultExtraTierWaitTimeout.
|
||||
//
|
||||
// THIS FIELD EXISTS BECAUSE OF A LIVE FAILURE (2026-07-26, R-82 Slice A validation). The runner
|
||||
// hard-coded a 30-minute wait, which is right for a local vzdump (minutes) and badly wrong for
|
||||
// an offsite PBS backup over a home uplink: the first full ~10 GB snapshot ran past 30 min, the
|
||||
// agent gave up waiting and recorded success=false — WHILE THE BACKUP WAS STILL RUNNING. That
|
||||
// false failure is worse than a slow pass: the tier stays "due", a retry collides with the
|
||||
// guest lock vzdump still holds, and the hub sees a DR tier that never succeeds.
|
||||
//
|
||||
// Same reasoning as RestoreTestPBSRestoreTimeoutSeconds on the restore side, and the same
|
||||
// direction: when in doubt wait LONGER. A slow backup is a slow backup; a false timeout is a
|
||||
// corrupt status plus lock contention.
|
||||
WaitTimeoutSeconds int `json:"wait_timeout_seconds"`
|
||||
}
|
||||
|
||||
// Per-tier vzdump wait bounds.
|
||||
//
|
||||
// The PRIMARY keeps the historical 30 minutes: it is the local tier, a local vzdump takes minutes,
|
||||
// and one hanging 30 minutes is a genuine fault worth surfacing. Unchanged behaviour.
|
||||
//
|
||||
// An ADDITIONAL tier is by construction the offsite/WAN one in this design, where the binding
|
||||
// constraint is uplink speed, not health. 6h covers a first FULL snapshot of a ~10 GB guest at the
|
||||
// ~30 Mbit/s measured on demo-felhom (that first backup alone projects to ~5.5h); later
|
||||
// incrementals are far quicker. Sized from the measurement, not guessed.
|
||||
const (
|
||||
defaultPrimaryTierWaitTimeout = 30 * time.Minute
|
||||
defaultExtraTierWaitTimeout = 6 * time.Hour
|
||||
)
|
||||
|
||||
// BackupTier is a RESOLVED backup tier: one target, its own cadence, its own retention. The agent
|
||||
// builds one runner per tier from these.
|
||||
type BackupTier struct {
|
||||
TargetID string
|
||||
Cadence time.Duration
|
||||
// WaitTimeout bounds the wait on this tier's vzdump task (see WaitTimeoutSeconds).
|
||||
WaitTimeout time.Duration
|
||||
// KeepLast is the per-run prune keep-last; 0 means DO NOT PRUNE this tier.
|
||||
KeepLast int
|
||||
// Primary marks the tier that the UNTARGETED local-API endpoints act on — the pre-R-82 tier.
|
||||
@@ -421,10 +451,11 @@ type BackupTier struct {
|
||||
// - Duplicate extras are rejected after the first.
|
||||
func (b BackupConfig) BackupTiers() ([]BackupTier, []string) {
|
||||
primary := BackupTier{
|
||||
TargetID: b.BackupTarget(),
|
||||
Cadence: b.BackupCadence(),
|
||||
KeepLast: b.KeepLast(),
|
||||
Primary: true,
|
||||
TargetID: b.BackupTarget(),
|
||||
Cadence: b.BackupCadence(),
|
||||
KeepLast: b.KeepLast(),
|
||||
WaitTimeout: defaultPrimaryTierWaitTimeout,
|
||||
Primary: true,
|
||||
}
|
||||
tiers := []BackupTier{primary}
|
||||
var warnings []string
|
||||
@@ -448,10 +479,15 @@ func (b BackupConfig) BackupTiers() ([]BackupTier, []string) {
|
||||
if keep < 0 {
|
||||
keep = 0
|
||||
}
|
||||
wait := defaultExtraTierWaitTimeout
|
||||
if t.WaitTimeoutSeconds > 0 {
|
||||
wait = time.Duration(t.WaitTimeoutSeconds) * time.Second
|
||||
}
|
||||
tiers = append(tiers, BackupTier{
|
||||
TargetID: id,
|
||||
Cadence: time.Duration(t.CadenceSeconds) * time.Second,
|
||||
KeepLast: keep,
|
||||
TargetID: id,
|
||||
Cadence: time.Duration(t.CadenceSeconds) * time.Second,
|
||||
KeepLast: keep,
|
||||
WaitTimeout: wait,
|
||||
})
|
||||
}
|
||||
return tiers, warnings
|
||||
|
||||
Reference in New Issue
Block a user