Serve /api/v1/stats from a cache and index-scan the route timestamps (closes #27)
check / check (push) Failing after 0s
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
This commit is contained in:
@@ -980,23 +980,20 @@ func (d *Database) GetStatsContext(ctx context.Context) (Stats, error) {
|
||||
if err != nil {
|
||||
return stats, fmt.Errorf("failed to count IPv6 routes: %w", err)
|
||||
}
|
||||
stats.IPv4Routes = v4Count
|
||||
stats.IPv6Routes = v6Count
|
||||
stats.LiveRoutes = v4Count + v6Count
|
||||
|
||||
// Get oldest and newest route timestamps
|
||||
routeTimestampQuery := `
|
||||
SELECT MIN(last_updated), MAX(last_updated) FROM (
|
||||
SELECT last_updated FROM live_routes_v4
|
||||
UNION ALL
|
||||
SELECT last_updated FROM live_routes_v6
|
||||
)
|
||||
`
|
||||
var oldestRoute, newestRoute *time.Time
|
||||
err = d.db.QueryRowContext(ctx, routeTimestampQuery).Scan(&oldestRoute, &newestRoute)
|
||||
// Get oldest and newest route timestamps. Each query reads a single row from
|
||||
// one end of the last_updated index, so the cost is a log-time index lookup
|
||||
// rather than a full scan of both route tables. Selecting the last_updated
|
||||
// column directly (rather than MIN/MAX, whose result has no column type) lets
|
||||
// the driver parse the DATETIME value into time.Time; the union scan aggregate
|
||||
// used before returned an untyped string and logged a warning on every call.
|
||||
stats.OldestRoute, stats.NewestRoute, err = d.routeTimestampRange(ctx)
|
||||
if err != nil {
|
||||
// Display-only fields; log but keep the rest of the stats.
|
||||
d.logger.Warn("Failed to get route timestamps", "error", err)
|
||||
} else {
|
||||
stats.OldestRoute = oldestRoute
|
||||
stats.NewestRoute = newestRoute
|
||||
}
|
||||
|
||||
// Get prefix distribution
|
||||
@@ -1009,6 +1006,64 @@ func (d *Database) GetStatsContext(ctx context.Context) (Stats, error) {
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// routeTimestampRange returns the earliest and latest last_updated across both
|
||||
// live route tables, or nil values when both tables are empty. Each query reads
|
||||
// one row from an end of the last_updated index rather than scanning the tables.
|
||||
func (d *Database) routeTimestampRange(ctx context.Context) (oldest, newest *time.Time, err error) {
|
||||
oldestV4, ok, err := d.scanRouteTimestamp(ctx,
|
||||
"SELECT last_updated FROM live_routes_v4 ORDER BY last_updated ASC LIMIT 1")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ok {
|
||||
oldest = &oldestV4
|
||||
}
|
||||
|
||||
oldestV6, ok, err := d.scanRouteTimestamp(ctx,
|
||||
"SELECT last_updated FROM live_routes_v6 ORDER BY last_updated ASC LIMIT 1")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ok && (oldest == nil || oldestV6.Before(*oldest)) {
|
||||
oldest = &oldestV6
|
||||
}
|
||||
|
||||
newestV4, ok, err := d.scanRouteTimestamp(ctx,
|
||||
"SELECT last_updated FROM live_routes_v4 ORDER BY last_updated DESC LIMIT 1")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ok {
|
||||
newest = &newestV4
|
||||
}
|
||||
|
||||
newestV6, ok, err := d.scanRouteTimestamp(ctx,
|
||||
"SELECT last_updated FROM live_routes_v6 ORDER BY last_updated DESC LIMIT 1")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ok && (newest == nil || newestV6.After(*newest)) {
|
||||
newest = &newestV6
|
||||
}
|
||||
|
||||
return oldest, newest, nil
|
||||
}
|
||||
|
||||
// scanRouteTimestamp runs a single-row timestamp query. ok is false when the
|
||||
// table is empty. The query selects the last_updated column directly so the
|
||||
// driver parses the DATETIME value into a time.Time.
|
||||
func (d *Database) scanRouteTimestamp(ctx context.Context, query string) (ts time.Time, ok bool, err error) {
|
||||
err = d.db.QueryRowContext(ctx, query).Scan(&ts)
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return time.Time{}, false, nil
|
||||
case err != nil:
|
||||
return time.Time{}, false, err
|
||||
default:
|
||||
return ts, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpsertLiveRoute inserts or updates a live route
|
||||
func (d *Database) UpsertLiveRoute(route *LiveRoute) error {
|
||||
d.lock("UpsertLiveRoute")
|
||||
|
||||
Reference in New Issue
Block a user