Repository navigation
Make history compaction's discovery pass read only the payloads it needs (covering index, then incremental passes) #21304
josephv123
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Note
Claude Opus 5.5 in Claude Code, writing on behalf of @josephv123. Following up on #21290, as suggested there.
Problem
compactEventStore(apps/server/src/orchestration-v2/ProjectionMaintenance.ts) pages through every event, newest first, on every pass: one minute after startup and then every 6 hours. Reading the whole history is deliberate. But each pass also reads every row's payload, because the query selectsapplication_event_version, and that column sits afterpayload_json(it was added withALTER TABLE). So the cost of a pass tracks the total size of all payloads, mostlyturn-item.updatedrows that compaction never deletes, rather than the number of events.#20877 moves discovery off the writer and paces it, which addresses the blocking. This is about the remaining cost: how much each pass reads.
Measurements
The database is a copy of a 2.06 GB
statev2.sqlite(190,360 events, 1.12 GB inorchestration_events), from nightly0.1.1-nightly.20261018.3315on Linux. The test runs one full discovery pass with the exact query, in pages of 50, under SQLite 3.46. "Cold" means the copy was evicted from the page cache first.INDEXED BY(sequence, application_event_version, aggregate_kind, stream_id, event_type). It is 22.6 MB here and took 0.5 s to build.CASEneedsentity_id. That's 56,733 of 190,360 rows here (v2message.updated,node.updatedandthread.pull-request-link-updated), and those make up the remaining 245 MB.INDEXED BYthe planner keeps scanning by rowid and ignores the index, becausesequenceis theINTEGER PRIMARY KEY.On the live server, a pass took 17.5 s, since it ran one minute after an OOM restart with a cold cache. It deleted 60 events. Earlier passes in the same log deleted 16 to 444. For the 17–22 GB databases in #19780 and #21154, a pass would read roughly ten times as much. That's extrapolated, not measured.
Proposal
Covering index plus
INDEXED BY(or an equivalent planner hint), so discovery reads only the payloads it needs. This needs a migration that adds about 1% of the events table to the database. It cuts I/O per pass about 4.6× and cold time about 2.5× here. Keep-or-delete decisions don't change.Incremental discovery, as a follow-up. An event only becomes deletable when a newer event arrives for the same entity, or for whole-thread events, the same stream. So a pass could:
(stream_id, event_type, entity), only the entities that received new events.Pass cost would then follow new events rather than total history. That's a bigger change, and the first-event retention rules make it subtler.
Would you take (1) as a focused PR, on top of #20877 or independent of it? Joseph can measure the result on the same database copy, and on a larger one if someone can share row counts.
All reactions