5.0 KiB
Historical stats
The rule
Historical stats never scan packet_observations per request or per refresh. They read
hourly rollup tables filled once per closed hour by one background task. Raw rows are read
only for per-entity drill-downs (a packet, a trace, a route), observer comparison and
sub-hour observer activity.
Rollup contract
- Hours.
houris the UTCdate_trunc('hour', heard_at)and leads every rollup table's primary key. Rollup tables have no foreign keys: history outlives deleted observers, nodes and IATAs. - Eligibility. Hour
[H, H+1h)can be rolled oncenow() >= H + 95 min. Ingest clampsheard_atto ±30 minutes of server time, so nothing can still land in it. - Registration. Every eligible hour since the earliest retained observation gets a row
in
analytics_rollup_hours, including hours with no traffic. Status ismissing,completeorpartial. - Rolling. One process holds the advisory lock (
analytics_rolluptask, every 5 minutes, plus a catch-up at boot). Each hour is rolled in one REPEATABLE READ transaction: copy its observations (joined to packet dimensions) into a temp table, delete and reinsert every family, hash the result, mark the hour complete. A serialization failure skips the hour until the next pass. - Revision.
analytics_state.revisionincreases when an hour's content hash changes or an hour turns partial;analytics_raw_state.coverageincreases when retention deletes rollup hours. Cache keys for rollup-backed endpoints include their sum (each counter only grows), read through a 10 s memo. - Raw deletion. Cleanup deletes packets older than
packets.retention, but holds the cutoff 35 minutes behind the oldestmissinghour, for at most 24 hours (it logs a warning past 6). Each batch advancesanalytics_raw_state.raw_deleted_beforeto the newest deletedlast_heard_at. Cleanup and the rollup never write the same row. - Partial hours. A not-yet-complete hour starting at or before
raw_deleted_before + 30 minlost raw rows before it was rolled. It is markedpartialand never rolled; readers report it without values. - Re-rolls. Hours are not re-rolled periodically. A message stored by channel backfill (at
startup, and when keys are imported at runtime) queues every hour its packet was heard in
analytics_dirty_hours, in the same statement as the message insert. Each pass re-rolls a few complete ones while their raw rows remain. A queued hour still waiting for its first roll keeps its entry unless that roll's snapshot already included it. - Retention. Rollups are kept for
analytics.rollup_retention(default 90 days), which is also the longest window/stats/seriesaccepts.
Combining hours
| Metric | Across hours and IATAs |
|---|---|
| Observations, receptions, payload counts | Sum |
| Unique packets, adverts, messages, scoped packets | Sum of hourly IATA-set counts: each is counted once per hour it was heard, so one heard across an hour boundary counts twice (measured +1.1%) |
| Active observers, IATAs, scopes | Distinct across the window |
| Active scope observers and nodes (per scope, per hour) | Distinct within the hour, across the requested IATAs |
| Max path entries | Max |
| SNR / RSSI averages | Σ sum / Σ samples, never an average of averages |
Windows snap to UTC hours on the server, and responses report the effective window.
IATA sets
A packet heard in YVR and YYZ appears in both IATAs' per-IATA rows, so summing per-IATA
distinct counts overcounts (2.26× on production data). The *_sets tables instead group
each distinct packet, advert or message under the exact sorted set of IATAs that heard it
that hour (iatas char(3)[], kept canonical by a CHECK). For any requested set S, the rows
where iatas && S count every item heard in S exactly once. This is exact for one IATA, a
region or all IATAs.
Raw or current, by design
- Packet, trace and route detail, the packet list, observer comparison, and observer activity below one hour.
- Current memberships: scope observer and node counts, node types, clock drift, radio presets.
/tracesreadstrace_tags, a per-tag summary kept at ingest, not a rollup. A new packet adds its count, type and payload in the statement that stores it; hearings set the times.
Adding a metric
- Add a family table to
db/migrations/001_baseline.sql(on 2.x, a new migration):hourfirst in the primary key, no foreign keys, and a set table withCHECK (iata_set_is_canonical(iatas))plus a GIN index if it needs distinct counts. - Add a delete in
DeleteRollupHour, aRoll*insert overrollup_obsindb/queries/rollup.sql, a line inRollupContentHashandDeleteOldRollups, and the call inrollHour(db/rollup.go). - Read it with an hour-bounded query, using the UNION-ALL IATA guard (see
series.sql) for per-IATA tables oriatas && @iatasfor set tables. - Cache it with
cachedRollupso its key carries the revision. - Add a raw-SQL oracle case to
db/rollup_integration_test.go. - Run
sqlc generate, regenerate mocks and swagger.