29bebbb937
gates / gates (push) Successful in 1m18s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
104 lines
3.4 KiB
Go
104 lines
3.4 KiB
Go
package backup
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// R-897 (2026-10-07): after a host restart the agent binds each drive under /mnt/felhom-drives, and when the guest
|
|
// started after that first bind it NORMALIZES it once more (umount + mount, felhom-agent localapi/intermediary.go). A
|
|
// recovery-unit capture that ran in that second failed — `mkdir /mnt/felhom-drives/hdd_1/backups: permission
|
|
// denied` — and the operator got a backup failure for a healthy app. The kernel lane restarts boxes at night, so this
|
|
// would have come after every kernel step.
|
|
//
|
|
// The rule: for DriveWaitWindow after the controller starts, a capture for an app on a drive runs only once that
|
|
// drive's bind is LIVE in this process's mount namespace (the same signal the startup app gate uses,
|
|
// web/intermediary.go driveBindLive). A periodic refresh skips the app until then (it runs every 5 minutes anyway);
|
|
// a data run waits for it. After the window the capture runs whatever the bind says, and says so once at WARN. The
|
|
// agent's bind order is unchanged. Pinned by driveready_test.go.
|
|
|
|
// DriveWaitWindow is how long after the controller starts a capture waits for a drive's bind.
|
|
const DriveWaitWindow = 10 * time.Minute
|
|
|
|
const drivesParent = "/mnt/felhom-drives/"
|
|
|
|
// driveRootOf is "/mnt/felhom-drives/<name>" for a path under it, "" for any other path (the system-data SSD).
|
|
func driveRootOf(p string) string {
|
|
if !strings.HasPrefix(p, drivesParent) {
|
|
return ""
|
|
}
|
|
name := strings.SplitN(strings.TrimPrefix(p, drivesParent), "/", 2)[0]
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
return drivesParent + name
|
|
}
|
|
|
|
// mountinfoHasTarget reports whether root is a mount target in this process's own mount namespace.
|
|
func mountinfoHasTarget(root string) bool {
|
|
data, err := os.ReadFile("/proc/self/mountinfo")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if f := strings.Fields(line); len(f) >= 5 && f[4] == root {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *Manager) now() time.Time {
|
|
if m.nowFn != nil {
|
|
return m.nowFn()
|
|
}
|
|
return time.Now()
|
|
}
|
|
|
|
// driveReady says whether a capture for an app at drivePath may run now (true outside the post-boot window, for a
|
|
// non-drive path, or once the bind is live). dataRun: wait (poll) for the bind until the window ends instead of
|
|
// skipping.
|
|
func (m *Manager) driveReady(drivePath string, dataRun bool) bool {
|
|
root := driveRootOf(drivePath)
|
|
if root == "" || m.startedAt.IsZero() {
|
|
return true
|
|
}
|
|
live := m.driveLive
|
|
if live == nil {
|
|
live = mountinfoHasTarget
|
|
}
|
|
deadline := m.startedAt.Add(DriveWaitWindow)
|
|
for {
|
|
if live(root) {
|
|
return true
|
|
}
|
|
if !m.now().Before(deadline) {
|
|
m.driveMu.Lock()
|
|
warned := m.driveWarned[root]
|
|
if m.driveWarned == nil {
|
|
m.driveWarned = map[string]bool{}
|
|
}
|
|
m.driveWarned[root] = true
|
|
m.driveMu.Unlock()
|
|
if !warned {
|
|
m.logger.Printf("[WARN] [backup] %s is still not bound live %s after the controller started — capturing anyway (R-897)",
|
|
root, DriveWaitWindow)
|
|
}
|
|
return true
|
|
}
|
|
if !dataRun {
|
|
if m.isDebug() {
|
|
m.logger.Printf("[DEBUG] [backup] recovery-unit refresh waits for %s — its bind is not live yet after the start (R-897)", root)
|
|
}
|
|
return false
|
|
}
|
|
sleep := m.driveWaitPoll
|
|
if sleep == nil {
|
|
sleep = time.Sleep
|
|
}
|
|
m.logger.Printf("[INFO] [backup] waiting for %s to be bound live before the capture (post-boot, R-897)", root)
|
|
sleep(5 * time.Second)
|
|
}
|
|
}
|