- Use PRAGMA incremental_vacuum instead of full VACUUM - Frees ~1000 pages (~4MB) per run without blocking writes - Run every 10 minutes instead of 6 hours since it's lightweight - Set auto_vacuum=INCREMENTAL pragma for new databases - Remove blocking VACUUM on startup
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
// Package routewatch contains the database maintainer for background maintenance tasks.
|
|
package routewatch
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
)
|
|
|
|
// Database maintenance configuration constants.
|
|
const (
|
|
// vacuumInterval is how often to run incremental vacuum.
|
|
// Since incremental vacuum only frees ~1000 pages (~4MB) per run,
|
|
// we run it frequently to keep up with deletions.
|
|
vacuumInterval = 10 * time.Minute
|
|
|
|
// analyzeInterval is how often to run ANALYZE.
|
|
analyzeInterval = 1 * time.Hour
|
|
|
|
// vacuumTimeout is the max time for incremental vacuum (should be quick).
|
|
vacuumTimeout = 30 * time.Second
|
|
|
|
// analyzeTimeout is the max time for ANALYZE.
|
|
analyzeTimeout = 5 * time.Minute
|
|
)
|
|
|
|
// DBMaintainer handles background database maintenance tasks.
|
|
type DBMaintainer struct {
|
|
db database.Store
|
|
logger *slog.Logger
|
|
stopCh chan struct{}
|
|
wg sync.WaitGroup
|
|
|
|
// Stats tracking
|
|
statsMu sync.Mutex
|
|
lastVacuum time.Time
|
|
lastAnalyze time.Time
|
|
vacuumCount int
|
|
analyzeCount int
|
|
lastVacuumError error
|
|
lastAnalyzeError error
|
|
}
|
|
|
|
// NewDBMaintainer creates a new database maintainer.
|
|
func NewDBMaintainer(db database.Store, logger *slog.Logger) *DBMaintainer {
|
|
return &DBMaintainer{
|
|
db: db,
|
|
logger: logger.With("component", "db_maintainer"),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start begins the background maintenance goroutine.
|
|
func (m *DBMaintainer) Start() {
|
|
m.wg.Add(1)
|
|
go m.run()
|
|
m.logger.Info("Database maintainer started",
|
|
"vacuum_interval", vacuumInterval,
|
|
"analyze_interval", analyzeInterval,
|
|
)
|
|
}
|
|
|
|
// Stop gracefully shuts down the maintainer.
|
|
func (m *DBMaintainer) Stop() {
|
|
close(m.stopCh)
|
|
m.wg.Wait()
|
|
m.logger.Info("Database maintainer stopped")
|
|
}
|
|
|
|
// run is the main background loop.
|
|
func (m *DBMaintainer) run() {
|
|
defer m.wg.Done()
|
|
|
|
// Use different timers for each task
|
|
vacuumTimer := time.NewTimer(vacuumInterval)
|
|
analyzeTimer := time.NewTimer(analyzeInterval)
|
|
defer vacuumTimer.Stop()
|
|
defer analyzeTimer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-m.stopCh:
|
|
return
|
|
|
|
case <-vacuumTimer.C:
|
|
m.runVacuum()
|
|
vacuumTimer.Reset(vacuumInterval)
|
|
|
|
case <-analyzeTimer.C:
|
|
m.runAnalyze()
|
|
analyzeTimer.Reset(analyzeInterval)
|
|
}
|
|
}
|
|
}
|
|
|
|
// runVacuum performs an incremental vacuum operation on the database.
|
|
func (m *DBMaintainer) runVacuum() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), vacuumTimeout)
|
|
defer cancel()
|
|
|
|
m.logger.Debug("Running incremental vacuum")
|
|
startTime := time.Now()
|
|
|
|
err := m.db.Vacuum(ctx)
|
|
|
|
m.statsMu.Lock()
|
|
m.lastVacuum = time.Now()
|
|
m.lastVacuumError = err
|
|
if err == nil {
|
|
m.vacuumCount++
|
|
}
|
|
m.statsMu.Unlock()
|
|
|
|
if err != nil {
|
|
m.logger.Error("Incremental vacuum failed", "error", err, "duration", time.Since(startTime))
|
|
} else {
|
|
m.logger.Debug("Incremental vacuum completed", "duration", time.Since(startTime))
|
|
}
|
|
}
|
|
|
|
// runAnalyze performs an ANALYZE operation on the database.
|
|
func (m *DBMaintainer) runAnalyze() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), analyzeTimeout)
|
|
defer cancel()
|
|
|
|
m.logger.Info("Starting database ANALYZE")
|
|
startTime := time.Now()
|
|
|
|
err := m.db.Analyze(ctx)
|
|
|
|
m.statsMu.Lock()
|
|
m.lastAnalyze = time.Now()
|
|
m.lastAnalyzeError = err
|
|
if err == nil {
|
|
m.analyzeCount++
|
|
}
|
|
m.statsMu.Unlock()
|
|
|
|
if err != nil {
|
|
m.logger.Error("ANALYZE failed", "error", err, "duration", time.Since(startTime))
|
|
} else {
|
|
m.logger.Info("ANALYZE completed", "duration", time.Since(startTime))
|
|
}
|
|
}
|