Skip to content

Run VACUUM after a large DeleteJob purge to fix B-tree fragmentation - #73

Open
srhoods wants to merge 1 commit into
masterfrom
vacuum-after-large-purge
Open

Run VACUUM after a large DeleteJob purge to fix B-tree fragmentation#73
srhoods wants to merge 1 commit into
masterfrom
vacuum-after-large-purge

Conversation

@srhoods

@srhoods srhoods commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Purging a job with millions of shard rows leaves the shards B-tree fragmented enough to visibly slow later queries — the existing auto_vacuum=INCREMENTAL pump (500 pages/30s, sized for the Shard Reaper's steady trickle) doesn't catch up before the next large purge lands.
  • DeleteJob now triggers a full VACUUM when a purge removes at least vacuumShardThreshold (1M) shard rows; smaller purges are left to the existing incremental pump.
  • Runs on its own goroutine (WAL checkpoint TRUNCATE, then VACUUM) off the purge API's own request path, and coalesces concurrent/rapid DeleteJob calls onto a single pending run — a bulk purge (POST /api/v1/jobs/purge) can call DeleteJob once per matched job, and since VACUUM compacts the whole file regardless of which job triggered it, running it once per job would be pure waste.
  • docs/DESIGN-coordinator.md §3 documents the tradeoff, including that this genuinely does hold store.mu (and therefore blocks the whole coordinator's writes, plus reads once the rewrite starts) for its full duration — moving it off the request path defers that cost, it doesn't remove it.

Test plan

  • gofmt -l . clean
  • go vet ./... clean
  • go test -count=1 ./... — all green
  • go test -race on store/api (skipping one unrelated pre-existing slow scale test that also times out under -race on unmodified master) — clean, no races
  • New tests (TestDeleteJobSkipsVacuumBelowThreshold, TestDeleteJobVacuumsAboveThreshold, TestDeleteJobCoalescesConcurrentVacuums) each verified via deliberate-breakage falsification: reverted the threshold gate / CAS coalescing, confirmed the corresponding test fails for the expected reason, restored

🤖 Generated with Claude Code

https://claude.ai/code/session_01PsdNZLfmAFrMX2VUtkLtmm

Purging a large job (millions of shards/splits/etc. rows deleted in one
transaction) left the shards B-tree fragmented badly enough to visibly
slow every later query against it. The existing auto_vacuum=INCREMENTAL
pump (500 pages/30s) is sized for the Shard Reaper's steady trickle and
doesn't catch up before the next large purge lands.

DeleteJob now triggers a full VACUUM whenever the purge removes at
least vacuumShardThreshold (1M) shard rows — below that, incremental
vacuuming already handles it. Runs on its own goroutine (checkpoint
TRUNCATE, then VACUUM) so the purge API call isn't held open for it,
and coalesces concurrent/rapid DeleteJob calls onto a single pending
run via a CAS-guarded flag, since a bulk purge can call DeleteJob once
per matched job and VACUUM compacts the whole file regardless of which
job triggered it — running it once per job in that loop would be pure
waste.

This is expensive by design, not despite the cost: VACUUM needs an
exclusive lock for as long as the full-file rewrite takes (the same
class of cost WALCheckpoint's TRUNCATE mode already documents at up to
144s for a much cheaper operation) and — since db is the sole writer
connection — has to run there, so it holds mu and blocks every other
write (and every rdb reader once the rewrite starts) for its complete
duration. Moving it off the request path doesn't remove that cost,
only defers it to a moment the caller isn't blocked on it.

Tests cover the threshold gate, that VACUUM actually reclaims space
(freelist_count -> 0), and that concurrent large purges coalesce into
exactly one run — each verified by deliberately breaking the logic
first and confirming the test fails for the expected reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsdNZLfmAFrMX2VUtkLtmm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant