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
gates / gates (push) Successful in 35s
This commit is contained in:
@@ -5,7 +5,9 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/poweroff"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
)
|
||||
|
||||
@@ -142,6 +144,11 @@ type staleLockController struct {
|
||||
runner proxmox.Runner
|
||||
pool string // ownership registry: only members of this PVE pool are ever scanned (A1)
|
||||
logger *slog.Logger // the one success-path scan-summary line; nil = silent
|
||||
// bootTime is the host's boot time (unix seconds) and whether it could be read (R-1008). nil = the
|
||||
// kernel's btime (poweroff.ProcBtime — the one reader of /proc/stat btime this agent shares).
|
||||
bootTime func() (int64, bool)
|
||||
deadMu sync.Mutex
|
||||
deadSeen map[string]bool // UPIDs of dead tasks already logged (once per task)
|
||||
}
|
||||
|
||||
// staleLockAPI is the subset of *proxmox.Client the controller uses (kept narrow for clarity/testing).
|
||||
@@ -209,19 +216,81 @@ func (c *staleLockController) Lock(ctx context.Context, vmid int) (string, bool,
|
||||
return cfg.Lock(), cfg.OnBoot(), nil
|
||||
}
|
||||
|
||||
// BackupRunning is THE answer to „is a vzdump in flight for vmid" for all four callers (startup stale-lock
|
||||
// recovery, the guest-power watchdog, the household power-off guard, the controller supervisor): every one of
|
||||
// them reaches this method through the StaleLockController interface. R-1008: Proxmox keeps a task in
|
||||
// `tasks?source=active` after a power cut — nobody is left to end it (measured on demo-hp 2026-10-14: still
|
||||
// listed RUNNING after the boot) — so a task that STARTED BEFORE THIS BOOT is dead, not in flight.
|
||||
func (c *staleLockController) BackupRunning(ctx context.Context, vmid int) (bool, error) {
|
||||
tasks, err := c.px.ListRunningTasks(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
bt := c.bootTime
|
||||
if bt == nil {
|
||||
bt = procBootTime
|
||||
}
|
||||
boot, ok := bt()
|
||||
running, dead := vzdumpInFlight(tasks, vmid, boot, ok)
|
||||
for _, t := range dead {
|
||||
c.logDeadOnce(t, vmid, boot)
|
||||
}
|
||||
return running, nil
|
||||
}
|
||||
|
||||
// vzdumpInFlight decides, from the node's active-task list, whether a vzdump for vmid is in flight. A matching
|
||||
// task is in flight when it started at or after the host's boot; one that started before the boot is dead
|
||||
// (returned in dead, for the log). FAIL SAFE on doubt — the old answer: an unreadable boot time, or a task whose
|
||||
// start time is unknown, counts as in flight. Pure; pinned by stalelock_inflight_test.go.
|
||||
func vzdumpInFlight(tasks []proxmox.TaskStatus, vmid int, boot int64, bootOK bool) (bool, []proxmox.TaskStatus) {
|
||||
id := strconv.Itoa(vmid)
|
||||
var dead []proxmox.TaskStatus
|
||||
for _, t := range tasks {
|
||||
// A single-guest vzdump task carries the vmid in its ID field.
|
||||
if t.Type == "vzdump" && t.ID == id {
|
||||
if t.Type != "vzdump" || t.ID != id {
|
||||
continue
|
||||
}
|
||||
if !bootOK || boot <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
start := taskStart(t)
|
||||
if start <= 0 || start >= boot {
|
||||
return true, nil
|
||||
}
|
||||
dead = append(dead, t)
|
||||
}
|
||||
return false, nil
|
||||
return false, dead
|
||||
}
|
||||
|
||||
// taskStart is the task's start time: the list's starttime field, else the one encoded in its UPID; 0 = unknown.
|
||||
func taskStart(t proxmox.TaskStatus) int64 {
|
||||
if t.StartTime > 0 {
|
||||
return t.StartTime
|
||||
}
|
||||
if u, err := proxmox.ParseUPID(t.UPID); err == nil {
|
||||
return int64(u.StartTime)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// procBootTime reads the kernel's boot time through poweroff.ProcBtime (no second /proc/stat parser).
|
||||
func procBootTime() (int64, bool) {
|
||||
b, err := strconv.ParseInt(poweroff.ProcBtime(), 10, 64)
|
||||
return b, err == nil && b > 0
|
||||
}
|
||||
|
||||
func (c *staleLockController) logDeadOnce(t proxmox.TaskStatus, vmid int, boot int64) {
|
||||
c.deadMu.Lock()
|
||||
defer c.deadMu.Unlock()
|
||||
if c.deadSeen == nil {
|
||||
c.deadSeen = map[string]bool{}
|
||||
}
|
||||
if c.deadSeen[t.UPID] || c.logger == nil {
|
||||
return
|
||||
}
|
||||
c.deadSeen[t.UPID] = true
|
||||
c.logger.Warn("backup-in-flight: a vzdump task from before this boot is listed as active — it is dead, not running",
|
||||
"vmid", vmid, "upid", t.UPID, "task_start", taskStart(t), "host_boot", boot)
|
||||
}
|
||||
|
||||
func (c *staleLockController) HasVzdumpSnapshot(ctx context.Context, vmid int) (bool, error) {
|
||||
|
||||
Reference in New Issue
Block a user