turbo-persistence: Use AtomicBool for is_empty() to avoid lock contention (#92481)

### What?

Replace the `RwLock`-based `is_empty()` check in `TurboPersistence` with
a lock-free `AtomicBool` that mirrors the emptiness of
`inner.meta_files`.

### Why?

`is_empty()` is called on the hot read path (`lookup_task_candidates`)
as a fast early-return before performing any real work. The previous
implementation acquired a read lock on the inner `RwLock` purely to
check whether `meta_files` is empty — a boolean state that changes
infrequently (only on `load_directory` and `commit`). Taking a lock on
every lookup, even a read lock, adds unnecessary contention and
overhead.

### How?

- Added an `is_empty: AtomicBool` field to `TurboPersistence`,
initialized to `true` (a fresh database is always empty).
- The atomic is updated at the two mutation points that change
`meta_files`:
- `load_directory()` — after reading meta files from disk into
`inner.meta_files`.
- `commit()` — after appending new meta files and retaining/removing old
ones (under the write lock).
- `is_empty()` now does a single `load(Ordering::Relaxed)` instead of
acquiring a read lock.

`Relaxed` ordering is appropriate because `is_empty()` is used as a
performance hint, not a synchronization point. A briefly stale value
only means a redundant hash computation, not a correctness issue.

<!-- NEXT_JS_LLM_PR -->

Co-authored-by: Tobias Koppers <[email protected]>
Co-authored-by: Claude <[email protected]>
This commit is contained in:
Luke Sandberg
2026-04-08 00:19:10 +02:00
committed by GitHub
co-authored by Tobias Koppers Claude
parent e513488d0d
commit dfa1201ab8
+9 -1
View File
@@ -117,6 +117,9 @@ pub struct TurboPersistence<S: ParallelScheduler, const FAMILIES: usize> {
read_only: bool,
/// The inner state of the database. Writing will update that.
inner: RwLock<Inner<FAMILIES>>,
/// A flag to indicate if the database is empty (no meta files). This is an atomic mirror of
/// `inner.meta_files.is_empty()` to avoid taking a lock on the hot path.
is_empty: AtomicBool,
/// A flag to indicate if a write operation is currently active. Prevents multiple concurrent
/// write operations.
active_write_operation: AtomicBool,
@@ -191,6 +194,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
accessed_key_hashes: [(); FAMILIES]
.map(|_| DashSet::with_hasher(BuildNoHashHasher::default())),
}),
is_empty: AtomicBool::new(true),
active_write_operation: AtomicBool::new(false),
key_block_cache: BlockCache::with(
KEY_BLOCK_CACHE_SIZE as usize / KEY_BLOCK_AVG_SIZE,
@@ -395,6 +399,8 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
}
let inner = self.inner.get_mut();
self.is_empty
.store(meta_files.is_empty(), Ordering::Relaxed);
inner.meta_files = meta_files;
inner.current_sequence_number = current;
Ok(true)
@@ -442,7 +448,7 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
/// Returns true if the database is empty.
pub fn is_empty(&self) -> bool {
self.inner.read().meta_files.is_empty()
self.is_empty.load(Ordering::Relaxed)
}
/// Starts a new WriteBatch for the database. Only a single write operation is allowed at a
@@ -686,6 +692,8 @@ impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES>
}
});
inner.meta_files.reverse();
self.is_empty
.store(inner.meta_files.is_empty(), Ordering::Relaxed);
has_delete_file = !sst_seq_numbers_to_delete.is_empty()
|| !blob_seq_numbers_to_delete.is_empty()
|| !meta_seq_numbers_to_delete.is_empty();