Repository navigation
Keep one change-feed row per entity instead of one per webhook - #233
Conversation
The change feed inserted a row for every webhook and the sweeper deleted it 15 minutes later. In production 94% of rows repeated an entity already in the window (CI sends dozens of check_run/workflow_job deliveries per commit), and D1 billed each row six times: the insert and the delete each touched the table and two indexes. That was ~8M rows written a day for a 3.5 MB database. github_change is now keyed by (repo, entity_key) and a repeat change upserts the row, raising its version. Clients poll by version, which each write takes from MAX(version) inside its own statement, so cursors stay exact. The sweeper keeps the newest row so versions never go backwards, and a cursor ahead of the table (issued before this migration) reads as expired, so open tabs refetch their active queries once after the deploy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. Walkthrough
Merge Risk:
|
The change feed was the main D1 cost: about 8M rows written per day for a 3.5 MB database.
Why. Every webhook inserted a row, and the sweeper deleted it 15 minutes later. D1 counts each index touched as another row written, so the insert and the delete cost three rows each, six per change. A production sample from 2026-11-10 had 14,769 rows covering only 865 distinct entities: 94% were repeats, mostly CI (
check_run/workflow_jobfire dozens of times per commit). One index,(repo, id), was never read by any query.What changes
0008_github_change_latestrecreatesgithub_changekeyed by(repo, entity_key), with an index onversiononly.versionandchanged_at, which costs about 2 rows written instead of 6.versionfromMAX(version) + positioninside its own statement. D1 runs one statement at a time, so a later write always gets a version above anything a reader has seen. No clocks are involved.versioncan't go backwards when traffic is quiet.expired: true. That covers cursors issued before this migration: after the deploy, each open tab refetches its active queries once.Expected effect. About 6 → 2 rows written per change, so roughly 240M → 75M rows written per month at today's webhook rate. Steady-state table size drops from ~16k rows to ~1k. I'll confirm with
wrangler d1 info quickhub-db(rows_written_24h) a day after the deploy.Deploy notes. The migration drops the old table, which only holds a 15-minute window of refresh signals. Between the migration and the new code going live, change-feed writes fail and are dropped, and reads answer "nothing changed". Both paths are designed to fail that way.
Tests
vitest run(480 passed), andplaywright test --project=isolated --no-deps tests/isolated/change-feed.spec.tspassed locally.migration-destructive: approved
🤖 Generated with Claude Code
Summary by CodeRabbit