mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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:
@@ -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{}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user