Aggregate metrics into per-minute rollups for fast, small stats#13
Conversation
The metrics table grew to ~7GB (one raw row per tick, ~11.9M/month kept for 90 days) and `hal stats` took minutes because p99 was computed by loading every matching row into Go and sorting it. Store per-minute rollups (count, sum, and a mergeable log-scale histogram) and query those for the hour/day/month windows, keeping raw points only for the high-resolution "last minute" view: - store: add MetricRollup model + a DDSketch-style histogram (fixed bucket boundaries, so per-minute histograms sum exactly and a quantile over any window is a true quantile accurate to ~2% relative error). - metrics: roll up completed minutes each minute; one-time Go backfill of historical raw (chunked); prune raw to 24h and rollups to 90d, gating raw pruning on backfill completion so history is never lost. - stats: last minute from raw (exact); longer windows merge rollup histograms and compute the p99 of the merged distribution. Stays on SQLite / the pure-static CGO_ENABLED=0 build (DuckDB would require CGO, incompatible with the scratch-based deployment image). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de75f7a090
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Fixes the issues Codex flagged on the rollup approach: - Rolling up now resumes from a watermark (the latest existing rollup) up to the current minute, in day chunks. This single mechanism does the initial backfill, fills gaps after downtime, and does steady-state per-minute rollups, so a long backfill can no longer leave permanent holes between the backfill window and the incremental lookback. - A failed rollup pass changes nothing and is retried on the next tick (the watermark doesn't advance), instead of permanently disabling raw pruning via a one-shot flag. - Raw pruning is gated on the watermark reaching the raw cut-off, so raw is never deleted before it has been captured in rollups, and pruning resumes automatically once rollups catch up. - stats now serves hour/day/month from rollups PLUS the not-yet-rolled raw tail, and falls back to raw entirely when no rollups cover the window, so standalone/upgraded `hal stats` and the current minute are reported correctly rather than as 0. - Add PRAGMA busy_timeout so the rollup loop, async writer, and separate CLI processes wait for the write lock instead of erroring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the Codex findings in 254c1f7. Summary of how each is handled: P1 — gap between backfill and incremental rollups: Replaced the separate backfill + fixed 5-minute lookback with a single watermark-based P1 — backfill error permanently disabled raw pruning: Removed the one-shot P2 — stats report 0 before rollups exist: P2 — longer windows excluded the current minute: hour/day/month now combine rollups with the not-yet-rolled raw tail (everything after the latest rolled minute). The watermark guarantees the tail never overlaps a rollup, so no double-counting. Added Also added
|
Problem
metricstable growing too largetick_processing_time≈ 11.9M rows/month), kept for 90 days.stats.gocomputed p99 by loading every matching row into Go and sorting it in memory (the 151s query in the report).Fix
Store per-minute rollups and serve long-range queries from them, keeping raw points only for the high-resolution "last minute" view.
store/histogram.go— a DDSketch-style log-scale histogram. Bucket boundaries aregamma^i, fixed for all time, so per-minute histograms sum exactly and a quantile over any window is a true quantile accurate to ~2% relative error (HistogramGamma = 1.0408). This is what makes the p99 correct rather than a proxy —MAX(per-minute p99)was rejected as it converges to "worst single minute," not a percentile.store/models.go—MetricRollupstorescount,sum,histogram(per-minute, retained 90d, ~260k rows total).metrics/service.go— roll up completed minutes each minute; one-time Go backfill of historical raw (chunked by day, since histograms can't be built in SQL); prune raw to 24h and rollups to 90d, with raw pruning gated on backfill completion so history is never deleted before it's captured.cmd/hal/commands/stats.go— last minute from raw (exact); hour/day/month merge the window's rollup histograms and compute the p99 of the merged distribution.Notes
VACUUMthen reclaims the file.HistogramGamma.