fix(rollout): share one bounded Lance cache session across resident stores - #182
Merged
Conversation
…tores Worker RSS grew linearly with cumulative appends (~350-490 KB/append) and never released across merge/compact cycles, converging on ~6 GiB regardless of which stores a worker owned. Root cause: each RolloutStore opened its base dataset via Dataset::open, which builds a fresh per-store Lance Session whose index/metadata caches default to 6 GiB + 1 GiB and are keyed by dataset URI. Every flushed MemWAL generation is a distinct URI, so a busy store's read path (observe/list/stats) fed an ever-growing key set into that per-store cache until it approached the 6 GiB cap. The session outlives the merged generations, so merge/compact never released it. Fix: build one shared, capacity-bounded Session and attach it to every resident store, so the process's total Lance cache is bounded by a single budget rather than 6 GiB per store. - RolloutStoreOptions gains a `session` field; RolloutStore threads it through every open/reload path (load, create, compact, zonemap, generation reads). - New RolloutStore::build_session constructor; re-export Session from core. - Server builds one shared session from a configurable total byte budget (--rollout-cache-bytes / ROLLOUT_CACHE_BYTES, default 2 GiB, 0 disables), split 6:1 index:metadata to match Lance's own default ratio, and injects it into every rollout store it opens. Co-Authored-By: Claude <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
Fixes the rollout worker RSS leak: memory grew linearly with cumulative appends (~350–490 KB/append) and was never released across
merge_wal/compactcycles, with the busiest workers all converging on ~5.95 GiB regardless of which stores they owned. Extrapolated, a busy worker OOMs.Root cause. Each
RolloutStoreopened its base dataset viaDataset::open, which builds a fresh per-store LanceSession. That session's index/metadata caches default to 6 GiB + 1 GiB and are byte-weighted LRUs keyed by dataset URI. Every flushed MemWAL generation is a distinct URI (_mem_wal/{shard}/{generation}), and the read paths (observe, list, stats) open each generation with the base dataset's session. So a busy store fed an ever-growing key set into its own 6 GiB cache — RSS tracked cumulative appends, and the ~5.95 GiB plateau is simplyDEFAULT_INDEX_CACHE_SIZE(6 GiB) filling up.merge/compactnever released it because theSessionoutlives the merged generations and every reload reuses the same session.This matches every data point in the issue: RSS tracks
rollout_appends_totalnot uptime; the three busiest workers converge on the same value despite owning different stores; the marginal KB/append declines (byte-weighted eviction) but never goes negative. The lanceShardWriterwas ruled out — its frozen memtables drain correctly (pop_front+retainon flush).Fix. Build one shared, capacity-bounded
Sessionand attach it to every resident store, so the process's total Lance cache is bounded by a single budget instead of 6 GiB per store.Changes
RolloutStoreOptionsgains asession: Option<Arc<Session>>field;RolloutStorethreads it through every open/reload path (load_with_options,create_with_options,compact, zonemap rebuild, generation reads, schema evolution).RolloutStore::build_session(index_bytes, metadata_bytes)constructor;Sessionre-exported fromlance-context-core.--rollout-cache-bytes/ROLLOUT_CACHE_BYTES, default 2 GiB,0disables sharing (restores the pre-fix per-store default). Split 6:1 index:metadata to match Lance's own default ratio, and injected into every rollout store the server opens.Semantic change
None to correctness or durability. Only the Lance cache is now bounded process-wide and shared, so cross-store cache hit rate improves and total cache memory is capped by config rather than
6 GiB × resident stores.Test plan
cargo build+cargo clippy --all-targetsclean onlance-context-coreandlance-context-servercargo fmt --checkcleancargo test -p lance-context-core(161 passed) and-p lance-context-server(49 passed), plus integration tests🤖 Generated with Claude Code