Phase C2+C3: atomic RunMaintenance + weekly integrity_check

C2: wrap the 19-statement purge loop in a single BEGIN IMMEDIATE /
COMMIT so a mid-maintenance crash can't leave half the cache tables
purged. SetMaxOpenConns(1) guarantees the BEGIN, DELETEs, and COMMIT
all share one SQLite connection.

C3: run PRAGMA integrity_check at most once per week, gated on the
mtime of a sentinel file (.integrity_check_last) under dataPath. Any
result other than 'ok' is logged via slog.Error. The sentinel is
touched even on failure so a corrupted DB doesn't re-run the check
every cycle and spam logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-15 07:57:10 -07:00
parent ce2f59c332
commit 2907a8ac1d

View File

@@ -560,7 +560,23 @@ func RunMaintenance() {
{"market_daily_summary", `DELETE FROM market_daily_summary WHERE snapshot_date < ?`, []interface{}{now.AddDate(-1, 0, 0).Format("2006-01-02")}}, {"market_daily_summary", `DELETE FROM market_daily_summary WHERE snapshot_date < ?`, []interface{}{now.AddDate(-1, 0, 0).Format("2006-01-02")}},
} }
// Wrap the purge loop in a single BEGIN IMMEDIATE / COMMIT so a crash
// mid-maintenance can't leave half the tables purged. SetMaxOpenConns(1)
// in Init guarantees the BEGIN, the DELETEs, and the COMMIT all share
// the same connection.
totalDeleted := int64(0) totalDeleted := int64(0)
if _, err := d.Exec(`BEGIN IMMEDIATE`); err != nil {
slog.Error("maintenance: begin tx", "err", err)
return
}
committed := false
defer func() {
if !committed {
if _, err := d.Exec(`ROLLBACK`); err != nil {
slog.Error("maintenance: rollback", "err", err)
}
}
}()
for _, q := range queries { for _, q := range queries {
res, err := d.Exec(q.sql, q.args...) res, err := d.Exec(q.sql, q.args...)
if err != nil { if err != nil {
@@ -573,15 +589,73 @@ func RunMaintenance() {
totalDeleted += n totalDeleted += n
} }
} }
if _, err := d.Exec(`COMMIT`); err != nil {
slog.Error("maintenance: commit", "err", err)
return
}
committed = true
// SQLite optimization // SQLite optimization
if _, err := d.Exec(`PRAGMA optimize`); err != nil { if _, err := d.Exec(`PRAGMA optimize`); err != nil {
slog.Error("maintenance: pragma optimize", "err", err) slog.Error("maintenance: pragma optimize", "err", err)
} }
// Weekly integrity check, gated on a sentinel file's mtime under dataPath
// so it runs at most once every 7 days regardless of maintenance cadence.
runIntegrityCheckIfDue(d)
slog.Info("maintenance: complete", "total_purged", totalDeleted) slog.Info("maintenance: complete", "total_purged", totalDeleted)
} }
// runIntegrityCheckIfDue runs `PRAGMA integrity_check` at most once per week.
// Cadence is tracked via the mtime of a sentinel file under dataPath; if the
// sentinel is missing or older than 7 days, the check runs and the sentinel
// is touched. Any result other than "ok" is logged via slog.Error.
func runIntegrityCheckIfDue(d *sql.DB) {
if dataPath == "" {
return
}
sentinel := filepath.Join(dataPath, ".integrity_check_last")
if info, err := os.Stat(sentinel); err == nil {
if time.Since(info.ModTime()) < 7*24*time.Hour {
return
}
}
rows, err := d.Query(`PRAGMA integrity_check`)
if err != nil {
slog.Error("maintenance: integrity_check", "err", err)
return
}
var problems []string
for rows.Next() {
var line string
if err := rows.Scan(&line); err != nil {
slog.Error("maintenance: integrity_check scan", "err", err)
continue
}
if line != "ok" {
problems = append(problems, line)
}
}
rows.Close()
if len(problems) > 0 {
slog.Error("maintenance: integrity_check failed", "problems", problems)
} else {
slog.Info("maintenance: integrity_check ok")
}
// Touch the sentinel even on failure — otherwise a corrupted DB would
// re-run the (expensive) check every maintenance cycle and spam logs.
now := time.Now()
if err := os.WriteFile(sentinel, nil, 0o644); err != nil {
slog.Error("maintenance: write integrity sentinel", "err", err)
return
}
_ = os.Chtimes(sentinel, now, now)
}
// Exec runs a write query, logging any error with the given label. // Exec runs a write query, logging any error with the given label.
// Use for fire-and-forget writes where the error doesn't affect control flow. // Use for fire-and-forget writes where the error doesn't affect control flow.
func Exec(label, query string, args ...interface{}) { func Exec(label, query string, args ...interface{}) {