After a power cut: remove the dead half-written backup (R-1006); a backup task from before the boot is not in flight (R-1008)
gates / gates (push) Successful in 35s

This commit is contained in:
2026-10-14 11:29:41 +02:00
parent e7d4ecd607
commit a2f3aeeeff
18 changed files with 1129 additions and 40 deletions
+13 -2
View File
@@ -58,8 +58,11 @@ type BackupRunner struct {
// tier cannot have that accident — its keep_last defaults to 0 (never prune), so any value
// there is a deliberate act.
allowPBSPrune bool
logger *slog.Logger
now func() time.Time
// beforeBackup runs just before the space preflight (R-1006): it removes a dead half-written vzdump of this guest on
// this target, so a leftover from a power cut cannot hold the space this backup needs. nil = nothing.
beforeBackup func(ctx context.Context, storage string, vmid int)
logger *slog.Logger
now func() time.Time
// rejected remembers the volids already announced by warnRejectedArchiveOnce, so an incomplete
// archive is reported ONCE rather than on every 5-minute due-check. Bounded in practice: one
// entry per aborted upload, and a process restart clears it. Guarded by rejectedMu because the
@@ -102,6 +105,11 @@ func NewBackupRunnerFull(api BackupAPI, target string, mode proxmox.BackupMode,
now: func() time.Time { return time.Now().UTC() }}
}
// SetBeforeBackup sets the step run just before the space preflight (R-1006: the dead-leftover cleanup).
func (r *BackupRunner) SetBeforeBackup(fn func(ctx context.Context, storage string, vmid int)) {
r.beforeBackup = fn
}
// localPruneSpec returns the `--prune-backups` spec to apply to THIS backup, or "" to skip pruning. It
// applies the configured retention ONLY when the target is a non-PBS storage — PBS offsite retention is a
// separate lifecycle and must never be pruned by the per-run flag (§9). Fail-safe: if the target's type
@@ -183,6 +191,9 @@ func (r *BackupRunner) backup(ctx context.Context, vmid int, onSnapshot func())
// R-685 (v0.134.0): will the new archive FIT on a local target? Asked before anything runs, so a
// target that cannot hold it is a named SKIP with the numbers, not a nightly "No space left on
// device" that only the vzdump log explains (demo-hp, every night from 2026-09-23 — R-684).
if r.beforeBackup != nil {
r.beforeBackup(ctx, r.target, vmid)
}
if ok, why := r.spaceFits(ctx, vmid); !ok {
rec.Error = BackupSkipNoSpacePrefix + why
rec.DurationSeconds = time.Since(start).Seconds()