controller v0.296.0: a backup run cut off by a power cut or restart is said on the backup pages (R-519); the whole-system backup text states today's measurement (R-518)
gates / gates (push) Successful in 30s
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -553,6 +553,8 @@ func main() {
|
||||
backupMgr = backup.NewManager(cfg, sett, logger)
|
||||
// R-550: load the persisted restore record BEFORE any page can ask for it.
|
||||
interruptedRestore = loadRestoreRecordAtStartup(backupMgr, cfg.Paths.DataDir, logger)
|
||||
// R-519: an app-data run the stop cut off is said on the backup pages until the next complete run.
|
||||
loadRunRecordAtStartup(backupMgr, cfg.Paths.DataDir, logger)
|
||||
// R-166: use the guard that already ran Recover at startup, not a second one over the same
|
||||
// file (see SetAppStopGuard — one file, one owner).
|
||||
backupMgr.SetAppStopGuard(appStopGuard)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
)
|
||||
|
||||
// R-519 (v0.296.0) — an app-data backup run cut off by a power cut or a restart is said on the backup pages until
|
||||
// the next complete run (internal/backup/run_record.go). main() calls this right after the restore record, before
|
||||
// any page or any backup run can read the manager. Pinned by TestMainWiresRunRecord.
|
||||
|
||||
// runRecordFileName lives in DataDir, beside settings.json and the restore record.
|
||||
const runRecordFileName = "appdata-run.json"
|
||||
|
||||
func loadRunRecordAtStartup(mgr *backup.Manager, dataDir string, logger *log.Logger) time.Time {
|
||||
if mgr == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
path := filepath.Join(dataDir, runRecordFileName)
|
||||
mgr.SetRunRecordPath(path)
|
||||
cut := mgr.LoadRunRecord()
|
||||
if logger != nil {
|
||||
if !cut.IsZero() {
|
||||
logger.Printf("[WARN] [backup] the app-data backup run started %s was cut off by the stop — the backup pages say so until the next complete run (R-519)", cut.UTC().Format(time.RFC3339))
|
||||
} else {
|
||||
logger.Printf("[INFO] [backup] app-data run record wired: %s (an earlier cut still shown: %t)", path, !mgr.InterruptedRunAt().IsZero())
|
||||
}
|
||||
}
|
||||
return cut
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
)
|
||||
|
||||
// R-519: the function main() calls finds a cut run.
|
||||
func TestRunRecordAtStartup_FindsACutRun(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
before := new(backup.Manager)
|
||||
before.SetRunRecordPath(dir + "/" + runRecordFileName)
|
||||
// the record a run leaves when the power goes mid-run (markRunStarted is unexported — write its file shape)
|
||||
writeRunning(t, dir+"/"+runRecordFileName)
|
||||
after := new(backup.Manager)
|
||||
if cut := loadRunRecordAtStartup(after, dir, lg); cut.IsZero() || after.InterruptedRunAt().IsZero() {
|
||||
t.Fatal("a cut run was not found at startup")
|
||||
}
|
||||
}
|
||||
|
||||
// The seam must be CALLED from main() — an AST call, not a string (a comment would match a string).
|
||||
// RED-PROOF: delete the call in main() → this fails.
|
||||
func TestMainWiresRunRecord(t *testing.T) {
|
||||
f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
if c, ok := n.(*ast.CallExpr); ok {
|
||||
if id, ok := c.Fun.(*ast.Ident); ok && id.Name == "loadRunRecordAtStartup" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !found {
|
||||
t.Fatal("main() never calls loadRunRecordAtStartup — a cut run is never said on the page")
|
||||
}
|
||||
}
|
||||
|
||||
func writeRunning(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
b := []byte(`{"running":true,"started_at":"` + time.Date(2026, 9, 14, 19, 40, 3, 0, time.UTC).Format(time.RFC3339) + `"}`)
|
||||
if err := writeFileForTest(path, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeFileForTest(path string, b []byte) error { return os.WriteFile(path, b, 0o600) }
|
||||
Reference in New Issue
Block a user