fix(retention): bound the five unpruned tables that filled the hosted D1 - #9415
Conversation
The hosted D1 reached its 10GB ceiling, after which every write failed with `D1_ERROR: Exceeded maximum DB size` -- including recordOrbWebhookEvent's INSERT, so /v1/orb/webhook returned 500 to GitHub and inbound webhook delivery stopped fleet-wide until rows were pruned by hand. Three compounding causes, all fixed here: - check_summaries, pull_request_files, repo_github_totals_snapshots, recent_merged_pull_requests and orb_pr_outcomes had no retention rule at all and grew without bound. check_summaries (100,459 rows / 0.30GB of payload_json) and pull_request_files (54,532 rows / 0.20GB) were the two largest single consumers in the database. - MAX_DELETED_PER_TABLE was 50k against a measured ~4.3M rows written per day, so a daily prune could delete at most ~1.25M rows/day across the whole policy -- a permanent ~3.5x deficit in which the cap, not the cutoff, was the binding constraint. Raised to 250k. - prune-retention ran once a day. It now runs hourly, so each pass stays small and the drain rate exceeds the write rate. Both webhook idempotency logs also drop 90d -> 14d; the dedup lookups they serve are minutes old at most.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-27 18:06:42 UTC
Review summary Nits — 5 non-blocking
Concerns raised — review before merging
📋 Copy for AI agents — paste into your coding agentDecision drivers
Context & advisory signals — never blocks the verdict
Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9415 +/- ##
==========================================
+ Coverage 89.52% 93.59% +4.07%
==========================================
Files 840 738 -102
Lines 109777 60119 -49658
Branches 26147 21249 -4898
==========================================
- Hits 98275 56268 -42007
+ Misses 10239 2900 -7339
+ Partials 1263 951 -312
Flags with carried forward coverage won't be shown. Click here to find out more.
|
The three scheduled-enqueue assertions in index.test.ts pin the exact job list per tick, so moving prune-retention from a 03:00-only slot to every hourly tick correctly failed them. Add it to each list at its dispatch position (after rollup-product-usage).
Summary
The hosted D1 reached its 10GB ceiling. Every write then failed with
D1_ERROR: Exceeded maximum DB size— includingrecordOrbWebhookEvent's INSERT, so/v1/orb/webhookreturned 500 to GitHub and inbound webhook delivery stopped fleet-wide until rows were pruned by hand. Reviews only kept moving because the internal regate sweep does not depend on webhooks.Three compounding causes, all addressed:
1. Five high-growth tables had no retention rule at all and grew without bound. Measured during the incident:
check_summariespull_request_filesrepo_github_totals_snapshotsrecent_merged_pull_requestsorb_pr_outcomesAll five are re-derivable GitHub mirrors or superseded snapshots, re-fetched on the next sync of the PR that needs them — never a contributor's evidentiary trail (
decision_records, 180d, remains the table carrying that).2. The per-run delete cap made retention structurally unable to keep up. At 50k/table against a measured ~4.3M rows written/day, a daily prune could delete at most ~1.25M rows/day across the whole policy — a permanent ~3.5x deficit. The cap, not the cutoff, was the binding constraint, so no window tightening alone could close it. Raised to 250k.
3. Cadence.
prune-retentionmoves from daily to hourly, so each pass stays small and the drain rate exceeds the write rate. Cheap when idle: each rule is an indexed range delete matching zero rows once a table is inside its window.Both webhook idempotency logs also drop 90d -> 14d — the dedup lookups they serve are minutes old at most, and at fleet write rates a 90-day window on a pure idempotency log was the single largest avoidable contributor.
Test plan
check_summaries(largest single consumer) exercising the real policy entry rather than an ad-hoc overrideorb_webhook_eventstest for the intentional 90d -> 14d changeseed()helper: its "recent" webhook row was anchored to a fixedNOWwhileprocessJob/the preview route evaluate against real wall-clock time, so that row survived the old 90-day window only by accident. Now anchored to real now, keeping each test's "recent rows are kept" intent independent of window width.npx vitest run test/unit/retention.test.ts— 36 passedselfhost-pg-retention,selfhost-cron-alignment,selfhost-queue-common,selfhost-pg-dialect— 119 passednpm run typecheckclean