Re-vendor the shared files from sneak/prompts at 0653579 (closes #52)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this repository's own entries after the shared content. Lint (golangci-lint v2.14.0) and test are Dockerfile phases that script/lint and script/test build alone with a new CACHEBUST value, keeping Go's and golangci-lint's caches; the image build depends on both. make fmt and make fmt-check also format the markdown with prettier, pinned in yarn.lock. The agent guidance moves to AGENTS.md. The Go code is fixed for what the new configuration finds, and the AS and prefix detail reads now return the rows' error after their loops. Model: opus-5-5
This commit was merged in pull request #85.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// Package routewatch contains the database maintainer for background maintenance tasks.
|
||||
package routewatch
|
||||
|
||||
import (
|
||||
@@ -64,10 +63,13 @@ func NewDBMaintainer(db database.Store, logger *slog.Logger) *DBMaintainer {
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the background maintenance goroutine.
|
||||
func (m *DBMaintainer) Start() {
|
||||
// Start begins the background maintenance goroutine. It runs until Stop:
|
||||
// the end of ctx does not stop it.
|
||||
func (m *DBMaintainer) Start(ctx context.Context) {
|
||||
m.wg.Add(1)
|
||||
go m.run()
|
||||
|
||||
go m.run(context.WithoutCancel(ctx))
|
||||
|
||||
m.logger.Info("Database maintainer started",
|
||||
"checkpoint_interval", checkpointInterval,
|
||||
"vacuum_interval", vacuumInterval,
|
||||
@@ -83,15 +85,17 @@ func (m *DBMaintainer) Stop() {
|
||||
}
|
||||
|
||||
// run is the main background loop.
|
||||
func (m *DBMaintainer) run() {
|
||||
func (m *DBMaintainer) run(ctx context.Context) {
|
||||
defer m.wg.Done()
|
||||
|
||||
// Use different timers for each task
|
||||
checkpointTimer := time.NewTimer(checkpointInterval)
|
||||
vacuumTimer := time.NewTimer(vacuumInterval)
|
||||
analyzeTimer := time.NewTimer(analyzeInterval)
|
||||
defer checkpointTimer.Stop()
|
||||
|
||||
vacuumTimer := time.NewTimer(vacuumInterval)
|
||||
defer vacuumTimer.Stop()
|
||||
|
||||
analyzeTimer := time.NewTimer(analyzeInterval)
|
||||
defer analyzeTimer.Stop()
|
||||
|
||||
for {
|
||||
@@ -100,90 +104,88 @@ func (m *DBMaintainer) run() {
|
||||
return
|
||||
|
||||
case <-checkpointTimer.C:
|
||||
m.runCheckpoint()
|
||||
m.runCheckpoint(ctx)
|
||||
checkpointTimer.Reset(checkpointInterval)
|
||||
|
||||
case <-vacuumTimer.C:
|
||||
m.runVacuum()
|
||||
m.runVacuum(ctx)
|
||||
vacuumTimer.Reset(vacuumInterval)
|
||||
|
||||
case <-analyzeTimer.C:
|
||||
m.runAnalyze()
|
||||
m.runAnalyze(ctx)
|
||||
analyzeTimer.Reset(analyzeInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runCheckpoint performs a WAL checkpoint to keep the WAL file small.
|
||||
func (m *DBMaintainer) runCheckpoint() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), checkpointTimeout)
|
||||
defer cancel()
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
err := m.db.Checkpoint(ctx)
|
||||
|
||||
m.statsMu.Lock()
|
||||
m.lastCheckpoint = time.Now()
|
||||
m.lastCheckpointError = err
|
||||
if err == nil {
|
||||
m.checkpointCount++
|
||||
}
|
||||
m.statsMu.Unlock()
|
||||
func (m *DBMaintainer) runCheckpoint(ctx context.Context) {
|
||||
duration, err := runWithTimeout(ctx, checkpointTimeout, m.db.Checkpoint)
|
||||
m.recordResult(&m.lastCheckpoint, &m.lastCheckpointError, &m.checkpointCount, err)
|
||||
|
||||
if err != nil {
|
||||
m.logger.Error("WAL checkpoint failed", "error", err, "duration", time.Since(startTime))
|
||||
m.logger.Error("WAL checkpoint failed", "error", err, "duration", duration)
|
||||
} else {
|
||||
m.logger.Debug("WAL checkpoint completed", "duration", time.Since(startTime))
|
||||
m.logger.Debug("WAL checkpoint completed", "duration", duration)
|
||||
}
|
||||
}
|
||||
|
||||
// runVacuum performs an incremental vacuum operation on the database.
|
||||
func (m *DBMaintainer) runVacuum() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), vacuumTimeout)
|
||||
defer cancel()
|
||||
|
||||
func (m *DBMaintainer) runVacuum(ctx context.Context) {
|
||||
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()
|
||||
duration, err := runWithTimeout(ctx, vacuumTimeout, m.db.Vacuum)
|
||||
m.recordResult(&m.lastVacuum, &m.lastVacuumError, &m.vacuumCount, err)
|
||||
|
||||
if err != nil {
|
||||
m.logger.Error("Incremental vacuum failed", "error", err, "duration", time.Since(startTime))
|
||||
m.logger.Error("Incremental vacuum failed", "error", err, "duration", duration)
|
||||
} else {
|
||||
m.logger.Debug("Incremental vacuum completed", "duration", time.Since(startTime))
|
||||
m.logger.Debug("Incremental vacuum completed", "duration", duration)
|
||||
}
|
||||
}
|
||||
|
||||
// runAnalyze performs an ANALYZE operation on the database.
|
||||
func (m *DBMaintainer) runAnalyze() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), analyzeTimeout)
|
||||
defer cancel()
|
||||
|
||||
func (m *DBMaintainer) runAnalyze(ctx context.Context) {
|
||||
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()
|
||||
duration, err := runWithTimeout(ctx, analyzeTimeout, m.db.Analyze)
|
||||
m.recordResult(&m.lastAnalyze, &m.lastAnalyzeError, &m.analyzeCount, err)
|
||||
|
||||
if err != nil {
|
||||
m.logger.Error("ANALYZE failed", "error", err, "duration", time.Since(startTime))
|
||||
m.logger.Error("ANALYZE failed", "error", err, "duration", duration)
|
||||
} else {
|
||||
m.logger.Info("ANALYZE completed", "duration", time.Since(startTime))
|
||||
m.logger.Info("ANALYZE completed", "duration", duration)
|
||||
}
|
||||
}
|
||||
|
||||
// runWithTimeout runs operation with a context that ends after timeout and
|
||||
// returns how long it took and its error.
|
||||
func runWithTimeout(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
operation func(context.Context) error,
|
||||
) (time.Duration, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
startTime := time.Now()
|
||||
err := operation(ctx)
|
||||
|
||||
return time.Since(startTime), err
|
||||
}
|
||||
|
||||
// recordResult stores when a maintenance task last ran and its error, and
|
||||
// counts it when it succeeded.
|
||||
func (m *DBMaintainer) recordResult(
|
||||
lastRun *time.Time, lastErr *error, successCount *int, err error,
|
||||
) {
|
||||
m.statsMu.Lock()
|
||||
defer m.statsMu.Unlock()
|
||||
|
||||
*lastRun = time.Now()
|
||||
*lastErr = err
|
||||
|
||||
if err == nil {
|
||||
*successCount++
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user