Repository navigation
fix(metastore): do not hold the state guard across a lazy index load - #6845
Open
aayushbaluni wants to merge 2 commits into
Open
aayushbaluni wants to merge 2 commits into
aayushbaluni wants to merge 2 commits into
Conversation
`FileBackedMetastore::index` awaited `LazyFileBackedIndex::get()` while holding the metastore state guard. On the first access to an index that is still lazy, that await spans a full `metastore.json` download from storage. tokio's `RwLock` is write-preferring, so one writer queued behind that download blocks every reader that arrives after it -- including readers of unrelated indexes whose metadata is already resident in memory. Every index starts uninitialized after a process start, so the guarded path is reached routinely rather than exceptionally. Hold the lock only long enough to resolve the handle, then release it and load outside. `LazyIndexStatus::Active` now carries an `Arc<LazyFileBackedIndex>` so the clone shares one `OnceCell`, preserving the existing guarantee that concurrent callers observe a single instance and perform a single load. `get_index_mutex` becomes the synchronous `resolve_lazy_index`, which cannot await by construction. The write branch had the same shape and is fixed the same way. In the insert path the index is handed to the constructor, so its `OnceCell` is already populated and resolving it after dropping the guard does no I/O. Measured with the reporter's reproduction, an unrelated warm read while a cold load is in flight and a writer is queued: before: stalls, observed after 514ms after: 20 consecutive reads complete, 232ms total Closes quickwit-oss#6840
This branch has not been deployed
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.
Problem
FileBackedMetastore::indexawaitedLazyFileBackedIndex::get()while holding the metastore state guard. On the first access to an index that is still lazy, that await spans a fullmetastore.jsondownload from storage.tokio's
RwLockis write-preferring, so a single writer queued behind that download blocks every reader arriving after it — including readers of unrelated indexes whose metadata is already resident in memory. Every index is uninitialized after a process start, so this path is reached routinely rather than exceptionally.The same shape appeared three times in
index(): the read fast path, the write-branch re-check, and the insert path.Measured
Using @YZL0v3ZZ's reproduction from #6840 (added here as a regression test), reading an unrelated already-loaded index while a cold load is in flight and one writer is queued:
Fix
Hold the lock only long enough to resolve the handle, then release it and load outside.
LazyIndexStatus::Activenow carries anArc<LazyFileBackedIndex>. That matters for correctness, not just ergonomics:LazyFileBackedIndexowns aOnceCell, so cloning the value out of the map would create a second cell and break the existing guarantee — stated in the comment right above the write branch — that concurrent callers observe a single instance and perform a single load. Cloning theArcshares one cell, so the dedup is preserved.get_index_mutexbecomesresolve_lazy_index, which is synchronous by construction — it cannot reintroduce an await under the guard without a signature change.In the insert path the index is handed to the constructor, so
OnceCell::new_withhas already populated it and resolving after dropping the guard performs no I/O. That is what makes reordering the insert ahead of the resolve safe.Considered consequence
A concurrent
delete_indexcan now interleave between the clone and the load, where previously it waited for the read guard. The outcome is unchanged: the caller holds anArcit already obtained, and in the old ordering the delete simply happened immediately afterwards instead. No caller relies on the state lock being held across the load — the otherLazyIndexStatus::Activesites are allActive(_)patterns that never await.Tests
quickwit-metastore/tests/file_backed_cold_load_cascade.rs, adapted from the reporter's reproduction into a regression test: it now asserts the unrelated read completes rather than asserting it stalls.test_file_backed_metastore_race_condition, which is the existing guard on the single-instance invariant this change touches.read of unrelated index 'index-warm' stalled behind the queued writer while 'index-cold' was still loading.cargo clippy -p quickwit-metastore --all-targets: no warnings.cargo +nightly fmt --check: clean.Note on how this was run
I am on Windows without a linker, so everything above ran in a
rust:1.90-bookwormcontainer;rust-toolchain.tomlpinned the actual toolchain to 1.96 as usual. The container'sprotocregeneratesquickwit-proto/src/codegen/with a one-character whitespace difference in a doc comment — that is a build artifact, not part of this change, and it is reverted here.Closes #6840