Repository navigation
feat(rollout): periodic per-shard WAL cleanup + concurrency hardening - #131
Merged
Merged
Conversation
Add a time-triggered per-shard WAL cleanup for the rollout store, complementing the existing count-triggered self-merge. Each instance cleans only the shard it owns and writes, reusing its own writer epoch, so it never fences a concurrent writer. - RolloutStoreOptions gains `cleanup_interval_secs` (time trigger) and `cleanup_min_generations` (count gate for a tick). - `cleanup_own_shard` runs one pass; `spawn_periodic_cleanup` runs a background timer that holds only a Weak ref, so it self-terminates when the store is dropped (e.g. on delete). - Server exposes `--rollout-cleanup-interval-secs` / `--rollout-cleanup-min-generations` and spawns the timer per store. Concurrency hardening (verified against Lance 7.0.0 commit/OCC model): - ensure_mem_wal: two concurrent first-writers both commit CreateIndex, which Lance treats as a hard (non-retried) conflict. Reload and accept "index now present" as success instead of surfacing the conflict. - merge drain: remove exactly the generations merged instead of clearing the vec, so a generation flushed during a merge is retained for the next pass rather than silently dropped. Tests: concurrent two-shard merge into one base table, surgical-drain row conservation, timer-driven cleanup, min-generation gating. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- rustfmt the new rollout cleanup code - avoid clippy unnecessary_to_owned / needless_update - add rollout_cleanup_* fields to server test AppState literals Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
4 tasks done
beinan
added a commit
that referenced
this pull request
Jul 14, 2026
## Summary Adds **base-table compaction** to the rollout store. Every WAL merge (`merge_own_shard`) appends a fragment to the base table, so a long-lived rollout store accumulates many small fragments that slow scans. This folds them back together via Lance's `compact_files`, mirroring `ContextStore::compact`. Follow-up to #131 (WAL cleanup + concurrency hardening). ### Core (`RolloutStore`) - `compact(config)` — runs `compact_files` on the base table, reloads the handle, and tracks `last_compaction` / `total_compactions` / `last_error`. - `should_compact(config)` — gates on `min_fragments` + `quiet_hours` so an external scheduler can pass the same config. - `compaction_stats()` — fragment count + compaction history. - Uses `defer_index_remap: true`: the rollout base table carries a **fieldless MemWAL index**, and Lance's inline index remap panics on it (`"An index existed with no fields"`). Deferring records a fragment-reuse index and remaps lazily instead of touching the MemWAL index during the rewrite. ### Server - `POST /api/v1/rollouts/{name}/compact` and `GET /api/v1/rollouts/{name}/compact/stats`, reusing the existing `Compact*` DTOs. ### Distributed model (important) Compaction rewrites the **shared** base table, and two concurrent `Rewrite` commits conflict in Lance's commit matrix (one wins, the other must redo its work). So this is meant to be driven by a **single external scheduler** (cron / k8s CronJob), **not** a per-worker background timer — unlike WAL merge, where each worker only ever touches its own shard. It is safe to run alongside worker appends / WAL merges: `Append` vs `Rewrite` is non-conflicting in Lance's matrix, so a concurrent append simply rebases on the compaction (or vice-versa). ## Test plan - [x] `compact_reduces_fragments_and_preserves_reads` — fragment count drops; all rows readable exactly once; inline artifact blobs still fetchable; stats updated - [x] `should_compact_respects_min_fragments` - [x] `compact_composes_with_concurrent_wal_merge` — a compaction (Rewrite) and a WAL merge (Append) from another shard run concurrently without failing or losing rows - [x] Full `lance-context-core` (114) and `lance-context-server` (28) suites, `cargo fmt --all --check`, `cargo clippy --workspace --all-targets` all green 🤖 Generated with [Claude Code](https://claude.com/claude-code) 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
Adds a time-triggered per-shard WAL cleanup for the rollout store, complementing the existing count-triggered self-merge, and hardens the merge path against several concurrency corner cases discovered by reviewing the actual Lance 7.0.0 commit/OCC model.
Each instance cleans only the shard it owns and writes, reusing its own writer epoch, so it never fences a concurrent writer — the same safety invariant as the existing count-triggered merge.
Feature: periodic WAL cleanup
RolloutStoreOptionsgainscleanup_interval_secs(the time trigger) andcleanup_min_generations(the count gate a tick must meet before merging).cleanup_own_shard()runs a single pass;spawn_periodic_cleanup()runs a background timer that holds only aWeakreference, so it self-terminates when the store is dropped (e.g. on delete) — no leaked task, no store kept alive.--rollout-cleanup-interval-secs/--rollout-cleanup-min-generations(envROLLOUT_CLEANUP_INTERVAL_SECS/ROLLOUT_CLEANUP_MIN_GENERATIONS) and spawns the timer per rollout store. Embedded use leaves it disabled (no long-lived runtime to own the timer).Concurrency hardening
ensure_mem_wal: two concurrent first-writers both observe no index and both commitCreateIndex, which Lance treats as a hard, non-retried conflict — the loser previously failed the whole write. Now we reload and accept "index now present" as success. (This was caught by a new test.)Corner cases analyzed (against Lance source) and confirmed safe
append) racing base compaction —AppendvsRewriteare non-conflicting; OCC auto-rebases.Appends auto-retry; each drains only its own shard manifest.&mut self+ per-storeRwLock, backstopped by MemWAL epoch fencing.idon read; next merge drains.Weak, exits on next tick.Test plan
cleanup_own_shard_merges_only_at_min_generations— min-generation gatingspawn_periodic_cleanup_reclaims_on_a_timer— timer drains the shardspawn_periodic_cleanup_disabled_returns_none— interval 0 spawns nothingconcurrent_merges_from_two_shards_into_one_base_table— two shards merge into one base table, all rows conserved exactly oncemerge_drains_only_merged_generations— surgical drain conserves rows across two merge roundslance-context-coresuite,cargo clippy --workspace, and workspace build all green🤖 Generated with Claude Code