Unified 13-month history horizon across all history tables #133
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot#133
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #22.
Retention windows currently disagree, so an older event renders with holes:
sent_alertscleared_atnws_alert_snapshotsreceived_atspc_outlook_pages(+ impacts, transitions)retained_until(= expires + 48h) + 30-day bufferretained_untilai_summary_records/_attemptscreated_atsent_alertsAn event 100 days old keeps its alert row and its snapshot but has already lost its AI summary and its SPC context. History would visibly rot in inconsistent, confusing ways.
Decision taken: one unified 13-month horizon, so year-over-year comparison ("this July vs last July") works and every product family ages out together.
Tasks
HISTORY_HORIZON_DAYS(default ~400) from which per-table windows derive. Keep per-table overrides for anything that genuinely must differ — AI payloads are large, so a shorter override with a stated reason is fine, but it must be a decision rather than an accident.sent_alertsbasis. Pruning oncleared_atmeans a row that never gets cleared is never pruned. Prune on a coalesce ofcleared_at/expires/sent_at.notification_deliveries,product_freshness_samples,sent_forecasts, andredirect_tokensall grow without limit today (retention.py:171-178).spc_outlook_pages.product_text(full narrative, stored per issuance) should be retained at full horizon or truncated — SPC reissues frequently, so this is likely the largest avoidable share of the growth.Measured production storage — 2026-07-28 14:30 UTC
Queried prod directly (
12d47dd, migration0032applied) rather than reusing the arithmetic estimates on #130/#132. Total database: 101 MB.product_freshness_samplessent_alertscleared_atspc_outlook_pagesretained_until+ 30 dnotification_deliveriessent_forecastssent_datenws_alert_snapshotsalert_radar_frameslightning_clustersredirect_tokensDisk:
radar_cache4.4 MB total, of whichalert_snapshotsis 216 kB across 31 PNGs → ~7.1 kB per radar frame.Four findings that change this issue's task list
1. Two of the four "unpruned" tables are already pruned. The task says
notification_deliveries,product_freshness_samples,sent_forecastsandredirect_tokensall grow without limit. Measured:product_freshness_samplesis pruned at 14 days, inline at observability.py:108-110 (SAMPLE_RETENTION_DAYS), not viaretention.py. Its oldest row is exactly 14.0 days old — the prune is working.sent_forecastsis pruned bysent_dateat forecast_delivery.py:45.notification_deliveries(32 rows) andredirect_tokens(0 rows) — both trivially small today.2. Task 5's hypothesis is disproved.
spc_outlook_pages.product_textaverages 2,614 bytes per issuance — 18 kB across all 7 pages. It is not "the largest avoidable share of the growth"; it is negligible. The 296 kB table is index and heap overhead, not narrative text. Truncating it would save nothing. Recommend closing that task as measured-and-rejected.3. The
cleared_atbasis bug is real and quantified. 225 of 1,708 rows (13.2%) havecleared_at IS NULLand are therefore immortal under the current basis. Under acoalesce(cleared_at, expires, sent_at)basis, 67 additional rows would be prunable at the current 90-day window today.4. The largest table is not history, and
sent_alertsis mostly bloat. Two things the estimates missed entirely:product_freshness_samplesis 62% of the database — and it is observability, not history. 11,119 rows/day, 78% of itspc_fetch(121,007 rows; thennws_alerts20,149,notifier_delivery5,937,media5,187,nws_forecast3,392). Its indexes cost as much as its data (31 MB vs 32 MB) — five indexes on a 164-byte row. Folding this into a 400-day horizon would take it from 63 MB to roughly 1.8 GB. It must stay excluded.sent_alertsholds 1,023 kB of live data in a 21 MB heap — roughly 20× bloat, from UPDATE churn (cleared_at,escalation_count,digest_pending,acknowledged_atall update in place). Last autovacuum was 2026-07-22. This is an autovacuum/VACUUM FULLmatter, not a retention one, and it is worth knowing before we raise this table's window.Correcting the estimates on #130 and #132
#130 (~165 MB/location/year) is an overestimate, by roughly 1.5–2.3×. Measured inputs: 839 dashboard records over 92 days across 5 locations = 1.82 alert events/day/location ≈ 666/year; ~7.1 kB/frame; frames average 15.5 per event against the
alert_radar_frame_max_per_event = 24cap.Both sit below 165 MB. Caveat: the frames-per-event average rests on n=2 events, since capture only ungated at 03:50 UTC today. The event rate (839 records / 92 days) is solid; the frames-per-event figure is not yet.
#132 (~25–50 MB/year/region) remains unmeasurable.
lightning_clustersis at 0 rows — the archive deployed at 14:27 UTC today andpoll_lightning_jobis running and fetching successfully, but the feed currently returns no strikes near the single opted-in location. Only 1 of 5 locations haslightning_alert_radius_miset, so archive coverage is one location until more opt in.What this does and doesn't unblock
The capture tables have 3 snapshots, 31 frames and 0 lightning clusters — 10.6 hours of history, not a storm season. So the "size the result at 13 months" task cannot be completed today for the capture tables, and I am not going to manufacture a projection that looks measured but isn't. It needs a real convective season; I'd revisit in the autumn.
What is settled enough to act on now is the structural work, which does not depend on that sizing:
HISTORY_HORIZON_DAYS(default 400) from which per-table windows derive.sent_alertsbasis →coalesce(cleared_at, expires, sent_at). Confirmed real, 225 rows affected.notification_deliveriesandredirect_tokens— the two genuinely unbounded tables.product_freshness_samplesat 14 d (observability, not history — 400 d would cost ~1.8 GB),lightning_clustersat 90 d (volume; unmeasured),public_tokensat 30 d (token hygiene, not history).One caveat on raising
sent_alerts90 → 400 d: the table then stops pruning entirely for ~10 months (its oldest row is 92 days old, sostale_400is currently 0) while continuing to accrue ~9.1 rows/day. That is the intended consequence of the 13-month decision, but combined with the 20× bloat above it means autovacuum behaviour on this table is worth watching after the change.Also worth a look (not this issue)
sent_alerts.radar_snapshot_pathis NULL on all 1,708 rows, whilealert_radar_frameshas 31. Most likely benign — both frame-bearing alerts were already active when capture ungated at 03:50, so the first-fire moment had passed and only periodic frames accrued. But if it is still 0 after a fresh alert fires post-deploy, that is a #85 regression. Flagging to re-check rather than asserting a bug.