check / check (push) Failing after 0s
Once the database passed about 4.5 GiB every stats request ran a COUNT(*) over each table plus a MIN/MAX union scan of both route tables, took the full timeout and returned HTTP 500, so the status page went blank. The server now keeps the last database statistics in memory and recomputes them at most once every 30 seconds; requests serve the cached copy and a stale copy triggers a single background refresh, so no request runs the scans. The route-count split is folded into the cached stats, removing the separate per-request live-route count query. The oldest/newest route timestamps now read one row from each end of the last_updated index instead of scanning both tables, and select the column directly so the driver parses it into time.Time; the old aggregate returned an untyped string that failed to scan and logged a warning every call. Model: opus-4-8
59 lines
1.8 KiB
Go
59 lines
1.8 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
|
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
|
)
|
|
|
|
// countingStatsDB embeds database.Store (left nil) and overrides only
|
|
// GetStatsContext, counting how many times it runs. The stats handlers read
|
|
// their database statistics through the cache, which calls this; every other
|
|
// Store method is unused on the stats path and would panic if called.
|
|
type countingStatsDB struct {
|
|
database.Store
|
|
calls *atomic.Int64
|
|
}
|
|
|
|
func (d countingStatsDB) GetStatsContext(_ context.Context) (database.Stats, error) {
|
|
d.calls.Add(1)
|
|
|
|
return database.Stats{}, nil
|
|
}
|
|
|
|
// TestStatsHandlersServeFromCache drives both stats handlers many times and
|
|
// checks that they answer 200 while the database statistics are computed at most
|
|
// once within the refresh interval. Before the fix each request ran the counts
|
|
// and MIN/MAX scans itself, which took the full timeout and returned 500 once
|
|
// the database grew large.
|
|
func TestStatsHandlersServeFromCache(t *testing.T) {
|
|
var calls atomic.Int64
|
|
db := countingStatsDB{calls: &calls}
|
|
s := New(db, streamer.New(logger.New(), metrics.New()), logger.New())
|
|
|
|
handlers := []http.HandlerFunc{s.handleStatusJSON(), s.handleStats()}
|
|
|
|
const iterations = 20
|
|
for _, handler := range handlers {
|
|
for range iterations {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("handler returned %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
}
|
|
}
|
|
|
|
if got := calls.Load(); got != 1 {
|
|
t.Fatalf("GetStatsContext ran %d times, want 1 within the interval", got)
|
|
}
|
|
}
|