feat(rollout): count-triggered self-merge of MemWAL shards into base table#127
Merged
Merged
Conversation
…table
Rollout MemWAL appends previously accumulated flushed generations under
_mem_wal/{shard}/ forever, so read cost grew linearly with append count
(every list/get unions all generations). This adds an opt-in, size-triggered
"external compactor" that folds a shard back into the base table.
When merge_after_generations (--rollout-merge-after-generations /
ROLLOUT_MERGE_AFTER_GENERATIONS) is set, an append that leaves the instance's
own shard with >= N un-merged generations synchronously reads those
generations, appends their rows to the base table, and commit_updates the
shard manifest to drain flushed_generations to empty (keeping the replay
cursor so a reopened writer never re-replays merged WAL entries).
Safe under the server-id sharding model: each instance merges only the shard
it writes, so claim_epoch fences no live peer. Crash between base-append and
manifest-drain is harmless because rollout rows are immutable and deduped by
id at read time. N=0 (default) preserves 0.6.0 pure-accumulation behavior.
Benchmark (200 single-row generations, local FS, release): list scan ~654ms
un-merged vs ~50ms merged (~13x); slowest merge-on-every-append add ~20ms.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
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
Follow-up to #126. In 0.6.0, rollout MemWAL appends accumulate flushed generations under
_mem_wal/{shard}/forever — there was no path to fold them back into the base table, so read cost grew linearly with append count (everylist/getunions all generations). This PR adds an opt-in, size-triggered self-merge (the "external compactor" path Lance's MemWAL LSM design anticipates).Set
--rollout-merge-after-generations N(envROLLOUT_MERGE_AFTER_GENERATIONS, orRolloutStoreOptions::merge_after_generations). After an append leaves this instance's own shard with ≥ N un-merged generations, the sameaddsynchronously:_mem_wal/{shard}/),Dataset::appends their rows into the base table, andcommit_updates the shard manifest to drainflushed_generationsto empty — keepingreplay_after_wal_entry_positionso a reopened writer never re-replays merged WAL entries.N = 0(default) preserves the 0.6.0 pure-accumulation behavior.Why it's safe
claim_epochs the shard, bumping its writer epoch — but each instance merges only the shard it writes, which has no other live writer under the server-id sharding model (feat(rollout): MemWAL ingest with server-id sharding #126 / spec §3). The instance's nextaddre-claims the current epoch.idat read time. A crash between step 2 and step 3 (rows in base, manifest not yet drained) just means a reader sees the rows via both base and the still-listed generation and dedups them — no double counting. The next merge drains the manifest.Benchmark
bench_merge_read_amplification(200 single-row generations, local FS, release):listscan of 200 rows)Slowest single
add(merge-on-every-append at N=1): ~20 ms.Changes
RolloutStoreOptions::merge_after_generations+RolloutStore::{maybe_merge_own_shard, merge_own_shard}, triggered inadd()afterclose().--rollout-merge-after-generationsflag →AppState→ rollout route.unified_rollout.rs).specs/rollout-deployment.md§6.1 documents the mechanism, safety, trade-off, and benchmark; §9 checklist updated.Test plan
below_threshold_no_merge— generations accumulate, no merge under thresholdmerge_at_threshold_drains_shard_and_preserves_reads— shard drains, rows present exactly once, inline artifact bytes still fetchable post-mergemerge_is_visible_to_a_fresh_reader_instance— merged rows land in base, visible to a newly-opened reader🤖 Generated with Claude Code