Conversation
`DatasetConsistencyWrapper::get_mut` reloaded the dataset and only then took the write lock, so during the reload it held only a read lock and no write request was queued. tokio's `RwLock` only holds back new readers once a writer is waiting, so every reader that arrived during the reload was admitted ahead of the writer, which then had to wait for all of them before it could take the write lock. `get_mut` now takes the write lock first and reloads while holding it, so write-fairness covers the whole operation. The reload body is factored into `reload_under_write_lock`, shared with the read path's `reload`, which still checks under a read lock and upgrades to a write lock only when a successor version exists. A unit test queues `get_mut` and then a reader behind an exclusively held lock and asserts that the writer is served before the reader.
amunra
marked this pull request as ready for review
September 16, 2026 14:52
Contributor
There was a problem hiding this comment.
✅ Gate recommendation: approve.
Acquiring the write lock before the successor probe makes Tokio’s FIFO writer preference cover the full get_mut() operation, while the read path keeps its existing optimistic probe. The shared reload helper preserves freshness and error handling, and the regression test covers the reader/writer ordering.
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
DatasetConsistencyWrapper(the cached manifest dataset behind the directory namespace) guards itsDatasetwith atokio::sync::RwLockand exposesget()/get_refreshed()for readers andget_mut()for writers.get_mut()calledreload()and only then requested the write lock.reload()itself takes a read lock to probe for a successor version and releases it before anything else happens, so from the lock's point of view the writer was just another reader until the moment it finally called.write().await.tokio's
RwLockis write-preferring: once a writer is queued, no new readers are admitted. That protection never engaged forget_mut()while it was reloading, because during that phase it held only a read lock and no write request was queued yet. Every reader that arrived whileget_mut()was still reloading was admitted alongside it, and the writer then had to wait for all of them before its write request was served; the fairness guarantee only covered the tail of the operation.get_mut()is a public method of the wrapper; the namespace's own write operations currently go throughget_refreshed()and the copy-on-write manifest rewrite path, so this affects external callers ofget_mut()rather than any in-tree operation.Change
get_mut()now acquires the write lock first and reloads the dataset while holding it, so write-fairness covers the whole operation: as soon as a writer callsget_mut(), new readers queue behind it.The reload body is factored into
reload_under_write_lock(), which takes the already-heldRwLockWriteGuard, probeshas_successor_version()and callscheckout_latest()if needed. The read path (get()/get_refreshed()->reload()) is unchanged in behaviour: it still checks under a read lock and only upgrades to a write lock when a successor version exists, then reuses the same helper instead of duplicating the probe-and-checkout logic.The cost is that every
get_mut()now performs the successor probe while holding the write lock rather than under a read lock. Writers already had to take the write lock immediately afterwards, andhas_successor_version()is a single object-store existence check on the manifest table, so the extra exclusive hold is short and only on the write path.Testing
New unit test
test_get_mut_is_served_before_later_readersinrust/lance-namespace-impls/src/dir/manifest.rs. It holds the wrapper's lock exclusively, startsget_mut()on one task and then, on a second task, a plain read-lock acquisition that keeps its permit until told to release it; it then releases the exclusive lock and asserts under a bounded timeout thatget_mut()completes before the late reader is served, and that the reader completes once released. On a current-thread runtime the two tasks park on the lock in spawn order and the late reader holds a single permit from admission to release, so the outcome depends only on which lockget_mut()requests first: with the old ordering it queued as a reader, the late reader was admitted alongside it, and the writer's subsequent write request blocked on that reader until the timeout; with the new ordering it is queued as a writer and served first. Verified both ways, five runs each, with the fix locally reverted and restored.The rest of the
lance-namespace-implssuite (which exercisesget()/get_refreshed()through the directory namespace operations andget_mut()in one existing unit test) passes unchanged, along withcargo fmt --all -- --checkandcargo clippy --all --tests --benches -- -D warnings.Compatibility
Internal locking change only. No public API, wire, or file-format changes; the manifest dataset layout is untouched.
Prior submission
The same fix was previously submitted as #6323 and went stale unreviewed.
Tracking: Ported from rerun-io#70.