fix: retry recording when storage is slow to mount at boot
A power cut makes the recordings drive's fsck slow, so the mount can appear tens of seconds after nextnvr starts. The recorders then hit the unmounted, root-owned mountpoint, failed mkdir, and exited permanently — recording stayed dead until a manual restart. - recorder: on mkdir failure, log and retry every 15s instead of exiting, so recording resumes once storage returns - storage: add waitForStorage() and call it before the writers start, so startup waits (up to 120s) for the recordings dir to be present and writable; live view still starts if storage never appears
This commit is contained in:
parent
a54ff08ebb
commit
f794c57198
3 changed files with 51 additions and 4 deletions
11
main.go
11
main.go
|
|
@ -62,6 +62,17 @@ func main() {
|
||||||
app.go2rtc = go2rtc
|
app.go2rtc = go2rtc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for the recordings storage before starting any writers. On a slow
|
||||||
|
// boot (e.g. fsck after a power cut) the mount can appear tens of seconds
|
||||||
|
// late; this avoids the recorders failing against an unmounted, root-owned
|
||||||
|
// mountpoint. If it times out we still start — live view does not need
|
||||||
|
// storage, and the recorder retries on its own.
|
||||||
|
if st := waitForStorage(cfg.Storage.RecordingsPath, 120*time.Second); st.OK {
|
||||||
|
log.Printf("startup: storage ready — %s", cfg.Storage.RecordingsPath)
|
||||||
|
} else {
|
||||||
|
log.Printf("startup: storage NOT ready (%s) — starting anyway; recording will retry", st.Message)
|
||||||
|
}
|
||||||
|
|
||||||
// Start snapshot engine for all enabled cameras.
|
// Start snapshot engine for all enabled cameras.
|
||||||
snaps := NewSnapshotEngine(cfg.Storage)
|
snaps := NewSnapshotEngine(cfg.Storage)
|
||||||
snaps.StartAll(cfg.Cameras)
|
snaps.StartAll(cfg.Cameras)
|
||||||
|
|
|
||||||
18
recorder.go
18
recorder.go
|
|
@ -107,10 +107,6 @@ func (rm *RecorderManager) startRecorder(cam CameraConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
outDir := filepath.Join(rm.config.RecordingsPath, camDirName)
|
outDir := filepath.Join(rm.config.RecordingsPath, camDirName)
|
||||||
if err := os.MkdirAll(outDir, 0755); err != nil {
|
|
||||||
log.Printf("recorder: %s — mkdir %s failed: %v", cam.ID, outDir, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -120,6 +116,20 @@ func (rm *RecorderManager) startRecorder(cam CameraConfig) {
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the output directory exists. Storage may not be mounted yet
|
||||||
|
// (e.g. a slow boot after a power cut) or may have vanished mid-run —
|
||||||
|
// retry instead of exiting, so recording resumes automatically once
|
||||||
|
// storage returns and the mount covers the directory again.
|
||||||
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
|
log.Printf("recorder: %s — storage not ready at %s: %v — retrying in 15s", cam.ID, outDir, err)
|
||||||
|
select {
|
||||||
|
case <-proc.done:
|
||||||
|
return
|
||||||
|
case <-time.After(15 * time.Second):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Generate filename: rec_{YYYY-MM-DD-HH-MM-SS}.mp4
|
// Generate filename: rec_{YYYY-MM-DD-HH-MM-SS}.mp4
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
timeStr := now.Format("2006-01-02-15-04-05")
|
timeStr := now.Format("2006-01-02-15-04-05")
|
||||||
|
|
|
||||||
26
storage.go
26
storage.go
|
|
@ -174,3 +174,29 @@ func isMountPoint(path string) bool {
|
||||||
}
|
}
|
||||||
return cs.Dev != ps.Dev
|
return cs.Dev != ps.Dev
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitForStorage blocks until the recordings path exists and is writable, or
|
||||||
|
// until the timeout elapses. It is meant to run before the writers start: on a
|
||||||
|
// slow boot (e.g. an fsck on the recordings drive after a power cut) the mount
|
||||||
|
// can appear tens of seconds late, and without this the recorders would stumble
|
||||||
|
// against an unmounted, root-owned mountpoint. Returns the final status; if the
|
||||||
|
// timeout expires the caller may still proceed (live view does not need storage,
|
||||||
|
// and the recorder retries on its own).
|
||||||
|
func waitForStorage(path string, timeout time.Duration) StorageStatus {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
warned := false
|
||||||
|
for {
|
||||||
|
st := checkStorage(path, false)
|
||||||
|
if st.OK {
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
if !warned {
|
||||||
|
log.Printf("startup: waiting for recordings storage at %s (%s)", path, st.Message)
|
||||||
|
warned = true
|
||||||
|
}
|
||||||
|
if !time.Now().Before(deadline) {
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue