Background compaction scheduler with reader-safe deferred deletion - #5
Merged
Conversation
Wires the compaction engine up to a tokio background task and applies
ReplacementRecord atomically to the manifest. Old segment files are
not deleted immediately — instead, each replacement is timestamped
(committed_at_ms) and a future tick past the grace window cleans them
up. This protects queries that snapshotted the manifest just before a
compaction commit from races where the file gets unlinked mid-read.
Per tick (default every 60s):
1. Sweep pending deletions. For every compacted_replacements entry
older than `compaction_grace_ms` (default 30s), delete the old
segment files and drop the record from the manifest.
2. Plan + execute. Per-bucket plans group small segments together;
each plan is merged (sort + cold-dedupe by event_id) into a
single output. Manifest swap is atomic: old metas dropped from
raw_segments, new meta added, replacement record appended, save.
Plan is aborted defensively if the input set is no longer fully in
raw_segments at commit time (future-proofing against concurrent
compaction workers; not possible today with single-worker setup).
Other changes:
- CompactionPlanner returns Vec<CompactionPlan> with explicit bucket
per plan, instead of an untagged Vec<Vec<String>>. The bucket flows
to build_segment_meta so the output keeps its label.
- ReplacementRecord gains committed_at_ms (#[serde(default)] so it
reads back from existing manifests as 0 — those records never get
finalized by the new code, which matches the pre-existing behavior
of "leave alone forever").
- Config gains compaction_tick_interval_secs, compaction_grace_ms,
compaction_max_small_segments.
- main.rs spawns the worker with Notify-based shutdown.
Tests (tests/compaction.rs, 5 tests):
- Per-bucket plans only trigger when the bucket exceeds the threshold
- Single output segment receives every event from the inputs (no loss
+ event_id cold-dedupe)
- Re-tick post-compaction is a no-op (no extra segments written, no
deletions until grace expires)
- Old files survive the grace window, then disappear on the next
post-grace tick; replacement record is removed from the manifest
- Buckets are processed independently in one tick
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the compaction-scheduler gap. The compaction engine has existed since the durability PR but wasn't reached by any background task. This PR adds the scheduler, makes the manifest swap atomic, and implements a reader-safe deferred-deletion grace window (spec §15.3) so queries that snapshotted the manifest just before a compaction commit don't race with file unlinks.
Each tick (default 60s)
compacted_replacementsentry older thancompaction_grace_ms(default 30s), delete the old segment files and drop the record from the manifest.max_small_segmentssegments) are merged: sort by(account, product, meter, model, ts), cold-dedupe byevent_id, write a single output segment. Manifest swap is atomic — old metas dropped fromraw_segments, new meta added,ReplacementRecordappended, save.The plan is defensively aborted if the input set isn't fully present in
raw_segmentsat commit time. Not reachable today (one worker), but it future-proofs against concurrent compaction.Key design choices
ReplacementRecord.committed_at_msis the deferred-delete clock.#[serde(default)]means existing manifests read back as 0; pre-existing records (none, currently) never get finalized by the new code, matching the prior "leave alone forever" behavior.CompactionPlannerreturnsVec<CompactionPlan>with an explicitbucketfield per plan instead of an untaggedVec<Vec<String>>. The bucket flows through tobuild_segment_metaso the compacted output keeps its label.Configgainscompaction_tick_interval_secs,compaction_grace_ms,compaction_max_small_segments.Notifypattern as the rollup worker.Test plan
cargo build --all-targetsclean with-D warningscargo test --all-targets— 32 tests pass (was 27; +5 intests/compaction.rs)New tests:
Storage layer status
With this PR, the storage path is end-to-end complete:
Still on the backlog
rejectedcountsparking_lotdepThese are smaller items suitable for a single "polish" PR.
🤖 Generated with Claude Code