lsm_tree/tree/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5#[cfg(feature = "columnar")]
6pub mod columnar_scan;
7pub mod ingest;
8pub mod inner;
9pub mod sealed;
10
11use crate::path::Path;
12use crate::{
13 AbstractTree, Checksum, KvPair, SeqNo, SequenceNumberCounter, TableId, UserKey, UserValue,
14 ValueType,
15 compaction::{CompactionStrategy, drop_range::OwnedBounds, state::CompactionState},
16 config::Config,
17 format_version::FormatVersion,
18 fs::Fs,
19 iter_guard::{IterGuard, IterGuardImpl},
20 key::InternalKey,
21 manifest::Manifest,
22 memtable::Memtable,
23 range_tombstone::RangeTombstone,
24 scan_since::ScanSinceEvent,
25 slice::Slice,
26 table::Table,
27 value::InternalValue,
28 version::{SuperVersion, SuperVersions, Version, recovery::recover},
29 vlog::BlobFile,
30};
31use alloc::sync::Arc;
32#[cfg(not(feature = "std"))]
33use alloc::{boxed::Box, string::ToString, vec::Vec};
34use core::ops::{Bound, RangeBounds};
35use inner::{FlushGuard, TreeId, TreeInner, VersionsWriteGuard};
36// no-std: spin mirrors parking_lot's Mutex/RwLock API without an allocator.
37// parking_lot wins on the std hot path, so keep it for std.
38#[cfg(feature = "std")]
39use parking_lot::{Mutex, RwLock};
40#[cfg(not(feature = "std"))]
41use spin::{Mutex, RwLock};
42
43#[cfg(feature = "metrics")]
44use crate::metrics::Metrics;
45
46/// Floor for the storage-admission reserved headroom band (see
47/// [`Tree::compute_write_admission`]). Even with an empty active memtable the
48/// gate keeps at least this much room below the budget so the next writes and a
49/// space-reclaiming compaction have somewhere to land. 1 MiB.
50pub const MIN_RESERVED_HEADROOM: u64 = 1024 * 1024;
51
52/// How long a cached disk-free sample stays valid before the admission gate
53/// re-probes. Bounds how stale the physical free-space figure can be when the
54/// filesystem fills from another process between flushes, without issuing a
55/// `statfs`/`statvfs` syscall on every gated write. 1 second.
56const ADMISSION_DISK_FREE_TTL: core::time::Duration = core::time::Duration::from_secs(1);
57
58/// Iterator value guard
59pub struct Guard(crate::Result<(UserKey, UserValue)>);
60
61impl IterGuard for Guard {
62 fn into_inner_if(
63 self,
64 pred: impl Fn(&UserKey) -> bool,
65 ) -> crate::Result<(UserKey, Option<UserValue>)> {
66 let (k, v) = self.0?;
67
68 if pred(&k) {
69 Ok((k, Some(v)))
70 } else {
71 Ok((k, None))
72 }
73 }
74
75 fn key(self) -> crate::Result<UserKey> {
76 self.0.map(|(k, _)| k)
77 }
78
79 fn size(self) -> crate::Result<u32> {
80 #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
81 self.into_inner().map(|(_, v)| v.len() as u32)
82 }
83
84 fn into_inner(self) -> crate::Result<(UserKey, UserValue)> {
85 self.0
86 }
87}
88
89/// Trait for monomorphized table point-read results.
90///
91/// Allows `find_in_tables` to operate generically over `InternalValue` (for
92/// `get`) and `(InternalValue, Block)` (for `get_pinned`), generating optimal
93/// code for each path without runtime dispatch or extra refcount overhead.
94trait TablePointLookup: Sized {
95 fn lookup(
96 table: &Table,
97 key: &[u8],
98 seqno: SeqNo,
99 key_hash: u64,
100 ) -> crate::Result<Option<Self>>;
101 fn entry_seqno(&self) -> SeqNo;
102 fn filter_tombstone(self) -> Option<Self>;
103}
104
105/// Lookup result for standard `get()` — entry only, no block retained.
106type TableEntry = InternalValue;
107
108/// One covered key in a batched run resolution: `(input index, key hash,
109/// resolved item)`. Aliased to keep `resolve_run_batched`'s return readable.
110type CoveredKey = (usize, u64, Option<InternalValue>);
111
112/// `(miss_keys, duplicates)` from [`Tree::dedup_sorted_miss_keys`]: `miss_keys`
113/// is `(key_index, bloom_hash)` for the strictly-sorted-unique batched resolver,
114/// `duplicates` is `(duplicate_index, representative_index)` for the fan-out.
115type DedupedMissKeys = (Vec<(usize, u64)>, Vec<(usize, usize)>);
116
117/// The outcome of resolving a key batch against one run (see `resolve_run_batched`).
118struct RunResolve {
119 /// Covered, non-skipped keys with their resolved item, in input order.
120 covered: Vec<CoveredKey>,
121 /// Keys this run does not cover, in input order, for the next run or level.
122 not_covered: Vec<(usize, u64)>,
123}
124
125/// One data block the chunked `multi_get` resolver will read (see
126/// `resolve_level_chunked`): the block, the SST it lives in (`table` + its `file`
127/// handle), the table-local read seqno, whether it needs the special load path
128/// (Page-ECC / columnar), and the ORIGINAL key indices that fall in this block.
129struct BlockTask<'a> {
130 table: &'a crate::Table,
131 file: Arc<dyn crate::fs::FsFile>,
132 handle: crate::table::BlockHandle,
133 table_seqno: SeqNo,
134 special: bool,
135 keys: Vec<usize>,
136}
137
138impl TablePointLookup for TableEntry {
139 fn lookup(
140 table: &Table,
141 key: &[u8],
142 seqno: SeqNo,
143 key_hash: u64,
144 ) -> crate::Result<Option<Self>> {
145 table.get(key, seqno, key_hash)
146 }
147
148 fn entry_seqno(&self) -> SeqNo {
149 self.key.seqno
150 }
151
152 fn filter_tombstone(self) -> Option<Self> {
153 ignore_tombstone_value(self)
154 }
155}
156
157/// Lookup result for `get_pinned()` — entry + block for zero-copy pinning.
158type TableEntryWithBlock = (InternalValue, crate::table::Block);
159
160impl TablePointLookup for TableEntryWithBlock {
161 fn lookup(
162 table: &Table,
163 key: &[u8],
164 seqno: SeqNo,
165 key_hash: u64,
166 ) -> crate::Result<Option<Self>> {
167 table.get_with_block(key, seqno, key_hash)
168 }
169
170 fn entry_seqno(&self) -> SeqNo {
171 self.0.key.seqno
172 }
173
174 fn filter_tombstone(self) -> Option<Self> {
175 ignore_tombstone_value(self.0).map(|iv| (iv, self.1))
176 }
177}
178
179/// Lookup result for the value-returning `get()` path: `(value_type, seqno,
180/// value)`, no key reconstruction (the caller has the needle).
181type TableValue = (ValueType, SeqNo, crate::Slice);
182
183impl TablePointLookup for TableValue {
184 fn lookup(
185 table: &Table,
186 key: &[u8],
187 seqno: SeqNo,
188 key_hash: u64,
189 ) -> crate::Result<Option<Self>> {
190 table.get_value(key, seqno, key_hash)
191 }
192
193 fn entry_seqno(&self) -> SeqNo {
194 self.1
195 }
196
197 fn filter_tombstone(self) -> Option<Self> {
198 if self.0.is_tombstone() {
199 None
200 } else {
201 Some(self)
202 }
203 }
204}
205
206fn ignore_tombstone_value(item: InternalValue) -> Option<InternalValue> {
207 if item.is_tombstone() {
208 None
209 } else {
210 Some(item)
211 }
212}
213
214/// A log-structured merge tree (LSM-tree/LSMT)
215#[derive(Clone)]
216pub struct Tree(#[doc(hidden)] pub Arc<TreeInner>);
217
218impl core::ops::Deref for Tree {
219 type Target = TreeInner;
220
221 fn deref(&self) -> &Self::Target {
222 &self.0
223 }
224}
225
226impl crate::abstract_tree::sealed::Sealed for Tree {}
227
228/// Maps a raw merge-pipeline item into a standard-tree iterator guard.
229fn standard_guard(item: crate::Result<InternalValue>) -> IterGuardImpl {
230 IterGuardImpl::Standard(Guard(item.map(|iv| (iv.key.user_key, iv.value))))
231}
232
233/// A guard carrying only an error: the single item an iterator surface yields
234/// when it fails before it can open a snapshot (no row, no version to bind a
235/// blob guard to), so the failure reaches the consumer through the same
236/// `Result` it already handles per row.
237#[expect(
238 clippy::redundant_pub_crate,
239 reason = "reached from blob_tree as crate::tree::error_guard"
240)]
241pub(crate) fn error_guard(e: crate::Error) -> IterGuardImpl {
242 IterGuardImpl::Standard(Guard(Err(e)))
243}
244
245/// Extract owned user-key bounds from any range.
246#[expect(
247 clippy::redundant_pub_crate,
248 reason = "reached from blob_tree as crate::tree::range_to_user_bounds"
249)]
250pub(crate) fn range_to_user_bounds<K: AsRef<[u8]>, R: RangeBounds<K>>(
251 range: &R,
252) -> (Bound<UserKey>, Bound<UserKey>) {
253 use core::ops::Bound::{Excluded, Included, Unbounded};
254 let lo = match range.start_bound() {
255 Included(x) => Included(x.as_ref().into()),
256 Excluded(x) => Excluded(x.as_ref().into()),
257 Unbounded => Unbounded,
258 };
259 let hi = match range.end_bound() {
260 Included(x) => Included(x.as_ref().into()),
261 Excluded(x) => Excluded(x.as_ref().into()),
262 Unbounded => Unbounded,
263 };
264 (lo, hi)
265}
266
267/// Wraps a [`SeekableTreeIter`](crate::range::SeekableTreeIter) so a standard
268/// tree can expose it as a [`SeekableGuardIter`](crate::iter_guard::SeekableGuardIter).
269struct StandardSeekable {
270 inner: crate::range::SeekableTreeIter,
271}
272
273impl Iterator for StandardSeekable {
274 type Item = IterGuardImpl;
275
276 fn next(&mut self) -> Option<Self::Item> {
277 self.inner.next().map(standard_guard)
278 }
279}
280
281impl DoubleEndedIterator for StandardSeekable {
282 fn next_back(&mut self) -> Option<Self::Item> {
283 self.inner.next_back().map(standard_guard)
284 }
285}
286
287impl crate::iter_guard::SeekableGuardIter for StandardSeekable {
288 fn seek_to(&mut self, key: &[u8]) {
289 self.inner.seek_to(key);
290 }
291
292 fn seek_to_for_prev(&mut self, key: &[u8]) {
293 self.inner.seek_to_for_prev(key);
294 }
295
296 fn peek_key(&mut self) -> Option<crate::Result<crate::UserKey>> {
297 self.inner.peek_key()
298 }
299}
300
301impl AbstractTree for Tree {
302 fn table_file_cache_size(&self) -> usize {
303 self.config
304 .descriptor_table
305 .as_ref()
306 .map_or(0, |dt| dt.len())
307 }
308
309 fn get_version_history_lock(&self) -> VersionsWriteGuard<'_> {
310 self.version_history.write()
311 }
312
313 fn next_table_id(&self) -> TableId {
314 self.0.table_id_counter.get()
315 }
316
317 fn id(&self) -> TreeId {
318 self.id
319 }
320
321 fn blob_file_count(&self) -> usize {
322 0
323 }
324
325 #[cfg(feature = "std")]
326 fn create_checkpoint(
327 &self,
328 target_path: &crate::path::Path,
329 ) -> crate::Result<crate::CheckpointInfo> {
330 crate::checkpoint::run_checkpoint(
331 self,
332 &crate::checkpoint::CheckpointParams {
333 target_root: target_path,
334 target_fs: &self.config.fs,
335 src_root: &self.config.path,
336 src_fs: &self.config.fs,
337 deletion_pause: &self.deletion_pause,
338 visible_seqno: &self.config.visible_seqno,
339 include_blobs: false,
340 runtime_config: self.0.runtime_config.load_full(),
341 encryption: self.0.config.encryption.clone(),
342 },
343 )
344 }
345
346 fn print_trace(&self, key: &[u8]) -> crate::Result<()> {
347 let super_version = self.version_history.read().latest_version();
348
349 let key = Slice::from(key);
350
351 for kv in super_version.active_memtable.range_internal((
352 Bound::Included(InternalKey::new(key.clone(), SeqNo::MAX, ValueType::Value)),
353 Bound::Unbounded,
354 )) {
355 log::info!("[Active] {kv:?}");
356 }
357
358 for mt in super_version.sealed_memtables.iter().rev() {
359 for kv in mt.range_internal((
360 Bound::Included(InternalKey::new(key.clone(), SeqNo::MAX, ValueType::Value)),
361 Bound::Unbounded,
362 )) {
363 log::info!("[Sealed #{}] {kv:?}", mt.id());
364 }
365 }
366
367 for table in super_version
368 .version
369 .iter_levels()
370 .flat_map(|lvl| lvl.iter())
371 .filter_map(|run| run.get_for_key_cmp(&key, self.config.comparator.as_ref()))
372 {
373 for kv in table.range(..) {
374 let kv = kv?;
375
376 if kv.key.user_key != key {
377 break;
378 }
379
380 log::info!("[Table #{}] {kv:?}", table.id());
381 }
382 }
383
384 Ok(())
385 }
386
387 fn get_internal_entry(&self, key: &[u8], seqno: SeqNo) -> crate::Result<Option<InternalValue>> {
388 let super_version = self.snapshot_for_read(seqno)?;
389
390 Self::get_internal_entry_from_version(
391 &super_version,
392 key,
393 seqno,
394 self.config.comparator.as_ref(),
395 )
396 }
397
398 fn current_version(&self) -> Version {
399 self.version_history
400 .read()
401 .latest_version_ref()
402 .version
403 .clone()
404 }
405
406 #[cfg(feature = "std")]
407 fn refresh_table_checksum(
408 &self,
409 table_id: TableId,
410 checksum: crate::checksum::Checksum,
411 expected_restriction: Option<&crate::UserKey>,
412 ) -> crate::Result<crate::abstract_tree::ChecksumRefreshOutcome> {
413 use crate::abstract_tree::ChecksumRefreshOutcome;
414 // Same lock order as flush / compaction version installs: compaction
415 // state first, then the version history write lock. But the caller (the
416 // patrol reconcile) holds this table's HEAL LOCK across this call, and a
417 // concurrent tight-space compaction acquires that heal lock WHILE holding
418 // `compaction_state`. Blocking on `compaction_state` here would invert the
419 // order (heal_lock -> compaction_state on this path vs
420 // compaction_state -> heal_lock on the compaction path) and deadlock
421 // permanently. `try_lock` instead: a failed acquire means a compaction is
422 // mid-install, so skip this refresh — but report the skip as CONTENDED,
423 // not as a benign no-op: the healed bytes are durable while the manifest
424 // digest stays stale, so a "clean" report would mislead a later
425 // integrity check / checkpoint. The caller keeps the attestation and
426 // surfaces a finding; the next patrol retries once the compaction
427 // releases the state.
428 let Some(mut _compaction_state) = self.compaction_state.try_lock() else {
429 return Ok(ChecksumRefreshOutcome::Contended);
430 };
431 let mut version_lock = self.version_history.write();
432
433 // Under the install lock, resolve the CURRENT view of this table and
434 // reject the refresh if either it is gone (compacted away — nothing to
435 // refresh) or its restriction no longer matches the one `checksum` was
436 // computed for. A tight-space compaction can swap the captured view for a
437 // restricted same-id view (punching its prefix) between the caller's read
438 // and this lock; installing the caller's digest against a different
439 // restriction would record one the punched file can never match. Skipping
440 // leaves the current view's own (compaction-installed) digest in place for
441 // the next patrol to reconcile.
442 let restriction_matches = version_lock
443 .latest_version_ref()
444 .version
445 .iter_tables()
446 .find(|t| t.id() == table_id)
447 .is_some_and(|t| t.restrict_lower_bound() == expected_restriction);
448 if !restriction_matches {
449 // No-op: the manifest digest is unchanged, so the caller must keep the
450 // attestation for the next patrol.
451 return Ok(ChecksumRefreshOutcome::Stale);
452 }
453
454 version_lock
455 .upgrade_version(
456 &self.config.path,
457 |current| {
458 let mut copy = current.clone();
459 if let Some(next) = copy
460 .version
461 .with_refreshed_table_checksum(table_id, checksum)
462 {
463 copy.version = next;
464 }
465 Ok(copy)
466 },
467 &self.config.seqno,
468 &self.config.visible_seqno,
469 &*self.config.fs,
470 self.0.runtime_config.load_full(),
471 self.0.config.encryption.clone(),
472 crate::version::RetentionEffect::Keep,
473 )
474 .map(|()| ChecksumRefreshOutcome::Refreshed)
475 }
476
477 fn sync_mode(&self) -> crate::fs::SyncMode {
478 self.config.sync_mode
479 }
480
481 fn prefix_extractor(&self) -> Option<alloc::sync::Arc<dyn crate::prefix::PrefixExtractor>> {
482 self.config.prefix_extractor.clone()
483 }
484
485 fn storage_stats(&self) -> crate::Result<crate::StorageStats> {
486 // One version snapshot reused for the footprint and the full-compaction
487 // estimate below: a second `current_version()` could race a concurrent
488 // flush / compaction and mix two snapshots.
489 let version = self.current_version();
490 // Standard tree: SST values ARE user values (no KV separation).
491 let mut stats =
492 crate::storage_stats::compute_storage_stats(&version, self.is_compacting(), true)?;
493 // Fill the disk-aware capacity figures (quota + free-space probe) the
494 // version-only computation can't know.
495 let (capacity, available, compaction_possible) = self.admission_capacity(stats.used_bytes);
496 stats.capacity_bytes = capacity;
497 stats.available_bytes = available;
498 stats.compaction_possible = compaction_possible;
499 // When admission gating is active and a compaction is not already
500 // running, surface whether a full compaction has working room through the
501 // SAME two-layer check the compaction space gate enforces (logical quota +
502 // physical free per destination volume), so the reported status matches
503 // what the gate will admit. With gating off the gate never runs, so the
504 // status stays `Healthy` even though the backend can report a finite
505 // capacity.
506 if self.storage_admission_enabled()
507 && capacity.is_some()
508 && stats.status == crate::StorageStatus::Healthy
509 {
510 // A full compaction's transient output is bounded by the largest
511 // level's on-disk size, but it LANDS in the last configured level's
512 // volume (`level_count - 1`), which under tiered routing can be a
513 // different filesystem than the largest level. A standard tree has no
514 // blob relocation. Using the per-volume gate (not `available >=
515 // full_compaction_bytes` against the min-volume free) keeps the status
516 // from reporting tight when a routed merge would actually be admitted.
517 let sst_need = crate::storage_stats::full_compaction_demand_bytes(&version)?;
518 // `saturating_sub`: `level_count >= 1` always, so this is the last
519 // level index; the clamp only guards a degenerate zero-level config.
520 let sst_dest_level = self.0.config.level_count.saturating_sub(1);
521 let quota_headroom = self.quota_headroom(stats.used_bytes);
522 let full_fits = crate::compaction::worker::space_fits_two_layer(
523 &self.0.config,
524 quota_headroom,
525 sst_need,
526 sst_dest_level,
527 0,
528 );
529 stats.status = if full_fits {
530 crate::StorageStatus::FullCompactionAvailable
531 } else {
532 crate::StorageStatus::TightCompactionAvailable
533 };
534 }
535 // A closed admission gate is the operator-actionable state, so it takes
536 // precedence over the others (a read-only tree may well be compacting to
537 // reclaim space).
538 if self.is_read_only() {
539 stats.status = crate::StorageStatus::ReadOnlyOutOfSpace;
540 }
541 Ok(stats)
542 }
543
544 fn write_admission(&self) -> crate::Result<()> {
545 self.compute_write_admission()
546 }
547
548 fn write_backpressure(
549 &self,
550 strategy: &dyn crate::compaction::CompactionStrategy,
551 ) -> crate::Backpressure {
552 // Copy the thresholds out (BackpressureThresholds is Copy) so the
553 // arc-swap guard drops immediately; the off check short-circuits before
554 // touching the version, keeping the disabled path free.
555 let thresholds = self.0.runtime_config.load().backpressure;
556 if thresholds.is_off() {
557 return crate::Backpressure::None;
558 }
559 let version = self.current_version();
560 // L0 is the first level; its table (file) count is the count-trigger
561 // signal, matching the leveled `choose` trigger and the L0 term of
562 // `pending_compaction_bytes`.
563 let l0_count = version
564 .iter_levels()
565 .next()
566 .map_or(0, |level| level.table_count());
567 let pending = strategy.pending_compaction_bytes(&version);
568 crate::Backpressure::compute(l0_count, pending, &thresholds)
569 }
570
571 fn get_flush_lock(&self) -> FlushGuard<'_> {
572 self.flush_lock.lock()
573 }
574
575 #[cfg(feature = "metrics")]
576 fn metrics(&self) -> &Arc<crate::Metrics> {
577 &self.0.metrics
578 }
579
580 #[cfg(feature = "metrics")]
581 fn cache_stats(&self) -> crate::CacheStats {
582 let cache = &self.0.config.cache;
583 self.metrics().cache_stats(cache.size(), cache.capacity())
584 }
585
586 fn version_free_list_len(&self) -> usize {
587 self.version_history.read().free_list_len()
588 }
589
590 fn prefix<K: AsRef<[u8]>>(
591 &self,
592 prefix: K,
593 seqno: SeqNo,
594 index: Option<(Arc<Memtable>, SeqNo)>,
595 ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
596 match self.create_prefix(&prefix, seqno, index) {
597 Ok(iter) => Box::new(iter.map(|kv| IterGuardImpl::Standard(Guard(kv)))),
598 Err(e) => Box::new(core::iter::once(error_guard(e))),
599 }
600 }
601
602 fn range<K: AsRef<[u8]>, R: RangeBounds<K>>(
603 &self,
604 range: R,
605 seqno: SeqNo,
606 index: Option<(Arc<Memtable>, SeqNo)>,
607 ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
608 match self.create_range(&range, seqno, index) {
609 Ok(iter) => Box::new(iter.map(|kv| IterGuardImpl::Standard(Guard(kv)))),
610 Err(e) => Box::new(core::iter::once(error_guard(e))),
611 }
612 }
613
614 fn range_seekable<K: AsRef<[u8]>, R: RangeBounds<K>>(
615 &self,
616 range: R,
617 seqno: SeqNo,
618 index: Option<(Arc<Memtable>, SeqNo)>,
619 ) -> Box<dyn crate::iter_guard::SeekableGuardIter + 'static> {
620 let (lo, hi) = range_to_user_bounds(&range);
621 match self.create_seekable_range_bounds(lo, hi, seqno, index) {
622 Ok(inner) => Box::new(StandardSeekable { inner }),
623 Err(e) => Box::new(crate::iter_guard::FailedSeekable::new(e)),
624 }
625 }
626
627 fn batch_range_scan<K: AsRef<[u8]>, R: RangeBounds<K> + 'static, I: IntoIterator<Item = R>>(
628 &self,
629 intervals: I,
630 seqno: SeqNo,
631 index: Option<(Arc<Memtable>, SeqNo)>,
632 ) -> Box<dyn Iterator<Item = IterGuardImpl> + Send + 'static>
633 where
634 I::IntoIter: Send + 'static,
635 {
636 // Open the seekable iterator over the whole keyspace once; each interval
637 // is served by repositioning it (single per-SST setup, amortized).
638 let inner = match self.create_seekable_range_bounds(
639 Bound::Unbounded,
640 Bound::Unbounded,
641 seqno,
642 index,
643 ) {
644 Ok(inner) => inner,
645 Err(e) => return Box::new(core::iter::once(error_guard(e))),
646 };
647 let intervals = intervals.into_iter().map(|r| range_to_user_bounds(&r));
648 Box::new(crate::range::BatchRangeScan::new(inner, intervals).map(standard_guard))
649 }
650
651 /// Returns the number of tombstones in the tree.
652 fn tombstone_count(&self) -> u64 {
653 self.current_version()
654 .iter_tables()
655 .map(Table::tombstone_count)
656 .sum()
657 }
658
659 /// Returns the number of weak tombstones (single deletes) in the tree.
660 fn weak_tombstone_count(&self) -> u64 {
661 self.current_version()
662 .iter_tables()
663 .map(Table::weak_tombstone_count)
664 .sum()
665 }
666
667 /// Returns the number of value entries that become reclaimable once weak tombstones can be GC'd.
668 fn weak_tombstone_reclaimable_count(&self) -> u64 {
669 self.current_version()
670 .iter_tables()
671 .map(Table::weak_tombstone_reclaimable)
672 .sum()
673 }
674
675 fn drop_range<K: AsRef<[u8]>, R: RangeBounds<K>>(&self, range: R) -> crate::Result<()> {
676 let (bounds, is_empty) = Self::range_bounds_to_owned_bounds(&range);
677
678 if is_empty {
679 return Ok(());
680 }
681
682 let strategy = Arc::new(crate::compaction::drop_range::Strategy::new(bounds));
683
684 // IMPORTANT: Write lock so we can be the only compaction going on
685 let _lock = self.0.major_compaction_lock.write();
686
687 log::info!("Starting drop_range compaction");
688 self.inner_compact(strategy, 0)?;
689 Ok(())
690 }
691
692 fn clear(&self) -> crate::Result<()> {
693 let config = self.tree_config();
694 let mut versions = self.get_version_history_lock();
695
696 // Pre-clear snapshot: every table + blob file it references becomes
697 // garbage the moment the new empty version is installed.
698 let prior = versions.latest_version();
699
700 versions.upgrade_version(
701 &config.path,
702 |v| {
703 let mut copy = v.clone();
704 copy.active_memtable = Arc::new(Memtable::new(
705 self.memtable_id_counter.next(),
706 self.config.comparator.clone(),
707 ));
708 copy.sealed_memtables = Arc::default();
709 copy.version = Version::new(v.version.id() + 1, self.tree_type());
710 Ok(copy)
711 },
712 &config.seqno,
713 &config.visible_seqno,
714 &*config.fs,
715 self.0.runtime_config.load_full(),
716 self.0.config.encryption.clone(),
717 // Every table goes: no snapshot up to this install is servable
718 // after a reopen.
719 crate::version::RetentionEffect::DropsData,
720 )?;
721
722 // Release the history's hold on the now-obsolete versions; only the new
723 // empty version remains. `prior` still holds them, so nothing reaches
724 // refcount zero yet.
725 versions.drain_obsolete_to_latest();
726 drop(versions); // release the version-history lock before any fs work
727
728 // Mark every obsolete table / blob file deleted so the file is
729 // reclaimed (Inner::Drop) once its last reference is released. A
730 // concurrent reader still holding the pre-clear snapshot keeps its own
731 // clone alive, deferring physical deletion until it finishes — the
732 // version-history Arc refcount is the MVCC guard, so reclaim never
733 // races a live read. Tables with no other live reference are reclaimed
734 // as `prior` drops at the end of this call.
735 for table in prior.version.iter_tables() {
736 table.mark_as_deleted();
737 }
738 for blob_file in prior.version.blob_files.iter() {
739 blob_file.mark_as_deleted();
740 }
741
742 Ok(())
743 }
744
745 #[doc(hidden)]
746 fn major_compact(
747 &self,
748 target_size: u64,
749 seqno_threshold: SeqNo,
750 ) -> crate::Result<crate::compaction::CompactionResult> {
751 let strategy = Arc::new(crate::compaction::major::Strategy::new(target_size));
752
753 // IMPORTANT: Write lock so we can be the only compaction going on
754 let _lock = self.0.major_compaction_lock.write();
755
756 log::info!("Starting major compaction");
757 self.inner_compact(strategy, seqno_threshold)
758 }
759
760 fn l0_run_count(&self) -> usize {
761 self.current_version()
762 .level(0)
763 .map(|x| x.run_count())
764 .unwrap_or_default()
765 }
766
767 fn size_of<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<u32>> {
768 #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
769 Ok(self.get(key, seqno)?.map(|x| x.len() as u32))
770 }
771
772 fn filter_size(&self) -> u64 {
773 self.current_version()
774 .iter_tables()
775 .map(Table::filter_size)
776 .map(u64::from)
777 .sum()
778 }
779
780 fn pinned_filter_size(&self) -> usize {
781 self.current_version()
782 .iter_tables()
783 .map(Table::pinned_filter_size)
784 .sum()
785 }
786
787 fn pinned_block_index_size(&self) -> usize {
788 self.current_version()
789 .iter_tables()
790 .map(Table::pinned_block_index_size)
791 .sum()
792 }
793
794 fn sealed_memtable_count(&self) -> usize {
795 self.version_history
796 .read()
797 .latest_version()
798 .sealed_memtables
799 .len()
800 }
801
802 fn flush_to_tables_with_rt(
803 &self,
804 stream: impl Iterator<Item = crate::Result<InternalValue>>,
805 range_tombstones: Vec<crate::range_tombstone::RangeTombstone>,
806 ) -> crate::Result<Option<(Vec<Table>, Option<Vec<BlobFile>>)>> {
807 use crate::table::multi_writer::MultiWriter;
808 use crate::time::Instant;
809
810 let start = Instant::now();
811
812 let (folder, level_fs) = self.config.tables_folder_for_level(0);
813
814 let data_block_size = self.config.data_block_size_policy.get(0);
815
816 let data_block_restart_interval = self.config.data_block_restart_interval_policy.get(0);
817 let index_block_restart_interval = self.config.index_block_restart_interval_policy.get(0);
818
819 let data_block_compression = self.config.data_block_compression_policy.get(0);
820 let index_block_compression = self.config.index_block_compression_policy.get(0);
821
822 let data_block_hash_ratio = self.config.data_block_hash_ratio_policy.get(0);
823
824 let index_partitioning = self.config.index_block_partitioning_policy.get(0);
825 let filter_partitioning = self.config.filter_block_partitioning_policy.get(0);
826
827 // One runtime-config snapshot for the whole flush writer setup. The
828 // index spill threshold, `seqno_in_index`, and the per-KV checksum
829 // policy are all live (toggleable via `update_runtime_config`); reading
830 // `load_full()` per field could straddle a concurrent update and mix two
831 // snapshots into one SST. Compaction is the migration mechanism, so a
832 // toggle takes effect on the next flush / compaction.
833 let rc = self.0.runtime_config.load_full();
834
835 log::debug!(
836 "Flushing memtable(s) to {}, data_block_restart_interval={data_block_restart_interval}, index_block_restart_interval={index_block_restart_interval}, data_block_size={data_block_size}, data_block_compression={data_block_compression:?}, index_block_compression={index_block_compression:?}",
837 folder.display(),
838 );
839
840 let mut table_writer = MultiWriter::new(
841 folder.clone(),
842 self.table_id_counter.clone(),
843 64 * 1_024 * 1_024,
844 0,
845 level_fs.clone(),
846 )?
847 .set_comparator(self.config.comparator.clone())
848 .use_data_block_restart_interval(data_block_restart_interval)
849 .use_index_block_restart_interval(index_block_restart_interval)
850 .use_data_block_compression(data_block_compression)
851 .use_index_block_compression(index_block_compression)
852 .use_data_block_size(data_block_size)
853 .use_data_block_hash_ratio(data_block_hash_ratio)
854 .use_bloom_policy({
855 use crate::config::FilterPolicyEntry::{Bloom, None};
856 use crate::table::filter::BloomConstructionPolicy;
857
858 match self.config.filter_policy.get(0) {
859 Bloom(policy) => policy,
860 None => BloomConstructionPolicy::BitsPerKey(0.0),
861 }
862 });
863
864 if index_partitioning {
865 // Size-adaptive: single-level index for small SSTs (where pinning
866 // the whole index is cheap and a two-level lookup is pure overhead),
867 // spilling to a partitioned index only once the index grows past the
868 // threshold. Recovers the point-read cost of an unconditional
869 // two-level index on small/medium SSTs.
870 table_writer = table_writer.use_adaptive_index(rc.index_partition_spill_threshold);
871 }
872 if filter_partitioning {
873 table_writer = table_writer.use_partitioned_filter();
874 }
875
876 table_writer = table_writer.use_prefix_extractor(self.config.prefix_extractor.clone());
877 table_writer = table_writer.use_encryption(self.config.encryption.clone());
878 // ECC scheme from the live runtime snapshot (same as `seqno_in_index`
879 // / `kv_checksums` below), so a flush after a scheme change writes the
880 // SST with the current scheme rather than the startup one.
881 table_writer = table_writer.use_page_ecc(self.config.page_ecc, rc.ecc_scheme);
882 table_writer = table_writer.use_sync_mode(self.config.sync_mode);
883
884 table_writer = table_writer.use_seqno_in_index(rc.seqno_in_index);
885 table_writer = table_writer.use_zone_map(rc.zone_map);
886 table_writer = table_writer.use_columnar(rc.columnar);
887 table_writer = table_writer.use_disable_cow_on_sst(rc.disable_cow_on_sst_files);
888 // `Off` (default) emits no per-KV footer and leaves the data-block
889 // payload encoding unchanged (the V5 header carries a block_flags byte
890 // and the meta block a descriptor key regardless, so the on-disk bytes
891 // are not identical to a pre-V5 table).
892 table_writer = table_writer.use_kv_checksums(rc.kv_checksums, rc.kv_checksum_algo);
893 // Flush writes level 0; resolve that level's locator policy entry.
894 table_writer = table_writer.use_locator(self.config.locator_policy.get(0));
895
896 #[cfg(zstd_any)]
897 {
898 table_writer = table_writer.use_zstd_dictionary(self.config.zstd_dictionary.clone());
899 }
900
901 // Parallel block compression for the flush writer, on the same pool the
902 // compaction writers use. Engaged only when the per-block transform does
903 // real CPU work (a codec, encryption, or page ECC): with the identity
904 // transform the pipeline's owned buffer + queue hop per block buys
905 // nothing over the serial reusable-buffer path. Safe wherever the host
906 // runs this flush — even on a thread of an injected pool — because the
907 // pipeline's help-first drain executes queued jobs inline instead of
908 // waiting on a saturated pool (see `parallel_compressor`).
909 #[cfg(feature = "std")]
910 {
911 let transform_does_work = data_block_compression != crate::CompressionType::None
912 || self.config.encryption.is_some()
913 || self.config.page_ecc;
914 if transform_does_work {
915 table_writer = table_writer.use_parallel_compression(
916 self.config.compaction_pool.clone(),
917 self.config.compaction_threads,
918 );
919 }
920 }
921
922 // Set range tombstones BEFORE writing KV items so that if MultiWriter
923 // rotates to a new table during the write loop, earlier tables already
924 // carry the RT metadata.
925 table_writer.set_range_tombstones(range_tombstones);
926
927 for item in stream {
928 table_writer.write(item?)?;
929 }
930
931 let result = table_writer.finish()?;
932
933 log::debug!("Flushed memtable(s) in {:?}", start.elapsed());
934
935 let pin_filter = self.config.filter_block_pinning_policy.get(0);
936 let pin_index = self.config.index_block_pinning_policy.get(0);
937
938 // Load tables
939 let tables = result
940 .into_iter()
941 .map(|(table_id, checksum)| -> crate::Result<Table> {
942 let mut params = crate::table::RecoverParams::new(
943 folder.join(table_id.to_string()),
944 checksum,
945 table_id,
946 level_fs.clone(),
947 self.config.comparator.clone(),
948 self.config.cache.clone(),
949 );
950 params.tree_id = self.id;
951 params
952 .descriptor_table
953 .clone_from(&self.config.descriptor_table);
954 params.pin_filter = pin_filter;
955 params.pin_index = pin_index;
956 params.encryption.clone_from(&self.config.encryption);
957 #[cfg(zstd_any)]
958 {
959 params
960 .zstd_dictionary
961 .clone_from(&self.config.zstd_dictionary);
962 }
963 #[cfg(feature = "metrics")]
964 {
965 params.metrics = self.metrics.clone();
966 }
967 Table::recover(params)
968 })
969 .collect::<crate::Result<Vec<_>>>()?;
970
971 // Return Some even when tables is empty (RT-only flush): the caller
972 // (AbstractTree::flush) handles empty tables by re-inserting RTs into
973 // the active memtable and still needs to delete sealed memtables.
974 Ok(Some((tables, None)))
975 }
976
977 #[expect(clippy::significant_drop_tightening)]
978 fn register_tables(
979 &self,
980 tables: &[Table],
981 blob_files: Option<&[BlobFile]>,
982 frag_map: Option<crate::blob_tree::FragmentationMap>,
983 sealed_memtables_to_delete: &[crate::tree::inner::MemtableId],
984 gc_watermark: SeqNo,
985 ) -> crate::Result<()> {
986 log::trace!(
987 "Registering {} tables, {} blob files",
988 tables.len(),
989 blob_files.map(<[BlobFile]>::len).unwrap_or_default(),
990 );
991
992 // Wire the tree-wide deletion pause into every fresh table / blob
993 // file so an in-flight checkpoint defers their cleanup if they
994 // later get marked `is_deleted` by compaction.
995 let sinks = crate::table::TableSinks {
996 deletion_pause: &self.deletion_pause,
997 heal_hints: &self.heal_hints,
998 #[cfg(feature = "std")]
999 background_deleter: Some(&self.background_deleter),
1000 };
1001 for table in tables {
1002 table.bind_to_tree(&sinks);
1003 }
1004 for bf in blob_files.unwrap_or(&[]) {
1005 bf.bind_to_tree(&sinks);
1006 }
1007
1008 let mut _compaction_state = self.compaction_state.lock();
1009 let mut version_lock = self.version_history.write();
1010
1011 version_lock.upgrade_version(
1012 &self.config.path,
1013 |current| {
1014 let mut copy = current.clone();
1015
1016 let ctx = crate::version::TransformContext::new(self.config.comparator.as_ref());
1017 copy.version = copy.version.with_new_l0_run(
1018 tables,
1019 blob_files,
1020 frag_map.filter(|x| !x.is_empty()),
1021 &ctx,
1022 );
1023
1024 for &table_id in sealed_memtables_to_delete {
1025 log::trace!("releasing sealed memtable #{table_id}");
1026 copy.sealed_memtables = Arc::new(copy.sealed_memtables.remove(table_id));
1027 }
1028
1029 Ok(copy)
1030 },
1031 &self.config.seqno,
1032 &self.config.visible_seqno,
1033 &*self.config.fs,
1034 self.0.runtime_config.load_full(),
1035 self.0.config.encryption.clone(),
1036 // A flush only adds a run; the watermark below prunes the
1037 // in-memory history, it discards no data.
1038 crate::version::RetentionEffect::Keep,
1039 )?;
1040
1041 if let Err(e) = version_lock.maintenance(&self.config.path, gc_watermark, &*self.config.fs)
1042 {
1043 log::warn!("Version GC failed: {e:?}");
1044 }
1045
1046 Ok(())
1047 }
1048
1049 fn clear_active_memtable(&self) {
1050 use crate::tree::sealed::SealedMemtables;
1051
1052 let mut version_history_lock = self.version_history.write();
1053 let super_version = version_history_lock.latest_version();
1054
1055 if super_version.active_memtable.is_empty() {
1056 return;
1057 }
1058
1059 let mut copy = version_history_lock.latest_version();
1060 copy.active_memtable = Arc::new(Memtable::new(
1061 self.memtable_id_counter.next(),
1062 self.config.comparator.clone(),
1063 ));
1064 copy.sealed_memtables = Arc::new(SealedMemtables::default());
1065
1066 // Rotate does not modify the memtable, so it cannot break snapshots
1067 copy.seqno = super_version.seqno;
1068
1069 version_history_lock.replace_latest_version(copy);
1070
1071 log::trace!("cleared active memtable");
1072 }
1073
1074 fn compact(
1075 &self,
1076 strategy: Arc<dyn CompactionStrategy>,
1077 seqno_threshold: SeqNo,
1078 ) -> crate::Result<crate::compaction::CompactionResult> {
1079 // NOTE: Read lock major compaction lock
1080 // That way, if a major compaction is running, we cannot proceed
1081 // But in general, parallel (non-major) compactions can occur
1082 let _lock = self.0.major_compaction_lock.read();
1083
1084 self.inner_compact(strategy, seqno_threshold)
1085 }
1086
1087 fn get_next_table_id(&self) -> TableId {
1088 self.0.get_next_table_id()
1089 }
1090
1091 fn tree_config(&self) -> &Config {
1092 &self.config
1093 }
1094
1095 fn active_memtable(&self) -> Arc<Memtable> {
1096 self.version_history
1097 .read()
1098 .latest_version_ref()
1099 .active_memtable
1100 .clone()
1101 }
1102
1103 #[expect(clippy::significant_drop_tightening)]
1104 fn rotate_memtable(&self) -> Option<Arc<Memtable>> {
1105 let mut version_history_lock = self.version_history.write();
1106 let super_version = version_history_lock.latest_version();
1107
1108 if super_version.active_memtable.is_empty() {
1109 return None;
1110 }
1111
1112 let yanked_memtable = super_version.active_memtable;
1113
1114 let mut copy = version_history_lock.latest_version();
1115 copy.active_memtable = Arc::new(Memtable::new(
1116 self.memtable_id_counter.next(),
1117 self.config.comparator.clone(),
1118 ));
1119 copy.sealed_memtables =
1120 Arc::new(super_version.sealed_memtables.add(yanked_memtable.clone()));
1121
1122 // Rotate does not modify the memtable so it cannot break snapshots
1123 copy.seqno = super_version.seqno;
1124
1125 version_history_lock.replace_latest_version(copy);
1126
1127 log::trace!(
1128 "rotate: added memtable id={} to sealed memtables",
1129 yanked_memtable.id,
1130 );
1131
1132 Some(yanked_memtable)
1133 }
1134
1135 fn table_count(&self) -> usize {
1136 self.current_version().table_count()
1137 }
1138
1139 fn level_table_count(&self, idx: usize) -> Option<usize> {
1140 self.current_version().level(idx).map(|x| x.table_count())
1141 }
1142
1143 fn approximate_len(&self) -> usize {
1144 let super_version = self.version_history.read().latest_version();
1145
1146 let tables_item_count = self
1147 .current_version()
1148 .iter_tables()
1149 .map(|x| x.metadata.item_count)
1150 .sum::<u64>();
1151
1152 let memtable_count = super_version.active_memtable.len() as u64;
1153 let sealed_count = super_version
1154 .sealed_memtables
1155 .iter()
1156 .map(|mt| mt.len())
1157 .sum::<usize>() as u64;
1158
1159 #[expect(clippy::expect_used, reason = "result should fit into usize")]
1160 (memtable_count + sealed_count + tables_item_count)
1161 .try_into()
1162 .expect("approximate_len too large for usize")
1163 }
1164
1165 fn disk_space(&self) -> u64 {
1166 self.current_version()
1167 .iter_levels()
1168 .map(super::version::Level::size)
1169 .sum()
1170 }
1171
1172 fn approximate_range_stats<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
1173 &self,
1174 range: R,
1175 seqno: SeqNo,
1176 ) -> crate::Result<crate::ApproximateRangeStats> {
1177 use crate::table::block_index::BlockIndex;
1178 use core::ops::Bound;
1179
1180 let lo: Bound<&[u8]> = match range.start_bound() {
1181 Bound::Included(k) => Bound::Included(k.as_ref()),
1182 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1183 Bound::Unbounded => Bound::Unbounded,
1184 };
1185 let hi: Bound<&[u8]> = match range.end_bound() {
1186 Bound::Included(k) => Bound::Included(k.as_ref()),
1187 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1188 Bound::Unbounded => Bound::Unbounded,
1189 };
1190 let bounds = (lo, hi);
1191
1192 let mut bytes: u64 = 0;
1193 let mut key_count: u64 = 0;
1194
1195 // Use ONE snapshot at the requested seqno for both the SST and memtable
1196 // contributions, so the estimate reflects the same visibility as a read
1197 // at `seqno` (no entries newer than the snapshot, and a consistent set of
1198 // tables + memtables even during a concurrent flush / compaction).
1199 let comparator = self.config.comparator.as_ref();
1200 let super_version = self
1201 .version_history
1202 .read()
1203 .get_version_for_snapshot(seqno)?;
1204
1205 // SST contribution: interpolate data-block offsets at the boundaries
1206 // (block granularity), no data-block reads. For a KV-separated SST the
1207 // referenced blob bytes are apportioned by the same in-range fraction.
1208 for table in super_version.version.iter_tables() {
1209 // Comparator-aware overlap: a custom user comparator orders keys
1210 // differently from raw bytes, so use the same comparison the read
1211 // path does instead of default byte ordering.
1212 if !table
1213 .metadata
1214 .key_range
1215 .overlaps_with_bounds_cmp(&bounds, comparator)
1216 {
1217 continue;
1218 }
1219 // The block index is keyed by the table-LOCAL seqno; a bulk-ingested
1220 // table carries a non-zero global seqno, so translate the snapshot
1221 // seqno the same way the read path does before seeking it. A snapshot
1222 // below the table's base means the table postdates it and contributes
1223 // nothing to the estimate, so skip it (`checked_sub` yields `None`).
1224 let Some(table_seqno) = seqno.checked_sub(table.global_seqno()) else {
1225 continue;
1226 };
1227 // The translation alone is not the visibility rule: a table with
1228 // `global_seqno == 0` passes it whatever its entries hold, so one
1229 // holding nothing but seqno 100 would charge rows and bytes to a
1230 // query at seqno 50 that reads none of them. Ask the same
1231 // classification the read path uses.
1232 if table.seqno_visibility(seqno) == crate::table::SeqnoVisibility::None {
1233 continue;
1234 }
1235
1236 // data_end = the data section's byte extent = last data block's end.
1237 let Some(last) = table.block_index.iter().next_back() else {
1238 continue;
1239 };
1240 let last = last?;
1241 let data_end = *last.offset() + u64::from(last.size());
1242 if data_end == 0 {
1243 continue;
1244 }
1245
1246 // The data block that would contain `key`, as (start, end) byte
1247 // offsets, or `None` when `key` is past the last block. The full
1248 // extent is returned so the lower bound counts from the block start
1249 // and the upper bound INCLUDES it (a range inside a single block must
1250 // not collapse to zero bytes).
1251 let block_span = |key: &[u8]| -> crate::Result<Option<(u64, u64)>> {
1252 let Some(mut iter) = table.block_index.forward_reader(key, table_seqno) else {
1253 return Ok(None);
1254 };
1255 let Some(handle) = iter.next() else {
1256 return Ok(None);
1257 };
1258 let h = handle?;
1259 let start = *h.offset();
1260 Ok(Some((start, (start + u64::from(h.size())).min(data_end))))
1261 };
1262 let off_lo = match lo {
1263 Bound::Included(k) | Bound::Excluded(k) => {
1264 block_span(k)?.map_or(data_end, |(start, _)| start)
1265 }
1266 Bound::Unbounded => 0,
1267 };
1268 // Tight-space restriction: a restricted table view serves only keys
1269 // at or above its lower bound, with the punched-out prefix served by
1270 // the replacement table. Raise the lower offset to that bound so the
1271 // prefix is not double-counted (matching how scans skip it).
1272 let off_lo = match table.restrict_lower_bound() {
1273 Some(rb) => {
1274 off_lo.max(block_span(rb.as_ref())?.map_or(data_end, |(start, _)| start))
1275 }
1276 None => off_lo,
1277 };
1278 let off_hi = match hi {
1279 Bound::Included(k) | Bound::Excluded(k) => {
1280 block_span(k)?.map_or(data_end, |(_, end)| end)
1281 }
1282 Bound::Unbounded => data_end,
1283 };
1284 let idx_bytes = off_hi.saturating_sub(off_lo);
1285 if idx_bytes == 0 {
1286 continue;
1287 }
1288
1289 // fraction = idx_bytes / data_end, in u128 to avoid overflow. For a
1290 // standard tree `idx_bytes` already includes the inline values. For a
1291 // KV-separated SST it covers only the key + pointer bytes, so the
1292 // SST's referenced blob bytes (recorded per-SST at both flush and
1293 // compaction) are apportioned by the same in-range fraction; blob
1294 // files are not key-indexed, so this fraction is the finest estimate
1295 // possible without reading data blocks.
1296 let num = u128::from(idx_bytes);
1297 let den = u128::from(data_end);
1298 let blob_bytes = table.referenced_blob_bytes()?;
1299 // `num <= den` (both offsets are bounded by `data_end`), so
1300 // `x * num / den <= x` and the u128 -> u64 narrowing is total —
1301 // no fallback value to mask a range error with.
1302 #[expect(
1303 clippy::cast_possible_truncation,
1304 reason = "num <= den, so the quotient never exceeds the u64 input"
1305 )]
1306 let sst_blob = (u128::from(blob_bytes) * num / den) as u64;
1307 // Round up to at least one entry: a non-empty byte span over a
1308 // non-empty SST always covers at least one row, so a narrow range
1309 // never reports bytes with a zero key count.
1310 #[expect(
1311 clippy::cast_possible_truncation,
1312 reason = "num <= den, so the quotient never exceeds the u64 input"
1313 )]
1314 let in_range_entries =
1315 ((u128::from(table.metadata.item_count) * num / den) as u64).max(1);
1316 bytes = bytes.saturating_add(idx_bytes).saturating_add(sst_blob);
1317 key_count = key_count.saturating_add(in_range_entries);
1318 }
1319
1320 // Memtable contribution: the in-range fraction of each memtable's
1321 // approximate size. Built from the SAME snapshot and the SAME
1322 // `range_internal` + internal-key bounds the read path uses (range.rs),
1323 // so the counted slice matches what a read at `seqno` would traverse.
1324 let mt_range = (
1325 match lo {
1326 Bound::Included(k) => {
1327 Bound::Included(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Tombstone))
1328 }
1329 Bound::Excluded(k) => {
1330 Bound::Excluded(InternalKey::new(k, 0, crate::ValueType::Tombstone))
1331 }
1332 Bound::Unbounded => Bound::Unbounded,
1333 },
1334 match hi {
1335 Bound::Included(k) => {
1336 Bound::Included(InternalKey::new(k, 0, crate::ValueType::Value))
1337 }
1338 Bound::Excluded(k) => {
1339 Bound::Excluded(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Value))
1340 }
1341 Bound::Unbounded => Bound::Unbounded,
1342 },
1343 );
1344 let estimate = |mt: &crate::Memtable| -> (u64, u64) {
1345 let total = mt.len() as u64;
1346 if total == 0 {
1347 return (0, 0);
1348 }
1349 // Count only entries visible at the snapshot (the same seqno cutoff
1350 // reads apply), so the estimate excludes writes newer than `seqno`.
1351 let count = mt
1352 .range_internal(mt_range.clone())
1353 .filter(|kv| kv.key.seqno < seqno)
1354 .count() as u64;
1355 if count == 0 {
1356 return (0, 0);
1357 }
1358 // `count <= total` (the counted entries are a filtered subset of
1359 // the memtable), so the quotient never exceeds the u64 `size()`
1360 // and the narrowing is total.
1361 #[expect(
1362 clippy::cast_possible_truncation,
1363 reason = "count <= total, so the quotient never exceeds the u64 input"
1364 )]
1365 let mt_bytes = (u128::from(mt.size()) * u128::from(count) / u128::from(total)) as u64;
1366 (mt_bytes, count)
1367 };
1368 let (b, c) = estimate(&super_version.active_memtable);
1369 bytes = bytes.saturating_add(b);
1370 key_count = key_count.saturating_add(c);
1371 for mt in super_version.sealed_memtables.iter() {
1372 let (b, c) = estimate(mt);
1373 bytes = bytes.saturating_add(b);
1374 key_count = key_count.saturating_add(c);
1375 }
1376
1377 Ok(crate::ApproximateRangeStats { bytes, key_count })
1378 }
1379
1380 fn approximate_range_cardinality<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
1381 &self,
1382 range: R,
1383 seqno: SeqNo,
1384 ) -> crate::Result<crate::RangeCardinality> {
1385 use crate::table::block_index::BlockIndex;
1386 use core::cmp::Ordering;
1387 use core::ops::Bound;
1388
1389 let lo: Bound<&[u8]> = match range.start_bound() {
1390 Bound::Included(k) => Bound::Included(k.as_ref()),
1391 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1392 Bound::Unbounded => Bound::Unbounded,
1393 };
1394 let hi: Bound<&[u8]> = match range.end_bound() {
1395 Bound::Included(k) => Bound::Included(k.as_ref()),
1396 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1397 Bound::Unbounded => Bound::Unbounded,
1398 };
1399 let bounds = (lo, hi);
1400 let comparator = self.config.comparator.as_ref();
1401 let super_version = self
1402 .version_history
1403 .read()
1404 .get_version_for_snapshot(seqno)?;
1405
1406 let mut rows: u64 = 0;
1407 let mut total_rows: u64 = 0;
1408
1409 for table in super_version.version.iter_tables() {
1410 // Snapshot visibility is settled BEFORE the denominator grows. A
1411 // table no read at `seqno` can see is not part of the dataset the
1412 // selectivity describes, and counting it there while excluding it
1413 // from `rows` reports a full-keyspace query as selecting half the
1414 // tree. Out-of-RANGE tables do belong in the denominator, which is
1415 // why that check stays below it.
1416 //
1417 // A snapshot below the table's base means the table postdates it
1418 // (`checked_sub` yields `None`).
1419 let Some(table_seqno) = seqno.checked_sub(table.global_seqno()) else {
1420 continue;
1421 };
1422 // Wholly above the snapshot: invisible to a read at `seqno`, so it
1423 // contributes nothing here either (mirrors approximate_range_stats).
1424 if table.seqno_visibility(seqno) == crate::table::SeqnoVisibility::None {
1425 continue;
1426 }
1427 // What this VIEW serves, not what the file holds: a tight-space
1428 // restricted table's metadata still counts the punched-out prefix
1429 // its replacement now owns, while the numerator below starts at the
1430 // restriction — so charging the prefix here would report a
1431 // full-keyspace query as selecting a fraction of the tree.
1432 total_rows = total_rows.saturating_add(table.live_item_count()?);
1433 if !table
1434 .metadata
1435 .key_range
1436 .overlaps_with_bounds_cmp(&bounds, comparator)
1437 {
1438 continue;
1439 }
1440 // Honor a tight-space restricted view: keys below
1441 // `restrict_lower_bound()` are the punched-out prefix served by the
1442 // replacement table, so raise this table's effective lower bound to
1443 // it (mirrors approximate_range_stats) and never charge that prefix.
1444 let eff_lo = effective_lower_bound(
1445 lo,
1446 table.restrict_lower_bound().map(AsRef::as_ref),
1447 comparator,
1448 );
1449 let zone_map = &table.zone_map;
1450 // A COLUMNAR block's per-column bounds are recorded in BYTE order,
1451 // which is the only ordering a value column has. Comparing them
1452 // with a non-lexicographic user comparator reads the recorded
1453 // minimum as a comparator maximum, so the walk stops before blocks
1454 // that overlap the query: the shortcut is skipped for such trees
1455 // and the byte-fraction estimate below answers instead. A row
1456 // block's bounds are the block's first and last key, already in
1457 // comparator order, so the default path is unaffected.
1458 let zone_bounds_ordered = comparator.is_lexicographic() || !table.metadata.columnar;
1459 if !zone_map.is_empty() && zone_bounds_ordered {
1460 // Zone map present: sum the per-block row counts of blocks whose
1461 // key range overlaps the query. A block is past the range once its
1462 // minimum key is above the upper bound; the boundary block at the
1463 // effective lower bound is counted in full (block granularity). A
1464 // range that lands in a key-space gap legitimately yields zero, so
1465 // this path is authoritative and never falls back to the byte fraction.
1466 let reader = match eff_lo {
1467 Bound::Included(k) | Bound::Excluded(k) => {
1468 table.block_index.forward_reader(k, table_seqno)
1469 }
1470 Bound::Unbounded => Some(table.block_index.iter()),
1471 };
1472 if let Some(reader) = reader {
1473 for handle in reader {
1474 let handle = handle?;
1475 let Some(col) = zone_map
1476 .columns_for(*handle.offset())
1477 .and_then(|c| c.first())
1478 else {
1479 continue;
1480 };
1481 let above_hi = match hi {
1482 Bound::Included(hk) => {
1483 comparator.compare(&col.min, hk) == Ordering::Greater
1484 }
1485 Bound::Excluded(hk) => {
1486 comparator.compare(&col.min, hk) != Ordering::Less
1487 }
1488 Bound::Unbounded => false,
1489 };
1490 if above_hi {
1491 break;
1492 }
1493 rows = rows.saturating_add(u64::from(col.row_count));
1494 }
1495 }
1496 } else if let Some(last) = table.block_index.iter().next_back() {
1497 // No zone map: apportion item_count by the in-range
1498 // data-block byte fraction, mirroring approximate_range_stats.
1499 let last = last?;
1500 let data_end = *last.offset() + u64::from(last.size());
1501 if data_end > 0 {
1502 let off = |key: &[u8], end: bool| -> crate::Result<u64> {
1503 match table.block_index.forward_reader(key, table_seqno) {
1504 Some(mut it) => match it.next() {
1505 Some(h) => {
1506 let h = h?;
1507 Ok(if end {
1508 (*h.offset() + u64::from(h.size())).min(data_end)
1509 } else {
1510 *h.offset()
1511 })
1512 }
1513 None => Ok(data_end),
1514 },
1515 None => Ok(data_end),
1516 }
1517 };
1518 let off_lo = match eff_lo {
1519 Bound::Included(k) | Bound::Excluded(k) => off(k, false)?,
1520 Bound::Unbounded => 0,
1521 };
1522 let off_hi = match hi {
1523 Bound::Included(k) | Bound::Excluded(k) => off(k, true)?,
1524 Bound::Unbounded => data_end,
1525 };
1526 let idx_bytes = off_hi.saturating_sub(off_lo);
1527 if idx_bytes > 0 {
1528 // `idx_bytes <= data_end` (both offsets are bounded by
1529 // `data_end`), so the quotient never exceeds the u64
1530 // `item_count` and the narrowing is total.
1531 #[expect(
1532 clippy::cast_possible_truncation,
1533 reason = "idx_bytes <= data_end, so the quotient never exceeds the u64 input"
1534 )]
1535 let est = ((u128::from(table.metadata.item_count) * u128::from(idx_bytes)
1536 / u128::from(data_end)) as u64)
1537 .max(1);
1538 rows = rows.saturating_add(est);
1539 }
1540 }
1541 }
1542 }
1543
1544 // Memtables: count the in-range, snapshot-visible entries and add them to
1545 // both the matched rows and the total (matching the SST accounting).
1546 let mt_range = (
1547 match lo {
1548 Bound::Included(k) => {
1549 Bound::Included(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Tombstone))
1550 }
1551 Bound::Excluded(k) => {
1552 Bound::Excluded(InternalKey::new(k, 0, crate::ValueType::Tombstone))
1553 }
1554 Bound::Unbounded => Bound::Unbounded,
1555 },
1556 match hi {
1557 Bound::Included(k) => {
1558 Bound::Included(InternalKey::new(k, 0, crate::ValueType::Value))
1559 }
1560 Bound::Excluded(k) => {
1561 Bound::Excluded(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Value))
1562 }
1563 Bound::Unbounded => Bound::Unbounded,
1564 },
1565 );
1566 let mut add_memtable = |mt: &crate::Memtable| {
1567 // The denominator counts what a read at this snapshot can SEE, the
1568 // same rule the SST loop applies: an entry at or above `seqno` is
1569 // filtered out of the numerator below, so counting it here would
1570 // report a full-keyspace query as selecting half of what it sees.
1571 let visible = mt.iter().filter(|kv| kv.key.seqno < seqno).count() as u64;
1572 total_rows = total_rows.saturating_add(visible);
1573 let in_range = mt
1574 .range_internal(mt_range.clone())
1575 .filter(|kv| kv.key.seqno < seqno)
1576 .count() as u64;
1577 rows = rows.saturating_add(in_range);
1578 };
1579 add_memtable(&super_version.active_memtable);
1580 for mt in super_version.sealed_memtables.iter() {
1581 add_memtable(mt);
1582 }
1583
1584 // selectivity is an approximate ratio; u64 row counts are well within
1585 // f64's exact-integer range (2^52) for any realistic table.
1586 #[expect(
1587 clippy::cast_precision_loss,
1588 reason = "row counts never approach 2^52; the ratio is approximate anyway"
1589 )]
1590 let selectivity = if total_rows == 0 {
1591 0.0
1592 } else {
1593 (rows.min(total_rows) as f64) / (total_rows as f64)
1594 };
1595 Ok(crate::RangeCardinality { rows, selectivity })
1596 }
1597
1598 fn get_highest_memtable_seqno(&self) -> Option<SeqNo> {
1599 let version = self.version_history.read().latest_version();
1600
1601 let active = version.active_memtable.get_highest_seqno();
1602
1603 let sealed = version
1604 .sealed_memtables
1605 .iter()
1606 .map(|mt| mt.get_highest_seqno())
1607 .max()
1608 .flatten();
1609
1610 active.max(sealed)
1611 }
1612
1613 fn get_highest_persisted_seqno(&self) -> Option<SeqNo> {
1614 self.current_version()
1615 .iter_tables()
1616 .map(Table::get_highest_seqno)
1617 .max()
1618 }
1619
1620 fn oldest_retained_seqno(&self) -> SeqNo {
1621 self.version_history.read().oldest_retained_seqno()
1622 }
1623
1624 fn retention_floor(&self) -> SeqNo {
1625 self.version_history
1626 .read()
1627 .latest_version_ref()
1628 .version
1629 .retention_floor()
1630 }
1631
1632 fn get<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<UserValue>> {
1633 let key = key.as_ref();
1634
1635 let super_version = self.snapshot_for_read(seqno)?;
1636
1637 Self::resolve_or_passthrough(
1638 &super_version,
1639 key,
1640 seqno,
1641 self.config.merge_operator.as_ref(),
1642 self.config.comparator.as_ref(),
1643 )
1644 }
1645
1646 fn get_pinned<K: AsRef<[u8]>>(
1647 &self,
1648 key: K,
1649 seqno: SeqNo,
1650 ) -> crate::Result<Option<crate::PinnableSlice>> {
1651 let key = key.as_ref();
1652
1653 let super_version = self.snapshot_for_read(seqno)?;
1654
1655 Self::resolve_or_passthrough_pinned(
1656 &super_version,
1657 key,
1658 seqno,
1659 self.config.merge_operator.as_ref(),
1660 self.config.comparator.as_ref(),
1661 )
1662 }
1663
1664 #[expect(
1665 clippy::indexing_slicing,
1666 reason = "indices are generated from 0..n range, always in bounds"
1667 )]
1668 fn multi_get<K: AsRef<[u8]>>(
1669 &self,
1670 keys: impl IntoIterator<Item = K>,
1671 seqno: SeqNo,
1672 ) -> crate::Result<Vec<Option<UserValue>>> {
1673 let super_version = self.snapshot_for_read(seqno)?;
1674 let comparator = self.config.comparator.as_ref();
1675 let merge_operator = self.config.merge_operator.as_ref();
1676
1677 // Collect keys up front; bloom hashes computed lazily in Phase 2
1678 let keys: Vec<_> = keys.into_iter().collect();
1679 let n = keys.len();
1680 if n == 0 {
1681 return Ok(Vec::new());
1682 }
1683
1684 // For small batches, use the simple per-key path
1685 if n <= 2 {
1686 return keys
1687 .iter()
1688 .map(|key| {
1689 Self::resolve_or_passthrough(
1690 &super_version,
1691 key.as_ref(),
1692 seqno,
1693 merge_operator,
1694 comparator,
1695 )
1696 })
1697 .collect();
1698 }
1699
1700 // Phase 1: Check active + sealed memtables (unsorted — memtable lookup
1701 // is O(log n) per key regardless of order, skip sort+hash overhead for
1702 // memtable-only batches).
1703 let mut internal_entries: Vec<Option<InternalValue>> = vec![None; n];
1704 let mut remaining: Vec<usize> = Vec::with_capacity(n);
1705
1706 for idx in 0..n {
1707 let key = keys[idx].as_ref();
1708
1709 // Active memtable
1710 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
1711 internal_entries[idx] = Some(entry);
1712 continue;
1713 }
1714
1715 // Sealed memtables (newest first)
1716 if let Some(entry) =
1717 Self::get_internal_entry_from_sealed_memtables(&super_version, key, seqno)
1718 {
1719 internal_entries[idx] = Some(entry);
1720 continue;
1721 }
1722
1723 remaining.push(idx);
1724 }
1725
1726 // Phase 2: Sort remaining keys + compute bloom hashes only if needed
1727 // (memtable-only batches skip this entirely).
1728 if !remaining.is_empty() {
1729 remaining.sort_by(|&a, &b| comparator.compare(keys[a].as_ref(), keys[b].as_ref()));
1730
1731 // De-duplicate equal query keys (the batched on-disk path requires
1732 // strictly-sorted-unique input) and resolve the misses. Shared with
1733 // the BlobTree path via these helpers so the two cannot drift.
1734 let (miss_keys, duplicates) =
1735 Self::dedup_sorted_miss_keys(&remaining, &keys, comparator);
1736
1737 Self::batch_get_from_tables(
1738 &super_version.version,
1739 &keys,
1740 miss_keys,
1741 seqno,
1742 comparator,
1743 &*self.config.fs,
1744 &mut internal_entries,
1745 )?;
1746
1747 Self::fan_out_duplicates(&duplicates, &mut internal_entries);
1748 }
1749
1750 // Phase 3: Resolve entries (tombstones, RT suppression, merge operands)
1751 let mut results = vec![None; n];
1752 for idx in 0..n {
1753 let entry = internal_entries[idx].take();
1754 results[idx] = Self::resolve_entry(
1755 &super_version,
1756 keys[idx].as_ref(),
1757 entry,
1758 seqno,
1759 merge_operator,
1760 comparator,
1761 )?;
1762 }
1763
1764 Ok(results)
1765 }
1766
1767 fn apply_batch(&self, batch: crate::WriteBatch, seqno: SeqNo) -> crate::Result<(u64, u64)> {
1768 if batch.is_empty() {
1769 return Ok((0, self.active_memtable().size()));
1770 }
1771 Ok(self.append_batch(batch.materialize(seqno)?))
1772 }
1773
1774 fn insert<K: Into<UserKey>, V: Into<UserValue>>(
1775 &self,
1776 key: K,
1777 value: V,
1778 seqno: SeqNo,
1779 ) -> (u64, u64) {
1780 let value = InternalValue::from_components(key, value, seqno, ValueType::Value);
1781 self.append_entry(value)
1782 }
1783
1784 fn merge<K: Into<UserKey>, V: Into<UserValue>>(
1785 &self,
1786 key: K,
1787 operand: V,
1788 seqno: SeqNo,
1789 ) -> (u64, u64) {
1790 let value = InternalValue::new_merge_operand(key, operand, seqno);
1791 self.append_entry(value)
1792 }
1793
1794 fn remove<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
1795 let value = InternalValue::new_tombstone(key, seqno);
1796 self.append_entry(value)
1797 }
1798
1799 fn remove_weak<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
1800 let value = InternalValue::new_weak_tombstone(key, seqno);
1801 self.append_entry(value)
1802 }
1803
1804 fn remove_range<K: Into<UserKey>>(&self, start: K, end: K, seqno: SeqNo) -> u64 {
1805 // The read guard is held through the insert, like `append_entry`: the
1806 // CDC scan's capture (write side of this lock) must exclude every
1807 // in-flight memtable write, or a backdated range deletion could land
1808 // after the capture yet below the returned watermark and be lost; the
1809 // guard also keeps a concurrent `rotate_memtable()` from sealing the
1810 // memtable mid-insert.
1811 let history = self.version_history.read();
1812
1813 #[cfg(test)]
1814 inner::TestHooks::fire(&self.test_hooks.range_write);
1815
1816 history
1817 .latest_version_ref()
1818 .active_memtable
1819 .insert_range_tombstone(start.into(), end.into(), seqno)
1820 }
1821}
1822
1823impl Tree {
1824 /// Maps a raw internal entry to its change-data-capture event, routing
1825 /// `Indirection` (KV-separated) values through `resolve_indirection`.
1826 ///
1827 /// A standard tree never stores `Indirection` and supplies a resolver that
1828 /// errors; the blob-tree scan path supplies one that reads the blob and
1829 /// returns an [`ScanSinceEvent::Insert`].
1830 fn map_event<F>(
1831 entry: InternalValue,
1832 version: &Version,
1833 resolve_indirection: &F,
1834 ) -> crate::Result<ScanSinceEvent>
1835 where
1836 F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
1837 {
1838 if entry.key.value_type == ValueType::Indirection {
1839 return resolve_indirection(version, entry);
1840 }
1841 let seqno = entry.key.seqno;
1842 let key = entry.key.user_key;
1843 Ok(match entry.key.value_type {
1844 ValueType::Value => ScanSinceEvent::Insert {
1845 key,
1846 value: entry.value,
1847 seqno,
1848 },
1849 ValueType::MergeOperand => ScanSinceEvent::MergeOperand {
1850 key,
1851 operand: entry.value,
1852 seqno,
1853 },
1854 // Weak (single-delete) tombstones keep their own event kind: a
1855 // weak tombstone annihilates exactly its matching put during
1856 // compaction and can then expose an older value, while a regular
1857 // tombstone keeps hiding it — collapsing both into one event
1858 // would make a replica replay a weak delete as a full delete and
1859 // diverge from the source.
1860 ValueType::Tombstone => ScanSinceEvent::PointTombstone { key, seqno },
1861 ValueType::WeakTombstone => ScanSinceEvent::WeakTombstone { key, seqno },
1862 ValueType::Indirection => unreachable!("Indirection handled above"),
1863 })
1864 }
1865
1866 /// Shared CDC aggregation behind [`Self::scan_since_seqno`] and the
1867 /// blob-tree scan path: gathers qualifying entries (`seqno >= target`) plus
1868 /// range tombstones from the active + sealed memtables and every SST (with
1869 /// block-skip), maps each entry to a [`ScanSinceEvent`] — routing
1870 /// `Indirection` values through `resolve_indirection` against the same
1871 /// version snapshot — and returns them in increasing seqno order.
1872 ///
1873 /// # Panics
1874 ///
1875 /// Panics if the internal version-history lock is poisoned.
1876 ///
1877 /// # Errors
1878 ///
1879 /// Returns `Err` if reading the index or a data block fails, or if
1880 /// `resolve_indirection` errors.
1881 pub(crate) fn scan_since_seqno_with<F>(
1882 &self,
1883 target_seqno: SeqNo,
1884 block_skip: bool,
1885 resolve_indirection: F,
1886 ) -> crate::Result<alloc::vec::IntoIter<ScanSinceEvent>>
1887 where
1888 F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
1889 {
1890 self.scan_since_seqno_scoped(target_seqno, block_skip, resolve_indirection, None)
1891 }
1892
1893 /// As [`Self::scan_since_seqno_with`], optionally scoped to a key range
1894 /// (in the tree comparator's order): point events are delivered only for
1895 /// keys INSIDE the bounds, range tombstones when their span OVERLAPS them
1896 /// (a tombstone reaching into the range affects replay within it), and
1897 /// SSTs whose key range cannot intersect the bounds are skipped without
1898 /// being read — which is what makes a post-repair reconciliation over
1899 /// [`RepairReport::lost_coverage`](crate::RepairReport) affordable.
1900 pub(crate) fn scan_since_seqno_scoped<F>(
1901 &self,
1902 target_seqno: SeqNo,
1903 block_skip: bool,
1904 resolve_indirection: F,
1905 key_range: Option<&(Bound<UserKey>, Bound<UserKey>)>,
1906 ) -> crate::Result<alloc::vec::IntoIter<ScanSinceEvent>>
1907 where
1908 F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
1909 {
1910 use core::cmp::Ordering;
1911
1912 let cmp = self.config.comparator.clone();
1913 let in_key_range = |key: &[u8]| -> bool {
1914 let Some((lo, hi)) = key_range else {
1915 return true;
1916 };
1917 (match lo {
1918 Bound::Included(b) => cmp.compare(key, b.as_ref()) != Ordering::Less,
1919 Bound::Excluded(b) => cmp.compare(key, b.as_ref()) == Ordering::Greater,
1920 Bound::Unbounded => true,
1921 }) && (match hi {
1922 Bound::Included(b) => cmp.compare(key, b.as_ref()) != Ordering::Greater,
1923 Bound::Excluded(b) => cmp.compare(key, b.as_ref()) == Ordering::Less,
1924 Bound::Unbounded => true,
1925 })
1926 };
1927 // A range tombstone covers `[start, end)`; it is delivered when that
1928 // span overlaps the scope, since a deletion reaching into the range
1929 // affects replay within it.
1930 let rt_in_range = |rt: &RangeTombstone| -> bool {
1931 let Some((lo, hi)) = key_range else {
1932 return true;
1933 };
1934 (match lo {
1935 // `rt.end` is EXCLUSIVE: the tombstone reaches keys strictly
1936 // below it, so it clears the lower bound only when its end is
1937 // ABOVE the bound key (for an excluded bound this over-includes
1938 // the touching case, which is harmless: replaying an extra
1939 // idempotent deletion event cannot corrupt a consumer).
1940 Bound::Included(b) | Bound::Excluded(b) => {
1941 cmp.compare(rt.end.as_ref(), b.as_ref()) == Ordering::Greater
1942 }
1943 Bound::Unbounded => true,
1944 }) && (match hi {
1945 Bound::Included(b) => {
1946 cmp.compare(rt.start.as_ref(), b.as_ref()) != Ordering::Greater
1947 }
1948 Bound::Excluded(b) => cmp.compare(rt.start.as_ref(), b.as_ref()) == Ordering::Less,
1949 Bound::Unbounded => true,
1950 })
1951 };
1952 // The active memtable is the one source a writer can still change, and
1953 // the seqno cap alone does not exclude that: a caller may commit with an
1954 // explicit seqno at or BELOW the cap (`apply_batch` takes the seqno from
1955 // the caller), and a live lock-free walk would then see that write or
1956 // miss it depending on where the node lands relative to the cursor —
1957 // even splitting one batch. A consumer that advanced past the returned
1958 // watermark would lose the change for good.
1959 //
1960 // So freeze it: writers hold the version-history READ guard for their
1961 // whole insert (that is what keeps `rotate_memtable` from sealing
1962 // mid-batch), so taking the WRITE guard excludes them. The cap and the
1963 // active memtable's raw entries are captured under it; everything else —
1964 // sealed memtables, tables — is immutable and needs no coordination.
1965 //
1966 // Mapping runs AFTER the guard drops: it resolves blob indirections,
1967 // which reads a blob file, and no I/O may happen with writers blocked.
1968 let (super_version, end_seqno, active_entries, active_range_tombstones) = {
1969 let guard = self.version_history.write();
1970 let super_version = guard.latest_version();
1971 #[cfg(test)]
1972 inner::TestHooks::fire(&self.test_hooks.scan_freeze);
1973 let end_seqno = {
1974 let active = super_version.active_memtable.get_highest_seqno();
1975 let sealed = super_version
1976 .sealed_memtables
1977 .iter()
1978 .map(|mt| mt.get_highest_seqno())
1979 .max()
1980 .flatten();
1981 let tables = super_version
1982 .version
1983 .iter_tables()
1984 .map(Table::get_highest_seqno)
1985 .max();
1986 active.max(sealed).max(tables)
1987 };
1988 let entries: Vec<InternalValue> = end_seqno.map_or_else(Vec::new, |cap| {
1989 super_version
1990 .active_memtable
1991 .iter()
1992 .filter(|e| e.key.seqno >= target_seqno && e.key.seqno <= cap)
1993 .collect()
1994 });
1995 let rts = super_version.active_memtable.range_tombstones_sorted();
1996 // Explicit: the guard must outlive the capture above, and writers
1997 // resume the moment it goes.
1998 drop(guard);
1999 (super_version, end_seqno, entries, rts)
2000 };
2001 let version = &super_version.version;
2002 // No entries anywhere ⇒ nothing qualifies, regardless of target.
2003 let Some(end_seqno) = end_seqno else {
2004 return Ok(Vec::new().into_iter());
2005 };
2006
2007 // Events are gathered PER SOURCE, not into one flat list: copies of a
2008 // change across two sources are the same change and collapse, but a
2009 // single source may legitimately hold a byte-identical event more than
2010 // once (a write batch may carry the same merge operand for a key
2011 // twice; both are stored under the batch's shared seqno and both are
2012 // applied on read). See `merge_source_events`.
2013 let mut sources: Vec<Vec<ScanSinceEvent>> = Vec::new();
2014
2015 let in_window = |seqno: SeqNo| seqno >= target_seqno && seqno <= end_seqno;
2016 let range_tombstone_event = |rt: &RangeTombstone| {
2017 in_window(rt.seqno).then(|| ScanSinceEvent::RangeTombstone {
2018 start_key: rt.start.clone(),
2019 end_key: rt.end.clone(),
2020 seqno: rt.seqno,
2021 })
2022 };
2023
2024 // The scope bounds as borrowed slices, for SST key-range pruning.
2025 fn as_ref_bound(b: &Bound<UserKey>) -> Bound<&[u8]> {
2026 match b {
2027 Bound::Included(k) => Bound::Included(k.as_ref()),
2028 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
2029 Bound::Unbounded => Bound::Unbounded,
2030 }
2031 }
2032 let ref_bounds = key_range.map(|(lo, hi)| (as_ref_bound(lo), as_ref_bound(hi)));
2033
2034 // Active memtable — mapped from the frozen capture above, not walked
2035 // again: a second walk would reintroduce exactly the race the freeze
2036 // closed.
2037 let mut source = Vec::new();
2038 for entry in active_entries {
2039 if !in_key_range(&entry.key.user_key) {
2040 continue;
2041 }
2042 source.push(Self::map_event(entry, version, &resolve_indirection)?);
2043 }
2044 for rt in active_range_tombstones {
2045 if rt_in_range(&rt) {
2046 source.extend(range_tombstone_event(&rt));
2047 }
2048 }
2049 sources.push(source);
2050
2051 // Sealed memtables, NEWEST first: the list is kept in seal order, and
2052 // every source below must be older than the one before it, because the
2053 // merge derives replay precedence from that position.
2054 for memtable in super_version.sealed_memtables.iter().rev() {
2055 let mut source = Vec::new();
2056 for entry in memtable.iter() {
2057 if in_window(entry.key.seqno) && in_key_range(&entry.key.user_key) {
2058 source.push(Self::map_event(entry, version, &resolve_indirection)?);
2059 }
2060 }
2061 for rt in memtable.range_tombstones_sorted() {
2062 if rt_in_range(&rt) {
2063 source.extend(range_tombstone_event(&rt));
2064 }
2065 }
2066 sources.push(source);
2067 }
2068
2069 // SSTs. A table whose key range cannot intersect the scope is skipped
2070 // without a single block read — the point of the scoped variant. The
2071 // key range is RANGE-TOMBSTONE-SAFE to prune on: every writer keeps
2072 // tombstone coverage inside it (a flush conservatively widens the
2073 // range over its tombstone spans — see `write_rts_to_writer` — and a
2074 // compaction clips its output's tombstones to the table's
2075 // responsibility range), so a tombstone overlapping the scope always
2076 // sits in a table this loop visits.
2077 for table in version.iter_tables() {
2078 if let Some(bounds) = &ref_bounds
2079 && !table
2080 .metadata
2081 .key_range
2082 .overlaps_with_bounds_cmp(bounds, cmp.as_ref())
2083 {
2084 continue;
2085 }
2086 // An RT-only table's synthetic weak-tombstone sentinel (the
2087 // writer's `finish`) is deliberately NOT filtered out here. It is
2088 // a real on-disk entry the READ path sees: at a seqno TIE between
2089 // the range deletion and an older source's write at the range's
2090 // start key, the sentinel is what makes the read converge to a
2091 // deletion — so the event stream must carry it too, or a consumer
2092 // replaying the stream keeps a value the tree itself does not
2093 // serve (this stream's one rule is mirroring the tree's reads,
2094 // see `merge_source_events`). It surfaces as the weak-tombstone
2095 // event it is on disk; away from that tie a replayed weak delete
2096 // at the range's start under the range deletion's own seqno is a
2097 // no-op.
2098 let mut source = Vec::new();
2099 for entry in table.scan_seqno_range(target_seqno, end_seqno, block_skip)? {
2100 if !in_key_range(&entry.key.user_key) {
2101 continue;
2102 }
2103 source.push(Self::map_event(entry, version, &resolve_indirection)?);
2104 }
2105 // Clamped to the view's tight-space restriction: the punched
2106 // prefix's deletions are re-emitted by the slice output that
2107 // superseded it, so the raw list would duplicate those events.
2108 for rt in table.visible_range_tombstones() {
2109 if rt_in_range(&rt) {
2110 source.extend(range_tombstone_event(&rt));
2111 }
2112 }
2113 sources.push(source);
2114 }
2115
2116 Ok(Self::merge_source_events(sources).into_iter())
2117 }
2118
2119 /// Merge per-source event lists into one replay-ordered stream, `sources`
2120 /// ordered NEWEST first.
2121 ///
2122 /// Replay order is increasing seqno, then — for events sharing one seqno —
2123 /// increasing source AGE reversed, so the newest source's event is applied
2124 /// LAST. That matters because two sources can hold different values for one
2125 /// key at one seqno ([`AbstractTree::apply_batch`] takes a caller-chosen
2126 /// seqno and does not require it to be unique), the tree serves the newer
2127 /// one, and a consumer keeps whatever it applies last. Sorting such ties by
2128 /// payload instead would decide precedence by byte order.
2129 ///
2130 /// What happens to byte-identical copies follows ONE rule: the stream must
2131 /// mirror what a read of the same tree does, because a consumer replaying
2132 /// it has to reach the state the tree itself serves.
2133 ///
2134 /// - **Merge operands are all kept.** The read path collects every
2135 /// physically stored operand for a key and applies them in order — it
2136 /// never deduplicates by seqno — so two operands are two applications
2137 /// whether they sit in one source or in two. Seqnos do not disambiguate
2138 /// here: [`AbstractTree::apply_batch`] takes a caller-chosen seqno and
2139 /// does not require it to be unique, and one batch may carry the same
2140 /// operand for a key twice.
2141 /// - **Idempotent events collapse across sources.** A write, a deletion or
2142 /// a range deletion replayed twice reaches the same state, and the read
2143 /// path shadows the copies by seqno rather than compounding them. One
2144 /// committed change can physically live in two published tables — a
2145 /// manifest-loss repair publishes every surviving SST as its own L0 run,
2146 /// including both the inputs and the outputs of a compaction that crashed
2147 /// before deleting its inputs — and delivering it twice would be noise.
2148 /// Repeats WITHIN one source are still kept: they are separate entries
2149 /// the source genuinely holds.
2150 fn merge_source_events(sources: Vec<Vec<ScanSinceEvent>>) -> Vec<ScanSinceEvent> {
2151 // Per source, collapse equal neighbours into runs, each tagged with
2152 // its source's recency (0 = newest) and the position of EVERY copy it
2153 // carries.
2154 //
2155 // The POSITIONS are what keep an order-sensitive merge operator
2156 // correct: a source applies the operands of one batch in the order
2157 // they were added, all at one seqno, and grouping them by payload
2158 // would replay them in a different order — an append or a list push
2159 // then converges somewhere the tree never was. Grouping still SORTS
2160 // (equal events have to meet), so each run remembers where each of its
2161 // members sat: one shared position would fold a repeated operand's
2162 // copies onto its twin's slot and replay `B, A, B` as `A, B, B`.
2163 struct Run {
2164 event: ScanSinceEvent,
2165 recency: usize,
2166 /// Original scan position of every copy, ascending.
2167 positions: Vec<usize>,
2168 }
2169 let mut runs: Vec<Run> = Vec::new();
2170 for (recency, source) in sources.into_iter().enumerate() {
2171 let mut indexed: Vec<(usize, ScanSinceEvent)> =
2172 source.into_iter().enumerate().collect();
2173 indexed.sort_by(|(_, a), (_, b)| ScanSinceEvent::grouping_order(a, b));
2174 let mut iter = indexed.into_iter();
2175 let Some((position, mut current)) = iter.next() else {
2176 continue;
2177 };
2178 let mut positions = alloc::vec![position];
2179 for (index, event) in iter {
2180 if event == current {
2181 positions.push(index);
2182 } else {
2183 positions.sort_unstable();
2184 runs.push(Run {
2185 event: core::mem::replace(&mut current, event),
2186 recency,
2187 positions: core::mem::replace(&mut positions, alloc::vec![index]),
2188 });
2189 }
2190 }
2191 positions.sort_unstable();
2192 runs.push(Run {
2193 event: current,
2194 recency,
2195 positions,
2196 });
2197 }
2198
2199 // Merge the per-source runs. Operands are never collapsed — every copy
2200 // is an application the read path makes, so each keeps ITS source's
2201 // recency and ITS scan position, and the replay order below (oldest
2202 // source first, then position) applies them exactly as the tree does.
2203 // Idempotent events collapse across sources to the count a single
2204 // source holds; the collapsed event keeps the recency of the NEWEST
2205 // source holding it (and that source's positions), which is the slot
2206 // the tree's own precedence gives it.
2207 //
2208 // WEAK tombstones belong to the idempotent class even though a weak
2209 // delete annihilates one put at compaction: its documented contract
2210 // pairs it with a key written at most once, byte-identical copies
2211 // meeting in one compaction stream drain to a single survivor, and a
2212 // consumer cannot materialize multiplicity anyway — replaying the
2213 // same `remove_weak` twice at one seqno lands on ONE internal key in
2214 // its memtable, so a preserved duplicate would replay to the same
2215 // physical state the collapsed stream does.
2216 runs.sort_by(|a, b| ScanSinceEvent::grouping_order(&a.event, &b.event));
2217 let mut merged: Vec<(ScanSinceEvent, usize, usize)> = Vec::new();
2218 let mut iter = runs.into_iter().peekable();
2219 while let Some(run) = iter.next() {
2220 if matches!(run.event, ScanSinceEvent::MergeOperand { .. }) {
2221 // Equal operand runs from OTHER sources follow as their own
2222 // iterations and emit their own copies — no draining here.
2223 for &position in &run.positions {
2224 merged.push((run.event.clone(), run.recency, position));
2225 }
2226 continue;
2227 }
2228 let Run {
2229 event,
2230 mut recency,
2231 mut positions,
2232 } = run;
2233 let mut count = positions.len();
2234 while let Some(other) = iter.next_if(|next| next.event == event) {
2235 debug_assert_eq!(other.event, event, "next_if matched the same event");
2236 count = count.max(other.positions.len());
2237 if other.recency < recency {
2238 recency = other.recency;
2239 positions = other.positions;
2240 }
2241 }
2242 // The winning source may hold fewer copies than the count another
2243 // source did; the extras repeat its last position (they are
2244 // byte-identical, so their relative order carries no information).
2245 while positions.len() < count {
2246 let last = positions.last().copied().unwrap_or_default();
2247 positions.push(last);
2248 }
2249 for &position in positions.iter().take(count) {
2250 merged.push((event.clone(), recency, position));
2251 }
2252 }
2253
2254 // Finally, replay order: seqno, then range deletions, then oldest source
2255 // first, then the position that source gave the event — which is how an
2256 // order-sensitive merge operator converges to what the tree serves.
2257 //
2258 // The range-deletion step comes BEFORE source recency, not after: a tied
2259 // deletion does not suppress the writes it spans (suppression is
2260 // strictly `entry.seqno < tombstone.seqno`), so the tree keeps them, and
2261 // a replay that applied the deletion last would drop them. Ordering by
2262 // recency first would do exactly that whenever the deletion sits in the
2263 // newer source.
2264 merged.sort_by(|(a, a_recency, a_pos), (b, b_recency, b_pos)| {
2265 fn deletion_first(e: &ScanSinceEvent) -> u8 {
2266 u8::from(!matches!(e, ScanSinceEvent::RangeTombstone { .. }))
2267 }
2268 a.seqno()
2269 .cmp(&b.seqno())
2270 .then_with(|| deletion_first(a).cmp(&deletion_first(b)))
2271 .then_with(|| b_recency.cmp(a_recency))
2272 .then_with(|| {
2273 // Within one source and one seqno, a scan yields a key's
2274 // versions NEWEST first, and the read path reverses that run
2275 // to apply them chronologically. Mirror it: same key ⇒ the
2276 // later scan position replays FIRST. Distinct keys at one
2277 // seqno touch different state, so their relative order is
2278 // free — keep it deterministic by scan position.
2279 //
2280 // Identity is the engine's one relation (byte equality, see
2281 // `same_user_key`), which is what the read path this mirrors
2282 // uses to group a key's versions. Asking the comparator here
2283 // would answer the same question — its contract makes
2284 // `Equal` imply byte equality — while letting the two paths
2285 // disagree the moment a comparator broke that.
2286 if crate::comparator::same_user_key(a.key(), b.key()) {
2287 b_pos.cmp(a_pos)
2288 } else {
2289 a_pos.cmp(b_pos)
2290 }
2291 })
2292 });
2293 merged.into_iter().map(|(event, ..)| event).collect()
2294 }
2295
2296 /// Iterate change events with `seqno >= target_seqno`.
2297 ///
2298 /// Returns every change committed at or after `target_seqno` as a stream
2299 /// of [`ScanSinceEvent`]s in increasing seqno order. This is the canonical
2300 /// change-data-capture primitive: a downstream consumer (replica, Kafka
2301 /// connector, Debezium-style pipeline) replays the events in order to
2302 /// reconstruct the source's history. Superseded versions are not collapsed
2303 /// (a key written three times after the target yields three events).
2304 ///
2305 /// # Concurrency
2306 ///
2307 /// The result is a snapshot of the tree as of the call: the active memtable
2308 /// is captured with writers excluded, so a batch committed concurrently is
2309 /// either wholly in the result or wholly absent, never split across it. This
2310 /// holds even for a caller-chosen sequence number at or below the reported
2311 /// watermark, which the seqno bound alone would not exclude. Writers are
2312 /// blocked only while that capture runs — the sealed memtables and SSTs the
2313 /// scan then reads are immutable.
2314 ///
2315 /// # History retention
2316 ///
2317 /// The stream carries what the tree still PHYSICALLY HOLDS. A compaction
2318 /// run with a GC watermark (the `seqno_threshold` passed to
2319 /// [`compact`](crate::AbstractTree::compact) /
2320 /// [`major_compact`](crate::AbstractTree::major_compact)) drops shadowed
2321 /// versions and evicted tombstones below that watermark and may fold
2322 /// merge chains and zero bottommost seqnos — history a later scan cannot
2323 /// resurrect. The result is therefore complete only for a `target_seqno`
2324 /// at or above the highest GC watermark ever applied: a deployment
2325 /// replaying this stream (an external-WAL consumer, a CDC replica) must
2326 /// keep its compaction watermark at or below the lowest cursor it may
2327 /// still rewind to, exactly as `docs/external-wal.md` section 4's
2328 /// GC-coordination rules require. The watermark is caller-supplied and
2329 /// not persisted, so this method cannot detect a violation for you.
2330 ///
2331 /// # Block-skip
2332 ///
2333 /// On SSTs written with the `seqno_bounds` section (`seqno_in_index`), data
2334 /// blocks whose bounds cannot overlap the target window are skipped without
2335 /// being read; SSTs without the section are read and filtered per entry, so
2336 /// mixed trees are handled transparently.
2337 ///
2338 /// # KV-separation
2339 ///
2340 /// Standard trees never store blob-indirected values. On the inner tree of
2341 /// a KV-separated (blob) tree this returns an `Err` for indirected entries:
2342 /// blob resolution into [`ScanSinceEvent::Insert`] is provided by the
2343 /// blob-tree scan path, which owns the blob files.
2344 ///
2345 /// # Corruption resilience
2346 ///
2347 /// The per-block seqno-bounds used for skipping live in the optional
2348 /// `seqno_bounds` SST section, a Block covered by XXH3-128 (+ optional Page
2349 /// ECC) and verified when it is loaded at open, plus a decode that rejects
2350 /// non-ascending offsets and inverted bounds, so a corrupted bound is caught
2351 /// rather than trusted. Even in the impossible case of a fault bypassing
2352 /// those checks, a bad bound can only cause a *missed* record, never a wrong
2353 /// one. Callers who want defense against that hypothetical can use
2354 /// [`Self::scan_since_seqno_full_scan`], which reads every block (slower, no
2355 /// skip).
2356 ///
2357 /// # Panics
2358 ///
2359 /// Panics if the internal version-history lock is poisoned.
2360 ///
2361 /// # Errors
2362 ///
2363 /// Returns `Err` if reading the index or a data block fails, or if an entry
2364 /// is a KV-separated value (see above).
2365 pub fn scan_since_seqno(
2366 &self,
2367 target_seqno: SeqNo,
2368 ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
2369 // A standard tree never stores blob-indirected values; the resolver
2370 // errors so an indirected entry (only reachable via a blob tree's inner
2371 // index) surfaces as a clear error rather than a wrong event.
2372 self.scan_since_seqno_with(target_seqno, true, |_version, _entry| {
2373 Err(crate::Error::FeatureUnsupported(
2374 "scan_since_seqno on KV-separated values requires the blob-tree scan path",
2375 ))
2376 })
2377 }
2378
2379 /// Paranoid variant of [`Self::scan_since_seqno`] that disables the
2380 /// per-block seqno-bounds skip: every data block is read and filtered per
2381 /// entry, even on seqno-indexed SSTs.
2382 ///
2383 /// # When to use
2384 ///
2385 /// The fast [`Self::scan_since_seqno`] trusts each block's recorded
2386 /// `[seqno_min, seqno_max]` to skip blocks that cannot hold a qualifying
2387 /// record. Those bounds live in the `seqno_bounds` SST section, a Block
2388 /// covered by XXH3-128 (and optional Page ECC) and verified at open, so
2389 /// on-disk corruption is caught, not silently trusted. This method exists
2390 /// for callers who
2391 /// want defense even against a fault that somehow bypassed those checks: a
2392 /// corrupted `seqno_max` can only ever cause a *missed* record (never a
2393 /// wrong one), and a full scan cannot miss. It is slower (no skip), so
2394 /// prefer [`Self::scan_since_seqno`] unless you specifically need this
2395 /// guarantee.
2396 ///
2397 /// # Panics
2398 ///
2399 /// Panics if the internal version-history lock is poisoned.
2400 ///
2401 /// # Errors
2402 ///
2403 /// Same as [`Self::scan_since_seqno`].
2404 pub fn scan_since_seqno_full_scan(
2405 &self,
2406 target_seqno: SeqNo,
2407 ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
2408 self.scan_since_seqno_with(target_seqno, false, |_version, _entry| {
2409 Err(crate::Error::FeatureUnsupported(
2410 "scan_since_seqno on KV-separated values requires the blob-tree scan path",
2411 ))
2412 })
2413 }
2414
2415 /// Range-scoped variant of [`Self::scan_since_seqno`]: delivers only
2416 /// events whose key falls within `range` (in the tree comparator's
2417 /// order); range-deletion events are delivered when their span OVERLAPS
2418 /// it, since a tombstone reaching into the range affects replay within
2419 /// it. SSTs whose key range cannot intersect the bounds are skipped
2420 /// without a single block read.
2421 ///
2422 /// This is the presence-check primitive for reconciling an external
2423 /// write-ahead log after a repair: [`RepairReport::lost_coverage`] names
2424 /// the affected key ranges, and deciding which retained WAL records to
2425 /// re-apply (in particular, which merge operands SURVIVED and must not be
2426 /// folded twice) only needs the events inside those ranges. See
2427 /// `docs/external-wal.md` § Replay after repair.
2428 ///
2429 /// The history-retention caveat of [`Self::scan_since_seqno`] applies
2430 /// unchanged: the stream is complete only for a `target_seqno` at or
2431 /// above the highest compaction GC watermark ever applied.
2432 ///
2433 /// [`RepairReport::lost_coverage`]: crate::RepairReport::lost_coverage
2434 ///
2435 /// # Panics
2436 ///
2437 /// Panics if the internal version-history lock is poisoned.
2438 ///
2439 /// # Errors
2440 ///
2441 /// Same as [`Self::scan_since_seqno`].
2442 pub fn scan_since_seqno_in_range<K: AsRef<[u8]>, R: RangeBounds<K>>(
2443 &self,
2444 target_seqno: SeqNo,
2445 range: R,
2446 ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<K, R>> {
2447 let bounds = range_to_user_bounds(&range);
2448 self.scan_since_seqno_scoped(
2449 target_seqno,
2450 true,
2451 |_version, _entry| {
2452 Err(crate::Error::FeatureUnsupported(
2453 "scan_since_seqno on KV-separated values requires the blob-tree scan path",
2454 ))
2455 },
2456 Some(&bounds),
2457 )
2458 }
2459
2460 /// Update the live [`crate::runtime_config::RuntimeConfig`].
2461 ///
2462 /// Mutator runs on a clone of the current snapshot; the new snapshot
2463 /// is then atomically swapped in. Subsequent calls to
2464 /// [`Self::runtime_config`] observe the new snapshot.
2465 ///
2466 /// ## Current scope
2467 ///
2468 /// This API ships the snapshot + atomic-swap mechanism. No write
2469 /// path in the current tree consults `runtime_config` yet — that
2470 /// wiring lands with the V5-batch format features (manifest
2471 /// hardening, per-KV protection, scan-since-seqno) which extend
2472 /// [`RuntimeConfig`](crate::runtime_config::RuntimeConfig) with
2473 /// their own fields and read it at block write / manifest commit /
2474 /// compaction boundaries.
2475 ///
2476 /// ## Designed semantics (effective once wired by V5 features)
2477 ///
2478 /// - Subsequent write paths load the new snapshot lockless on their
2479 /// next operation.
2480 /// - Existing on-disk data remains in its original format and reads
2481 /// transparently — every block / manifest is self-describing via
2482 /// its own header.
2483 /// - Compaction acts as the live-migration mechanism: source blocks
2484 /// are rewritten per the current snapshot over subsequent cycles,
2485 /// so all data converges to the current settings without
2486 /// stop-the-world coordination.
2487 ///
2488 /// ## Concurrency
2489 ///
2490 /// **Reader atomicity:** concurrent readers observe either the old
2491 /// or the new snapshot, never a torn intermediate state.
2492 ///
2493 /// **Writer semantics: last-writer-wins.** Two `update` calls racing
2494 /// from the same starting snapshot will have the second `store`
2495 /// overwrite the first — the first writer's mutation is lost. There
2496 /// is no CAS / RCU merge. Callers that need lost-update avoidance
2497 /// (e.g. two threads concurrently toggling different fields) MUST
2498 /// serialize their `update_runtime_config` calls, typically via a
2499 /// `Mutex` around the call site.
2500 /// # Errors
2501 ///
2502 /// Returns [`crate::Error::PageEccUnsupported`] when the mutator
2503 /// leaves `page_ecc = true` on a binary built without the
2504 /// `page_ecc` cargo feature. The live snapshot stays at its
2505 /// pre-mutation value on error.
2506 pub fn update_runtime_config<F>(&self, mutator: F) -> crate::Result<()>
2507 where
2508 F: FnOnce(&mut crate::runtime_config::RuntimeConfig),
2509 {
2510 // Route through the validating handle path so an invalid
2511 // mutation (currently: `page_ecc = true` on a non-`page_ecc`
2512 // build) is rejected at update time, not silently swallowed
2513 // at the next manifest write.
2514 // Capture this update's `auto_heal` inside the mutation so the read-path
2515 // heal gate reflects exactly the config THIS call commits, rather than a
2516 // separate `load_full()` that could observe a different concurrent
2517 // update's value. Concurrent `update_runtime_config` calls must be
2518 // serialized by the caller (see the last-writer-wins note above); under
2519 // that contract the gate and the committed config stay in sync. On a
2520 // validation error `try_update` does not commit and `?` returns before
2521 // the gate is touched, so it keeps tracking the unchanged config.
2522 let mut auto_heal = false;
2523 self.0.runtime_config.try_update(|c| {
2524 mutator(c);
2525 auto_heal = c.auto_heal;
2526 })?;
2527 self.0.heal_hints.set_enabled(auto_heal);
2528 // Mirror the insert-time digest gate for the write hot path (see
2529 // `TreeInner::kv_digest_at_insert`). Relaxed: a toggle taking effect
2530 // on the next inserts is the documented contract (mixed inserts are
2531 // supported), so no ordering against other memory is needed.
2532 let gate = inner::kv_digest_at_insert_gate(&self.0.runtime_config.load());
2533 self.0
2534 .kv_digest_at_insert
2535 .store(gate, core::sync::atomic::Ordering::Relaxed);
2536 // Drop the cached admission footprint so the next check re-probes
2537 // disk-free: an operator who just raised the budget (or freed disk)
2538 // should see it promptly, not at the next flush.
2539 *self.0.admission_used_cache.lock() = None;
2540 Ok(())
2541 }
2542
2543 /// Snapshot of the current runtime config. Cheap atomic load —
2544 /// safe to call on hot paths.
2545 #[must_use]
2546 pub fn runtime_config(&self) -> Arc<crate::runtime_config::RuntimeConfig> {
2547 self.0.runtime_config.load_full()
2548 }
2549
2550 /// Shared handle to this tree's ECC heal-hint queue.
2551 ///
2552 /// A read that recovers a block from Page-ECC parity records the owning SST
2553 /// here (when the on-disk fault is confirmed persistent). Pass the handle to
2554 /// [`compaction::EccHeal`](crate::compaction::EccHeal) and run that strategy
2555 /// via [`Tree::compact`](crate::AbstractTree::compact) — leader-only in a
2556 /// clustered deployment — to rewrite the flagged SSTs clean. Check
2557 /// [`HealHints::is_empty`](crate::heal_hints::HealHints::is_empty) to skip
2558 /// the pass when nothing is queued.
2559 ///
2560 /// # Examples
2561 ///
2562 /// ```no_run
2563 /// use lsm_tree::{AbstractTree, AnyTree, Config, SequenceNumberCounter, compaction::EccHeal};
2564 /// use std::sync::Arc;
2565 /// # fn main() -> lsm_tree::Result<()> {
2566 /// let AnyTree::Standard(tree) = Config::new(
2567 /// "/tmp/db",
2568 /// SequenceNumberCounter::default(),
2569 /// SequenceNumberCounter::default(),
2570 /// )
2571 /// .open()?
2572 /// else {
2573 /// return Ok(());
2574 /// };
2575 ///
2576 /// // Opt into rewrite scheduling; reads that recover a block from parity now
2577 /// // flag its SST for healing.
2578 /// tree.update_runtime_config(|c| c.auto_heal = true)?;
2579 ///
2580 /// // Drain the queue, rewriting each flagged SST clean (leader-only in a
2581 /// // clustered deployment).
2582 /// let hints = tree.heal_hints();
2583 /// while !hints.is_empty() {
2584 /// tree.compact(Arc::new(EccHeal::new(tree.heal_hints(), u64::MAX)), 0)?;
2585 /// }
2586 /// # Ok(())
2587 /// # }
2588 /// ```
2589 #[must_use]
2590 pub fn heal_hints(&self) -> Arc<crate::heal_hints::HealHints> {
2591 Arc::clone(&self.0.heal_hints)
2592 }
2593
2594 /// Shared point-read logic for `get()` and `multi_get()`: finds the newest
2595 /// entry, applies merge resolution or RT suppression, and returns the value.
2596 fn resolve_or_passthrough(
2597 super_version: &SuperVersion,
2598 key: &[u8],
2599 seqno: SeqNo,
2600 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
2601 comparator: &dyn crate::comparator::UserComparator,
2602 ) -> crate::Result<Option<UserValue>> {
2603 let entry = Self::get_value(super_version, key, seqno, comparator)?;
2604
2605 match entry {
2606 Some((ValueType::MergeOperand, entry_seqno, value)) => {
2607 if let Some(merge_op) = merge_operator {
2608 // Build a bloom-filtered single-key iterator pipeline that
2609 // reuses MvccStream for merge/RT/Indirection resolution,
2610 // eliminating the previous hand-rolled merge collection.
2611 Self::resolve_merge_via_pipeline(
2612 super_version.clone(),
2613 key,
2614 seqno,
2615 Arc::clone(merge_op),
2616 )
2617 } else if Self::is_suppressed_by_range_tombstones(
2618 super_version,
2619 key,
2620 entry_seqno,
2621 seqno,
2622 comparator,
2623 ) {
2624 Ok(None)
2625 } else {
2626 Ok(Some(value))
2627 }
2628 }
2629 Some((_, _, value)) => Ok(Some(value)),
2630 None => Ok(None),
2631 }
2632 }
2633
2634 /// Shared post-lookup resolution for `get_pinned` and `multi_get`:
2635 /// tombstone filter, range-tombstone suppression, merge operand resolution.
2636 /// Returns `None` if entry is tombstoned or suppressed.
2637 fn resolve_pinned_entry(
2638 super_version: &SuperVersion,
2639 key: &[u8],
2640 entry: InternalValue,
2641 seqno: SeqNo,
2642 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
2643 comparator: &dyn crate::comparator::UserComparator,
2644 wrap: impl FnOnce(UserValue) -> crate::PinnableSlice,
2645 ) -> crate::Result<Option<crate::PinnableSlice>> {
2646 use crate::PinnableSlice;
2647
2648 let Some(entry) = ignore_tombstone_value(entry) else {
2649 return Ok(None);
2650 };
2651 if Self::is_suppressed_by_range_tombstones(
2652 super_version,
2653 key,
2654 entry.key.seqno,
2655 seqno,
2656 comparator,
2657 ) {
2658 return Ok(None);
2659 }
2660 if entry.key.value_type == ValueType::MergeOperand
2661 && let Some(merge_op) = merge_operator
2662 {
2663 // Merge resolution always produces Owned (pipeline result).
2664 return Self::resolve_merge_via_pipeline(
2665 super_version.clone(),
2666 key,
2667 seqno,
2668 Arc::clone(merge_op),
2669 )
2670 .map(|opt| opt.map(PinnableSlice::owned));
2671 }
2672 Ok(Some(wrap(entry.value)))
2673 }
2674
2675 /// Like [`Tree::resolve_or_passthrough`], but returns a [`PinnableSlice`](crate::PinnableSlice)
2676 /// that may keep the decompressed block buffer alive.
2677 fn resolve_or_passthrough_pinned(
2678 super_version: &SuperVersion,
2679 key: &[u8],
2680 seqno: SeqNo,
2681 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
2682 comparator: &dyn crate::comparator::UserComparator,
2683 ) -> crate::Result<Option<crate::PinnableSlice>> {
2684 use crate::PinnableSlice;
2685
2686 // Check memtables first — always Owned
2687 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
2688 return Self::resolve_pinned_entry(
2689 super_version,
2690 key,
2691 entry,
2692 seqno,
2693 merge_operator,
2694 comparator,
2695 PinnableSlice::owned,
2696 );
2697 }
2698
2699 // Sealed memtables — always Owned
2700 if let Some(entry) =
2701 Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
2702 {
2703 return Self::resolve_pinned_entry(
2704 super_version,
2705 key,
2706 entry,
2707 seqno,
2708 merge_operator,
2709 comparator,
2710 PinnableSlice::owned,
2711 );
2712 }
2713
2714 // Tables — Pinned (value shares decompressed block buffer)
2715 let key_hash = crate::hash::hash64(key);
2716
2717 if let Some((entry, block)) = Self::get_internal_entry_with_block_from_tables(
2718 &super_version.version,
2719 key,
2720 seqno,
2721 key_hash,
2722 comparator,
2723 )? {
2724 return Self::resolve_pinned_entry(
2725 super_version,
2726 key,
2727 entry,
2728 seqno,
2729 merge_operator,
2730 comparator,
2731 |value| PinnableSlice::pinned(block, value),
2732 );
2733 }
2734
2735 Ok(None)
2736 }
2737
2738 /// Like [`Tree::get_internal_entry_from_tables`], but returns the block
2739 /// along with the entry for pinned zero-copy access.
2740 fn get_internal_entry_with_block_from_tables(
2741 version: &Version,
2742 key: &[u8],
2743 seqno: SeqNo,
2744 key_hash: u64,
2745 comparator: &dyn crate::comparator::UserComparator,
2746 ) -> crate::Result<Option<(InternalValue, crate::table::Block)>> {
2747 Self::find_in_tables::<TableEntryWithBlock>(version, key, seqno, key_hash, comparator)
2748 }
2749
2750 /// Resolves merge operands for a point read via a bloom-filtered iterator pipeline.
2751 ///
2752 /// Builds a single-key range (`key..=key`) with bloom pre-filtering, wraps
2753 /// all sources in `Merger → MvccStream`, and takes the first result. This
2754 /// reuses the unified merge/RT/Indirection resolution logic from `MvccStream`
2755 /// instead of duplicating it in a hand-rolled collection loop.
2756 ///
2757 /// Bloom pre-filtering can reject many disk tables at the filter level,
2758 /// which typically improves point-read performance on deep LSM trees.
2759 pub(crate) fn resolve_merge_via_pipeline(
2760 version: SuperVersion,
2761 key: &[u8],
2762 seqno: SeqNo,
2763 merge_operator: Arc<dyn crate::merge_operator::MergeOperator>,
2764 ) -> crate::Result<Option<UserValue>> {
2765 use crate::range::{IterState, TreeIter};
2766
2767 let key_hash = crate::hash::hash64(key);
2768 // NOTE: Slice::from(&[u8]) copies the key (small, typically < 100 bytes).
2769 // This runs once per merge resolution, not per-table — cost is negligible
2770 // compared to the I/O saved by partition-aware bloom filtering.
2771 let bloom_key = crate::Slice::from(key);
2772 let comparator = version.active_memtable.comparator.clone();
2773
2774 let iter_state = IterState {
2775 version,
2776 ephemeral: None,
2777 merge_operator: Some(merge_operator),
2778 comparator,
2779 prefix_hash: None,
2780 key_hash: Some(key_hash),
2781 bloom_key: Some(bloom_key),
2782 #[cfg(feature = "metrics")]
2783 metrics: None,
2784 };
2785
2786 // Point-read fast path: skips eager RT collection, sort+dedup, table-skip,
2787 // and RangeTombstoneFilter wrapper. MvccStream handles merge-internal RT
2788 // suppression; a post-merge linear RT check catches the rest.
2789 let mut iter = TreeIter::create_range_point(iter_state, key, seqno);
2790
2791 match iter.next() {
2792 Some(Ok(entry)) => Ok(Some(entry.value)),
2793 Some(Err(e)) => Err(e),
2794 None => Ok(None),
2795 }
2796 }
2797
2798 #[doc(hidden)]
2799 pub fn create_internal_range<'a, K: AsRef<[u8]> + 'a, R: RangeBounds<K> + 'a>(
2800 version: SuperVersion,
2801 range: &'a R,
2802 seqno: SeqNo,
2803 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
2804 merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
2805 comparator: crate::comparator::SharedComparator,
2806 ) -> impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static {
2807 Self::create_internal_range_with_prefix_hash(
2808 version,
2809 range,
2810 seqno,
2811 ephemeral,
2812 merge_operator,
2813 comparator,
2814 None,
2815 )
2816 }
2817
2818 /// Like [`Tree::create_internal_range`], but with an optional prefix hash
2819 /// for prefix bloom filter skipping during prefix scans.
2820 #[doc(hidden)]
2821 pub(crate) fn create_internal_range_with_prefix_hash<
2822 'a,
2823 K: AsRef<[u8]> + 'a,
2824 R: RangeBounds<K> + 'a,
2825 >(
2826 version: SuperVersion,
2827 range: &'a R,
2828 seqno: SeqNo,
2829 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
2830 merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
2831 comparator: crate::comparator::SharedComparator,
2832 prefix_hash: Option<u64>,
2833 ) -> impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static {
2834 use crate::range::{IterState, TreeIter};
2835 use core::ops::Bound::{self, Excluded, Included, Unbounded};
2836
2837 let lo: Bound<UserKey> = match range.start_bound() {
2838 Included(x) => Included(x.as_ref().into()),
2839 Excluded(x) => Excluded(x.as_ref().into()),
2840 Unbounded => Unbounded,
2841 };
2842
2843 let hi: Bound<UserKey> = match range.end_bound() {
2844 Included(x) => Included(x.as_ref().into()),
2845 Excluded(x) => Excluded(x.as_ref().into()),
2846 Unbounded => Unbounded,
2847 };
2848
2849 let bounds: (Bound<UserKey>, Bound<UserKey>) = (lo, hi);
2850
2851 let iter_state = IterState {
2852 version,
2853 ephemeral,
2854 merge_operator,
2855 comparator,
2856 prefix_hash,
2857 key_hash: None,
2858 bloom_key: None,
2859 #[cfg(feature = "metrics")]
2860 metrics: None,
2861 };
2862
2863 TreeIter::create_range(iter_state, bounds, seqno)
2864 }
2865
2866 pub(crate) fn get_internal_entry_from_version(
2867 super_version: &SuperVersion,
2868 key: &[u8],
2869 seqno: SeqNo,
2870 comparator: &dyn crate::comparator::UserComparator,
2871 ) -> crate::Result<Option<InternalValue>> {
2872 // Search order: active → sealed → SST (newest first). A point
2873 // tombstone in a newer source is authoritative — no older source
2874 // can contain a newer value, so returning None is correct.
2875 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
2876 let Some(entry) = ignore_tombstone_value(entry) else {
2877 return Ok(None);
2878 };
2879
2880 // Check if any range tombstone suppresses this entry
2881 if Self::is_suppressed_by_range_tombstones(
2882 super_version,
2883 key,
2884 entry.key.seqno,
2885 seqno,
2886 comparator,
2887 ) {
2888 return Ok(None);
2889 }
2890 return Ok(Some(entry));
2891 }
2892
2893 // Now look in sealed memtables
2894 if let Some(entry) =
2895 Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
2896 {
2897 let Some(entry) = ignore_tombstone_value(entry) else {
2898 return Ok(None);
2899 };
2900
2901 if Self::is_suppressed_by_range_tombstones(
2902 super_version,
2903 key,
2904 entry.key.seqno,
2905 seqno,
2906 comparator,
2907 ) {
2908 return Ok(None);
2909 }
2910 return Ok(Some(entry));
2911 }
2912
2913 // Now look in tables... this may involve disk I/O
2914 let entry =
2915 Self::get_internal_entry_from_tables(&super_version.version, key, seqno, comparator)?;
2916
2917 if let Some(entry) = entry {
2918 if Self::is_suppressed_by_range_tombstones(
2919 super_version,
2920 key,
2921 entry.key.seqno,
2922 seqno,
2923 comparator,
2924 ) {
2925 return Ok(None);
2926 }
2927 return Ok(Some(entry));
2928 }
2929
2930 Ok(None)
2931 }
2932
2933 /// Value-only mirror of [`Self::get_internal_entry_from_version`].
2934 ///
2935 /// Returns `(value_type, seqno, value)` for the newest visible entry without
2936 /// reconstructing the entry key. Same search order (active -> sealed -> SST,
2937 /// newest first), tombstone filtering, and range-tombstone suppression; only
2938 /// the SST path differs, using the value-only [`TableValue`] lookup that
2939 /// skips the delta-key fusion of the full `InternalValue` path. Used by the
2940 /// value-returning `get` path, which never reads the matched key.
2941 pub(crate) fn get_value(
2942 super_version: &SuperVersion,
2943 key: &[u8],
2944 seqno: SeqNo,
2945 comparator: &dyn crate::comparator::UserComparator,
2946 ) -> crate::Result<Option<(ValueType, SeqNo, crate::Slice)>> {
2947 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
2948 let Some(entry) = ignore_tombstone_value(entry) else {
2949 return Ok(None);
2950 };
2951 if Self::is_suppressed_by_range_tombstones(
2952 super_version,
2953 key,
2954 entry.key.seqno,
2955 seqno,
2956 comparator,
2957 ) {
2958 return Ok(None);
2959 }
2960 return Ok(Some((entry.key.value_type, entry.key.seqno, entry.value)));
2961 }
2962
2963 if let Some(entry) =
2964 Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
2965 {
2966 let Some(entry) = ignore_tombstone_value(entry) else {
2967 return Ok(None);
2968 };
2969 if Self::is_suppressed_by_range_tombstones(
2970 super_version,
2971 key,
2972 entry.key.seqno,
2973 seqno,
2974 comparator,
2975 ) {
2976 return Ok(None);
2977 }
2978 return Ok(Some((entry.key.value_type, entry.key.seqno, entry.value)));
2979 }
2980
2981 let key_hash = crate::hash::hash64(key);
2982 let entry = Self::find_in_tables::<TableValue>(
2983 &super_version.version,
2984 key,
2985 seqno,
2986 key_hash,
2987 comparator,
2988 )?;
2989 if let Some((value_type, entry_seqno, value)) = entry {
2990 if Self::is_suppressed_by_range_tombstones(
2991 super_version,
2992 key,
2993 entry_seqno,
2994 seqno,
2995 comparator,
2996 ) {
2997 return Ok(None);
2998 }
2999 return Ok(Some((value_type, entry_seqno, value)));
3000 }
3001
3002 Ok(None)
3003 }
3004
3005 /// Checks if a key at `key_seqno` is suppressed by any range tombstone
3006 /// in the active memtable, sealed memtables, or SST tables, visible at `read_seqno`.
3007 pub(crate) fn is_suppressed_by_range_tombstones(
3008 super_version: &SuperVersion,
3009 key: &[u8],
3010 key_seqno: SeqNo,
3011 read_seqno: SeqNo,
3012 comparator: &dyn crate::comparator::UserComparator,
3013 ) -> bool {
3014 // Check active memtable range tombstones.
3015 // Future optimization: skip lock when memtable has no RTs (atomic count).
3016 if super_version
3017 .active_memtable
3018 .is_key_suppressed_by_range_tombstone(key, key_seqno, read_seqno)
3019 {
3020 return true;
3021 }
3022
3023 // Check sealed memtable range tombstones
3024 for mt in super_version.sealed_memtables.iter().rev() {
3025 if mt.is_key_suppressed_by_range_tombstone(key, key_seqno, read_seqno) {
3026 return true;
3027 }
3028 }
3029
3030 // Check SST table range tombstones.
3031 //
3032 // Per-table RT lists are sorted by start key (using comparator) on load,
3033 // so binary search narrows candidates to RTs with start <= key.
3034 // The key_range early reject uses the comparator so it works with
3035 // non-lexicographic orderings.
3036 for table in super_version
3037 .version
3038 .iter_levels()
3039 .flat_map(|lvl| lvl.iter())
3040 .flat_map(|run| run.iter())
3041 .filter(|t| !t.range_tombstones().is_empty())
3042 .filter(|t| {
3043 // Early reject: skip tables whose key range doesn't contain the key.
3044 let kr = &t.metadata.key_range;
3045 comparator.compare(kr.min(), key) != core::cmp::Ordering::Greater
3046 && comparator.compare(key, kr.max()) != core::cmp::Ordering::Greater
3047 })
3048 {
3049 let rts = table.range_tombstones();
3050
3051 // Binary search: find the first RT whose start is > key (in comparator order).
3052 // All RTs before that index have start <= key and are candidates.
3053 let candidate_end = rts.partition_point(|rt| {
3054 comparator.compare(&rt.start, key) != core::cmp::Ordering::Greater
3055 });
3056
3057 for rt in rts.iter().take(candidate_end) {
3058 // Check: start <= key < end (in comparator order) AND seqno visibility.
3059 if rt.visible_at(read_seqno)
3060 && comparator.compare(&rt.start, key) != core::cmp::Ordering::Greater
3061 && comparator.compare(key, &rt.end) == core::cmp::Ordering::Less
3062 && key_seqno < rt.seqno
3063 {
3064 return true;
3065 }
3066 }
3067 }
3068
3069 false
3070 }
3071
3072 /// Resolves a single internal entry into a user value, handling tombstones,
3073 /// range tombstone suppression, and merge operand resolution.
3074 /// Resolves an entry for `multi_get`: tombstone filter, RT suppression,
3075 /// merge operand resolution. Delegates to [`Self::resolve_pinned_entry`] with
3076 /// `Owned` wrapping, then extracts the value.
3077 fn resolve_entry(
3078 super_version: &SuperVersion,
3079 key: &[u8],
3080 entry: Option<InternalValue>,
3081 seqno: SeqNo,
3082 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
3083 comparator: &dyn crate::comparator::UserComparator,
3084 ) -> crate::Result<Option<UserValue>> {
3085 let Some(entry) = entry else {
3086 return Ok(None);
3087 };
3088 Self::resolve_pinned_entry(
3089 super_version,
3090 key,
3091 entry,
3092 seqno,
3093 merge_operator,
3094 comparator,
3095 crate::PinnableSlice::owned,
3096 )
3097 .map(|opt| opt.map(crate::PinnableSlice::into_value))
3098 }
3099
3100 /// De-duplicates equal query keys in a comparator-sorted `remaining` index
3101 /// list, returning the `(key_index, bloom_hash)` pairs for the batched
3102 /// on-disk resolver (which requires strictly-sorted-unique input) and a
3103 /// `(duplicate_index, representative_index)` map. Pair with
3104 /// [`Self::fan_out_duplicates`] after the batch resolves.
3105 ///
3106 /// Shared by [`Self::multi_get`] and the `BlobTree` multi-get so the two
3107 /// cannot silently diverge: forwarding duplicate miss keys into the
3108 /// strictly-sorted-unique resolver was exactly the regression class this
3109 /// guards against. `remaining` must already be sorted by `comparator`.
3110 #[expect(
3111 clippy::indexing_slicing,
3112 reason = "remaining/miss_keys carry batch-local key indices < keys.len()"
3113 )]
3114 pub(crate) fn dedup_sorted_miss_keys<K: AsRef<[u8]>>(
3115 remaining: &[usize],
3116 keys: &[K],
3117 comparator: &dyn crate::comparator::UserComparator,
3118 ) -> DedupedMissKeys {
3119 let mut miss_keys: Vec<(usize, u64)> = Vec::with_capacity(remaining.len());
3120 let mut duplicates: Vec<(usize, usize)> = Vec::new();
3121 for &idx in remaining {
3122 let key = keys[idx].as_ref();
3123 match miss_keys.last() {
3124 Some(&(rep_idx, _))
3125 if comparator.compare(keys[rep_idx].as_ref(), key)
3126 == core::cmp::Ordering::Equal =>
3127 {
3128 duplicates.push((idx, rep_idx));
3129 }
3130 _ => miss_keys.push((idx, crate::hash::hash64(key))),
3131 }
3132 }
3133 (miss_keys, duplicates)
3134 }
3135
3136 /// Fans each representative's resolved entry out to its duplicate positions,
3137 /// so every input slot carries the same answer the per-key path would have
3138 /// produced. Counterpart to [`Self::dedup_sorted_miss_keys`]; call after the
3139 /// batched resolver fills `internal_entries`.
3140 #[expect(
3141 clippy::indexing_slicing,
3142 reason = "duplicate/representative indices are batch-local key indices < entries.len()"
3143 )]
3144 pub(crate) fn fan_out_duplicates(
3145 duplicates: &[(usize, usize)],
3146 internal_entries: &mut [Option<InternalValue>],
3147 ) {
3148 for &(dup_idx, rep_idx) in duplicates {
3149 let resolved = internal_entries[rep_idx].clone();
3150 internal_entries[dup_idx] = resolved;
3151 }
3152 }
3153
3154 /// Queries tables for multiple keys using sorted access order.
3155 ///
3156 /// `miss_keys` contains `(key_index, bloom_hash)` pairs for keys not yet
3157 /// found, in comparator-sorted order. Keys are looked up individually via
3158 /// `Table::get`, but sorted order improves I/O locality. The precomputed
3159 /// bloom hash in each pair is reused across all table probes. Per-SST
3160 /// batched bloom checks and block walks are tracked in `#223`.
3161 #[expect(
3162 clippy::indexing_slicing,
3163 reason = "miss_keys entries carry batch-local indices; callers must pass a results slice aligned with keys"
3164 )]
3165 pub(crate) fn batch_get_from_tables<K: AsRef<[u8]>>(
3166 version: &Version,
3167 keys: &[K],
3168 miss_keys: Vec<(usize, u64)>,
3169 seqno: SeqNo,
3170 comparator: &dyn crate::comparator::UserComparator,
3171 fs: &dyn crate::fs::Fs,
3172 results: &mut [Option<InternalValue>],
3173 ) -> crate::Result<()> {
3174 debug_assert_eq!(results.len(), keys.len());
3175 debug_assert!(miss_keys.iter().all(|&(i, _)| i < keys.len()));
3176
3177 // Consume the caller's Vec directly — no allocation+copy.
3178 let mut still_remaining = miss_keys;
3179
3180 for (level_idx, level) in version.iter_levels().enumerate() {
3181 if still_remaining.is_empty() {
3182 break;
3183 }
3184
3185 // Warm the cold data blocks this level will read across ALL its SSTs
3186 // in one cross-file batched read, so the serial resolve below hits the
3187 // cache. On io_uring the reads coalesce into one submission and the
3188 // kernel fans them out across the underlying devices. When the cold
3189 // working set is too large to warm without thrashing the cache, this
3190 // signals oversize and warms nothing; the level is then resolved by
3191 // reading its blocks in budget-sized chunks into a scratch and
3192 // point-reading directly (no cache, no eviction).
3193 if Self::prewarm_level_cross_sst(fs, level, &still_remaining, keys, seqno, comparator)
3194 && Self::resolve_level_chunked(
3195 fs,
3196 level,
3197 &mut still_remaining,
3198 keys,
3199 seqno,
3200 comparator,
3201 results,
3202 )?
3203 {
3204 continue;
3205 }
3206
3207 if level_idx == 0 {
3208 // L0: must check ALL runs, keep highest seqno per key. Track keys
3209 // at the seqno ceiling (seqno + 1 == read_seqno): no other L0 run
3210 // can beat them, so skip them in subsequent runs. The bitmap is
3211 // dense over 0..keys.len().
3212 let mut at_ceiling = vec![false; keys.len()];
3213
3214 for run in level.iter() {
3215 // `at_ceiling` is read as this run's skip set (a key is visited
3216 // once per run, so the updates below only affect later runs)
3217 // and mutated from the returned outcomes: never both at once.
3218 let resolved = Self::resolve_run_batched(
3219 run,
3220 &still_remaining,
3221 keys,
3222 seqno,
3223 comparator,
3224 |idx| at_ceiling[idx],
3225 )?;
3226 for (idx, _hash, item) in resolved.covered {
3227 let Some(item) = item else { continue };
3228 match &results[idx] {
3229 Some(current) if current.key.seqno >= item.key.seqno => {}
3230 _ => {
3231 if item.key.seqno.checked_add(1) == Some(seqno) {
3232 at_ceiling[idx] = true;
3233 }
3234 results[idx] = Some(item);
3235 }
3236 }
3237 }
3238 // Uncovered keys stay in `still_remaining`; the retain below
3239 // prunes the ones any run resolved.
3240 }
3241
3242 // Remove found keys (both values and tombstones)
3243 still_remaining.retain(|&(idx, _)| results[idx].is_none());
3244 } else {
3245 // L1+ runs have non-overlapping key ranges within a level. A
3246 // covering run resolves a key definitively: a hit sets the result,
3247 // a covering miss drops it to lower levels (`covered_miss`), and an
3248 // uncovered key tries the next run in this level (`not_covered`).
3249 let mut covered_miss: Vec<(usize, u64)> = Vec::new();
3250
3251 for run in level.iter() {
3252 let resolved = Self::resolve_run_batched(
3253 run,
3254 &still_remaining,
3255 keys,
3256 seqno,
3257 comparator,
3258 |_| false,
3259 )?;
3260 for (idx, hash, item) in resolved.covered {
3261 if let Some(item) = item {
3262 results[idx] = Some(item);
3263 } else {
3264 // Covering run found, key absent: no other run in this
3265 // level can have it. Keep for lower levels.
3266 covered_miss.push((idx, hash));
3267 }
3268 }
3269 still_remaining = resolved.not_covered;
3270 }
3271
3272 // Merge back: keys without a covering run + keys with a covering
3273 // miss both proceed to lower levels. Re-sort to preserve
3274 // comparator order for the next level's sequential scan.
3275 let needs_sort = !covered_miss.is_empty();
3276 still_remaining.extend(covered_miss);
3277 if needs_sort {
3278 still_remaining.sort_by(|&(a, _), &(b, _)| {
3279 comparator.compare(keys[a].as_ref(), keys[b].as_ref())
3280 });
3281 }
3282 }
3283 }
3284
3285 Ok(())
3286 }
3287
3288 /// Resolves `remaining` (sorted ascending under `comparator`) against a
3289 /// single run with per-table batched gets instead of a per-key `table.get`:
3290 /// consecutive keys covered by the same table within the run share one
3291 /// [`Table::batch_get`], so co-located keys decode their data block once.
3292 /// Byte-identical to per-key resolution (the same point reads, the same
3293 /// values). `skip(idx)` omits a key (e.g. one already pinned at the L0 seqno
3294 /// ceiling, where no later run can beat it).
3295 ///
3296 /// Returns, per covered non-skipped key, `(idx, hash, resolved item)` in
3297 /// input order, plus the keys this run does not cover (also in input order)
3298 /// for the caller to pass to the next run or level.
3299 #[expect(
3300 clippy::indexing_slicing,
3301 reason = "i < remaining.len() is loop-checked; idx values are valid key indices (caller's keys/results are aligned, same as batch_get_from_tables)"
3302 )]
3303 fn resolve_run_batched<K: AsRef<[u8]>>(
3304 run: &crate::version::Run<crate::Table>,
3305 remaining: &[(usize, u64)],
3306 keys: &[K],
3307 seqno: SeqNo,
3308 comparator: &dyn crate::comparator::UserComparator,
3309 skip: impl Fn(usize) -> bool,
3310 ) -> crate::Result<RunResolve> {
3311 let mut covered: Vec<CoveredKey> = Vec::new();
3312 let mut not_covered: Vec<(usize, u64)> = Vec::new();
3313
3314 // One pair of buffers reused across the run's tables, cleared per
3315 // table. Each is consumed before the next table fills it, so a fresh
3316 // Vec per table would only re-allocate the capacity the previous one
3317 // just released — once per table on a batched read path.
3318 let mut batch: Vec<(&[u8], u64)> = Vec::new();
3319 let mut batch_keys: Vec<(usize, u64)> = Vec::new();
3320
3321 let mut i = 0;
3322 while i < remaining.len() {
3323 let (idx, hash) = remaining[i];
3324 if skip(idx) {
3325 i += 1;
3326 continue;
3327 }
3328 let key = keys[idx].as_ref();
3329 let Some(table) = run.get_for_key_cmp(key, comparator) else {
3330 not_covered.push((idx, hash));
3331 i += 1;
3332 continue;
3333 };
3334
3335 // Gather the contiguous, non-skipped keys covered by THIS table. The
3336 // input is sorted and a run's tables partition the key space, so the
3337 // keys for one table form a contiguous slice; one `batch_get` drains
3338 // them with a single block decode for co-located keys.
3339 let table_id = table.id();
3340 batch.clear();
3341 batch_keys.clear();
3342 while i < remaining.len() {
3343 let (jdx, jhash) = remaining[i];
3344 if skip(jdx) {
3345 i += 1;
3346 continue;
3347 }
3348 let jkey = keys[jdx].as_ref();
3349 match run.get_for_key_cmp(jkey, comparator) {
3350 Some(t) if t.id() == table_id => {
3351 batch.push((jkey, jhash));
3352 batch_keys.push((jdx, jhash));
3353 i += 1;
3354 }
3355 _ => break,
3356 }
3357 }
3358
3359 // `drain`, not `into_iter`: the buffer is reused by the next table,
3360 // so its capacity has to survive the walk. The lint assumes the
3361 // vector is dead afterwards, which is exactly what this is not.
3362 #[expect(
3363 clippy::iter_with_drain,
3364 reason = "buffer is reused across tables; into_iter would consume it"
3365 )]
3366 for ((kidx, khash), item) in batch_keys.drain(..).zip(table.batch_get(&batch, seqno)?) {
3367 covered.push((kidx, khash, item));
3368 }
3369 }
3370
3371 Ok(RunResolve {
3372 covered,
3373 not_covered,
3374 })
3375 }
3376
3377 /// Warms an entire level's COLD data blocks across ALL its SSTs in one
3378 /// cross-file batched read ([`crate::fs::Fs::read_blocks_batched`]), so the
3379 /// serial resolve walk that follows hits the cache. On `io_uring` the reads of
3380 /// many SSTs (and, on a multi-device filesystem, many physical devices)
3381 /// coalesce into one submission and overlap in flight.
3382 ///
3383 /// Purely best-effort: it never changes a query result (the resolve walk
3384 /// re-reads authoritatively), and it is size-bounded to at most half the
3385 /// shared cache so the warmed blocks survive until the walk reads them.
3386 ///
3387 /// Returns `true` when the level's cold working set EXCEEDS that half-cache
3388 /// bound: warming would thrash the cache, so nothing is warmed and the caller
3389 /// resolves the level with the chunked read-into-scratch path instead
3390 /// ([`Tree::resolve_level_chunked`]). Returns `false` when it warmed the
3391 /// blocks (or had nothing to warm), i.e. the serial resolve should run.
3392 #[expect(
3393 clippy::indexing_slicing,
3394 reason = "planned[ti] and all_buffers[k..end] indices are built from `planned` itself, so they are in range by construction"
3395 )]
3396 fn prewarm_level_cross_sst<K: AsRef<[u8]>>(
3397 fs: &dyn crate::fs::Fs,
3398 level: &crate::version::Level,
3399 remaining: &[(usize, u64)],
3400 keys: &[K],
3401 seqno: SeqNo,
3402 comparator: &dyn crate::comparator::UserComparator,
3403 ) -> bool {
3404 // Gather per-table prewarm plans across the level's runs (group remaining
3405 // keys by covering table, mirroring resolve_run_batched's walk).
3406 let mut planned: Vec<(
3407 &crate::Table,
3408 Arc<dyn crate::fs::FsFile>,
3409 Vec<crate::table::BlockHandle>,
3410 )> = Vec::new();
3411 // Reused across the level's tables, cleared per table: `plan_prewarm`
3412 // reads it and returns before the next table fills it.
3413 let mut batch: Vec<(&[u8], u64)> = Vec::new();
3414 for run in level.iter() {
3415 let mut i = 0;
3416 while i < remaining.len() {
3417 let (idx, _) = remaining[i];
3418 let key = keys[idx].as_ref();
3419 let Some(table) = run.get_for_key_cmp(key, comparator) else {
3420 i += 1;
3421 continue;
3422 };
3423 let table_id = table.id();
3424 batch.clear();
3425 while i < remaining.len() {
3426 let (jdx, jhash) = remaining[i];
3427 let jkey = keys[jdx].as_ref();
3428 match run.get_for_key_cmp(jkey, comparator) {
3429 Some(t) if t.id() == table_id => {
3430 batch.push((jkey, jhash));
3431 i += 1;
3432 }
3433 _ => break,
3434 }
3435 }
3436 if let Some((file, handles)) = table.plan_prewarm(&batch, seqno) {
3437 planned.push((table, file, handles));
3438 }
3439 }
3440 }
3441
3442 let total_cold: usize = planned.iter().map(|(_, _, h)| h.len()).sum();
3443 if total_cold < 2 {
3444 return false;
3445 }
3446 // Eviction-avoiding bound: warm at most half the (shared) cache.
3447 let Some((first_table, _, _)) = planned.first() else {
3448 return false;
3449 };
3450 let cap = first_table.cache_capacity();
3451 let total_bytes: u64 = planned
3452 .iter()
3453 .flat_map(|(_, _, h)| h.iter().map(|x| u64::from(x.size())))
3454 .sum();
3455 if cap == 0 {
3456 return false;
3457 }
3458 if total_bytes > cap / 2 {
3459 // Cold working set too large to warm without thrash: signal the caller
3460 // to resolve this level via the chunked read-into-scratch path.
3461 return true;
3462 }
3463
3464 // One buffer per cold block, in (table, block) order.
3465 //
3466 // One buffer per cold block, in (table, block) order.
3467 let mut all_buffers: Vec<Vec<u8>> = planned
3468 .iter()
3469 .flat_map(|(_, _, handles)| handles.iter().map(|h| vec![0u8; h.size() as usize]))
3470 .collect();
3471
3472 {
3473 // Paired directly with the plan rather than through a parallel
3474 // (table index, offset) vector: `all_buffers` is already in
3475 // (table, block) order, so zipping the two walks needs no third
3476 // collection to remember which file each buffer belongs to.
3477 let mut bufs = all_buffers.iter_mut();
3478 let mut reqs: Vec<crate::fs::BlockRead<'_>> = planned
3479 .iter()
3480 .flat_map(|(_, file, handles)| {
3481 handles.iter().map(move |h| (file.as_ref(), *h.offset()))
3482 })
3483 .zip(&mut bufs)
3484 .map(|((file, offset), buf)| crate::fs::BlockRead {
3485 file,
3486 offset,
3487 buf: crate::fs::BlockBuf::new(&mut buf[..]),
3488 })
3489 .collect();
3490 // Best-effort: a batched-read failure just leaves the blocks for the
3491 // resolve walk to read normally.
3492 if fs.read_blocks_batched(&mut reqs).is_err() {
3493 return false;
3494 }
3495 // Independently of what the call returned: an implementation that
3496 // reported success without filling a request leaves it short, and
3497 // a block decoded from a buffer nobody wrote would be decoded from
3498 // whatever the allocation held.
3499 if !reqs.iter().all(|r| r.buf.is_full()) {
3500 return false;
3501 }
3502 }
3503
3504 // Views, not owned copies: the decode reads these bytes and builds its
3505 // own block out of them, so staging them into an owning type would copy
3506 // every block a second time.
3507 let all_buffers: Vec<&[u8]> = all_buffers.iter().map(Vec::as_slice).collect();
3508
3509 // Decode each table's blocks (its contiguous slice of all_buffers).
3510 let mut k = 0;
3511 for (table, _, handles) in &planned {
3512 let end = k + handles.len();
3513 table.decode_prewarmed(handles, &all_buffers[k..end]);
3514 k = end;
3515 }
3516 false
3517 }
3518
3519 /// Plans every data block this level's SSTs will read for `remaining`,
3520 /// grouping keys by covering table per run (mirrors `resolve_run_batched`'s
3521 /// walk). Each task carries the ORIGINAL key indices (into `keys`).
3522 ///
3523 /// # Errors
3524 ///
3525 /// Propagates a table-side planning failure ([`Table::plan_block_tasks`]) so
3526 /// the resolver surfaces it instead of letting a stale lower level answer.
3527 #[expect(
3528 clippy::indexing_slicing,
3529 reason = "i < remaining.len() loop-checked; idx/jdx are valid key indices; batch_idx[pos] is in range (pos came from this table's own plan)"
3530 )]
3531 fn plan_level_block_tasks<'a, K: AsRef<[u8]>>(
3532 level: &'a crate::version::Level,
3533 remaining: &[(usize, u64)],
3534 keys: &[K],
3535 seqno: SeqNo,
3536 comparator: &dyn crate::comparator::UserComparator,
3537 ) -> crate::Result<Vec<BlockTask<'a>>> {
3538 let mut tasks: Vec<BlockTask<'a>> = Vec::new();
3539 // Reused across the level's tables, cleared per table: both are read by
3540 // the plan below and are done with before the next table fills them.
3541 let mut batch: Vec<(&[u8], u64)> = Vec::new();
3542 let mut batch_idx: Vec<usize> = Vec::new();
3543 for run in level.iter() {
3544 let mut i = 0;
3545 while i < remaining.len() {
3546 let (idx, _) = remaining[i];
3547 let key = keys[idx].as_ref();
3548 let Some(table) = run.get_for_key_cmp(key, comparator) else {
3549 i += 1;
3550 continue;
3551 };
3552 let table_id = table.id();
3553 batch.clear();
3554 batch_idx.clear();
3555 while i < remaining.len() {
3556 let (jdx, jhash) = remaining[i];
3557 let jkey = keys[jdx].as_ref();
3558 match run.get_for_key_cmp(jkey, comparator) {
3559 Some(t) if t.id() == table_id => {
3560 batch.push((jkey, jhash));
3561 batch_idx.push(jdx);
3562 i += 1;
3563 }
3564 _ => break,
3565 }
3566 }
3567 if let Some((file, table_seqno, special, blocks)) =
3568 table.plan_block_tasks(&batch, seqno)?
3569 {
3570 for (handle, positions) in blocks {
3571 let task_keys: Vec<usize> =
3572 positions.iter().map(|&pos| batch_idx[pos]).collect();
3573 tasks.push(BlockTask {
3574 table,
3575 file: Arc::clone(&file),
3576 handle,
3577 table_seqno,
3578 special,
3579 keys: task_keys,
3580 });
3581 }
3582 }
3583 }
3584 }
3585 Ok(tasks)
3586 }
3587
3588 /// Resolves an ENTIRE level by reading its blocks in chunks into a scratch and
3589 /// point-reading directly (no cache, no eviction). Called after
3590 /// [`Tree::prewarm_level_cross_sst`] signals the cold working set is too large
3591 /// to warm. Returns `Ok(true)` when it resolved the level (results updated,
3592 /// found keys dropped from `still_remaining`); `Ok(false)` when the level has
3593 /// no blocks to read for this batch (every key bloom-skips) or holds a
3594 /// Page-ECC / columnar table, in which cases the caller falls through to the
3595 /// serial resolve.
3596 #[expect(
3597 clippy::indexing_slicing,
3598 reason = "start/end stay within tasks by construction"
3599 )]
3600 fn resolve_level_chunked<K: AsRef<[u8]>>(
3601 fs: &dyn crate::fs::Fs,
3602 level: &crate::version::Level,
3603 still_remaining: &mut Vec<(usize, u64)>,
3604 keys: &[K],
3605 seqno: SeqNo,
3606 comparator: &dyn crate::comparator::UserComparator,
3607 results: &mut [Option<InternalValue>],
3608 ) -> crate::Result<bool> {
3609 let tasks = Self::plan_level_block_tasks(level, still_remaining, keys, seqno, comparator)?;
3610 let Some(first) = tasks.first() else {
3611 return Ok(false);
3612 };
3613 // A Page-ECC / columnar table covers some of these keys (only possible
3614 // when the columnar/ECC policy differs between the SSTs in this level).
3615 // The scratch decode path is row-format only, so hand the whole level to
3616 // the serial resolve, which loads those blocks through their format-aware
3617 // path. The scratch fast path stays homogeneous and row-only.
3618 if tasks.iter().any(|t| t.special) {
3619 return Ok(false);
3620 }
3621 // Read blocks in chunks of at most half the shared cache, so a chunk's
3622 // scratch never dwarfs the cache it is meant to spare. `.max(1)` keeps the
3623 // chunk loop's `end > start` guard the sole progress condition when the
3624 // cache is disabled (capacity 0).
3625 let budget = (first.table.cache_capacity() / 2).max(1);
3626
3627 let mut start = 0;
3628 while start < tasks.len() {
3629 let mut bytes = 0u64;
3630 let mut end = start;
3631 while end < tasks.len() {
3632 let sz = u64::from(tasks[end].handle.size());
3633 if end > start && bytes + sz > budget {
3634 break;
3635 }
3636 bytes += sz;
3637 end += 1;
3638 }
3639 Self::resolve_block_task_chunk(fs, &tasks[start..end], keys, results)?;
3640 start = end;
3641 }
3642 still_remaining.retain(|&(idx, _)| results[idx].is_none());
3643 Ok(true)
3644 }
3645
3646 /// Reads one chunk of block-tasks in ONE cross-file `read_blocks_batched`,
3647 /// decodes each from its scratch buffer, and point-reads its keys, keeping the
3648 /// highest-seqno hit per key in `results`. Every task is row-format (the caller
3649 /// routes any level with a Page-ECC / columnar table to the serial resolve).
3650 #[expect(
3651 clippy::indexing_slicing,
3652 reason = "buffers is built from chunk so indices align; key indices are valid (caller's keys/results aligned)"
3653 )]
3654 fn resolve_block_task_chunk<K: AsRef<[u8]>>(
3655 fs: &dyn crate::fs::Fs,
3656 chunk: &[BlockTask<'_>],
3657 keys: &[K],
3658 results: &mut [Option<InternalValue>],
3659 ) -> crate::Result<()> {
3660 let mut buffers: Vec<Vec<u8>> = chunk
3661 .iter()
3662 .map(|t| vec![0u8; t.handle.size() as usize])
3663 .collect();
3664 {
3665 let mut reqs: Vec<crate::fs::BlockRead<'_>> = chunk
3666 .iter()
3667 .zip(buffers.iter_mut())
3668 .map(|(t, buf)| crate::fs::BlockRead {
3669 file: t.file.as_ref(),
3670 offset: *t.handle.offset(),
3671 buf: crate::fs::BlockBuf::new(&mut buf[..]),
3672 })
3673 .collect();
3674 fs.read_blocks_batched(&mut reqs)?;
3675 // An implementation that reported success without filling a request
3676 // leaves it short; refuse to decode a block out of bytes it never
3677 // wrote.
3678 if !reqs.iter().all(|r| r.buf.is_full()) {
3679 return Err(crate::Error::Io(crate::io::Error::new(
3680 crate::io::ErrorKind::UnexpectedEof,
3681 "read_blocks_batched reported success on an unfilled block",
3682 )));
3683 }
3684 }
3685
3686 for (task, buf) in chunk.iter().zip(buffers.iter()) {
3687 if let Some(block) = task.table.decode_data_block_from_bytes(buf)? {
3688 for &kidx in &task.keys {
3689 if let Some(item) = task.table.point_read_translated(
3690 &block,
3691 keys[kidx].as_ref(),
3692 task.table_seqno,
3693 )? {
3694 Self::keep_highest(results, kidx, item);
3695 }
3696 }
3697 }
3698 }
3699 Ok(())
3700 }
3701
3702 /// Keeps the higher-seqno of an existing result and a new candidate (the L0
3703 /// newest-version-wins merge; correct for L1+ too, where each key has one
3704 /// candidate).
3705 #[expect(
3706 clippy::indexing_slicing,
3707 reason = "idx is a valid key index aligned with results"
3708 )]
3709 fn keep_highest(results: &mut [Option<InternalValue>], idx: usize, item: InternalValue) {
3710 match &results[idx] {
3711 Some(current) if current.key.seqno >= item.key.seqno => {}
3712 _ => results[idx] = Some(item),
3713 }
3714 }
3715
3716 fn get_internal_entry_from_tables(
3717 version: &Version,
3718 key: &[u8],
3719 seqno: SeqNo,
3720 comparator: &dyn crate::comparator::UserComparator,
3721 ) -> crate::Result<Option<InternalValue>> {
3722 let key_hash = crate::hash::hash64(key);
3723 Self::find_in_tables::<TableEntry>(version, key, seqno, key_hash, comparator)
3724 }
3725
3726 /// Generic level-walk for point reads, monomorphized over the lookup result type.
3727 ///
3728 /// L0: check ALL runs, keep highest seqno (runs may not be newest-first).
3729 /// L1+: at most one run contains the key — return on first match.
3730 /// Once a level yields a match, lower levels cannot have newer data.
3731 fn find_in_tables<T: TablePointLookup>(
3732 version: &Version,
3733 key: &[u8],
3734 seqno: SeqNo,
3735 key_hash: u64,
3736 comparator: &dyn crate::comparator::UserComparator,
3737 ) -> crate::Result<Option<T>> {
3738 for (level_idx, level) in version.iter_levels().enumerate() {
3739 if level_idx == 0 {
3740 let mut best: Option<T> = None;
3741
3742 for run in level.iter() {
3743 if let Some(table) = run.get_for_key_cmp(key, comparator)
3744 && let Some(item) = T::lookup(table, key, seqno, key_hash)?
3745 {
3746 match &best {
3747 Some(current) if current.entry_seqno() >= item.entry_seqno() => {}
3748 _ => {
3749 // Short-circuit: point reads use exclusive upper bound,
3750 // so the highest visible seqno is read_seqno - 1.
3751 // If matched, no other L0 run can have a higher one.
3752 if item.entry_seqno().checked_add(1) == Some(seqno) {
3753 return Ok(item.filter_tombstone());
3754 }
3755 best = Some(item);
3756 }
3757 }
3758 }
3759 }
3760
3761 if let Some(entry) = best {
3762 return Ok(entry.filter_tombstone());
3763 }
3764 } else {
3765 // L1+ runs have non-overlapping key ranges. Once we find the
3766 // covering run (get_for_key_cmp returns Some), no other run in
3767 // this level can contain the key — break regardless of hit/miss.
3768 for run in level.iter() {
3769 if let Some(table) = run.get_for_key_cmp(key, comparator) {
3770 if let Some(item) = T::lookup(table, key, seqno, key_hash)? {
3771 return Ok(item.filter_tombstone());
3772 }
3773 break;
3774 }
3775 }
3776 }
3777 }
3778
3779 Ok(None)
3780 }
3781
3782 pub(crate) fn get_internal_entry_from_sealed_memtables(
3783 super_version: &SuperVersion,
3784 key: &[u8],
3785 seqno: SeqNo,
3786 ) -> Option<InternalValue> {
3787 for mt in super_version.sealed_memtables.iter().rev() {
3788 if let Some(entry) = mt.get(key, seqno) {
3789 return Some(entry);
3790 }
3791 }
3792
3793 None
3794 }
3795
3796 /// Resolves the super-version serving snapshot `seqno`; see
3797 /// [`SuperVersions::get_version_for_snapshot`](crate::version::SuperVersions::get_version_for_snapshot)
3798 /// for the retention error.
3799 pub(crate) fn get_version_for_snapshot(&self, seqno: SeqNo) -> crate::Result<SuperVersion> {
3800 self.version_history.read().get_version_for_snapshot(seqno)
3801 }
3802
3803 /// The snapshot for one point read, without a clone when it is the latest.
3804 ///
3805 /// Lock-free fast path: when reading at or beyond the latest installed
3806 /// version (always the case for `MAX_SEQNO`, and the common case), the
3807 /// mirrored latest [`SuperVersion`] is exactly what `get_version_for_snapshot`
3808 /// would return (it yields the latest iff `latest.seqno < seqno`), so
3809 /// load it without taking the history `RwLock` or cloning a deque entry.
3810 /// Recent inserts stay visible because they mutate the shared
3811 /// `active_memtable` behind a stable Arc; the back only changes on
3812 /// flush / compaction, which refresh this mirror under the write lock.
3813 ///
3814 /// Historical snapshot reads (seqno <= latest.seqno) consult the locked
3815 /// version history for the correct point-in-time [`SuperVersion`].
3816 ///
3817 /// Point reads only: a guard held across a long scan would delay the
3818 /// mirror's writers, so iterators keep their own clones. no-std has no
3819 /// mirror (`arc-swap` is std-only) and always clones out of the locked
3820 /// history, as before.
3821 ///
3822 /// # Errors
3823 ///
3824 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
3825 /// when the history no longer retains a version for `seqno`. The fast path
3826 /// cannot hit it: a snapshot above the latest version is always served.
3827 ///
3828 /// Kept to the mirror load plus one compare so it inlines into the point
3829 /// reads; the locked history walk lives in
3830 /// [`historical_snapshot_for_read`](Self::historical_snapshot_for_read),
3831 /// which is deliberately NOT inlined. Folding the two together made this
3832 /// function large enough to stay a call, and a call returning
3833 /// `Result<SnapshotRef>` (an `arc-swap` guard or a `SuperVersion`, plus
3834 /// the error payload) costs the caller a measurable move per read.
3835 #[inline]
3836 pub(crate) fn snapshot_for_read(
3837 &self,
3838 seqno: SeqNo,
3839 ) -> crate::Result<crate::version::SnapshotRef> {
3840 #[cfg(feature = "std")]
3841 {
3842 let latest = self.latest_super_version.load();
3843 if seqno > latest.seqno {
3844 return Ok(crate::version::SnapshotRef::Latest(latest));
3845 }
3846 }
3847 self.historical_snapshot_for_read(seqno)
3848 }
3849
3850 /// The locked-history half of [`snapshot_for_read`](Self::snapshot_for_read):
3851 /// a point-in-time read that the latest-version mirror cannot serve.
3852 ///
3853 /// `#[inline(never)]` keeps it out of every point read's instruction
3854 /// stream. It is NOT `#[cold]`: a historical read is a normal operation
3855 /// (`AS OF` queries, a lagging consumer), just not the common one.
3856 #[inline(never)]
3857 fn historical_snapshot_for_read(
3858 &self,
3859 seqno: SeqNo,
3860 ) -> crate::Result<crate::version::SnapshotRef> {
3861 self.version_history
3862 .read()
3863 .get_version_for_snapshot(seqno)
3864 .map(crate::version::SnapshotRef::Owned)
3865 }
3866
3867 /// Normalizes a user-provided range into owned `Bound<Slice>` values.
3868 ///
3869 /// Returns a tuple containing:
3870 /// - the `OwnedBounds` that mirror the original bounds semantics (including
3871 /// inclusive/exclusive markers and unbounded endpoints), and
3872 /// - a `bool` flag indicating whether the normalized range is logically
3873 /// empty (e.g., when the lower bound is greater than the upper bound).
3874 ///
3875 /// Callers can use the flag to detect empty ranges and skip further work
3876 /// while still having access to the normalized bounds for non-empty cases.
3877 fn range_bounds_to_owned_bounds<K: AsRef<[u8]>, R: RangeBounds<K>>(
3878 range: &R,
3879 ) -> (OwnedBounds, bool) {
3880 use Bound::{Excluded, Included, Unbounded};
3881
3882 let start = match range.start_bound() {
3883 Included(key) => Included(Slice::from(key.as_ref())),
3884 Excluded(key) => Excluded(Slice::from(key.as_ref())),
3885 Unbounded => Unbounded,
3886 };
3887
3888 let end = match range.end_bound() {
3889 Included(key) => Included(Slice::from(key.as_ref())),
3890 Excluded(key) => Excluded(Slice::from(key.as_ref())),
3891 Unbounded => Unbounded,
3892 };
3893
3894 let is_empty =
3895 if let (Included(lo) | Excluded(lo), Included(hi) | Excluded(hi)) = (&start, &end) {
3896 lo.as_ref() > hi.as_ref()
3897 } else {
3898 false
3899 };
3900
3901 (OwnedBounds { start, end }, is_empty)
3902 }
3903
3904 /// Opens an LSM-tree in the given directory.
3905 ///
3906 /// Will recover previous state if the folder was previously
3907 /// occupied by an LSM-tree, including the previous configuration.
3908 /// If not, a new tree will be initialized with the given config.
3909 ///
3910 /// After recovering a previous state, use `Tree::set_active_memtable`
3911 /// to fill the memtable with data from a write-ahead log for full durability.
3912 ///
3913 /// # Errors
3914 ///
3915 /// Returns error, if an IO error occurred.
3916 pub(crate) fn open(config: Config) -> crate::Result<Self> {
3917 log::debug!("Opening LSM-tree at {}", config.path.display());
3918
3919 // Resolve the per-tree compaction compression pool once, at open: if the
3920 // caller supplied no shared pool but asked for >1 thread, build the
3921 // default rayon-backed pool now so every compaction reuses it (building
3922 // a pool per compaction would spawn threads on each run). A caller-
3923 // supplied pool is left untouched. Shadowed under `parallel` only, so
3924 // non-parallel builds don't carry an unused `mut`.
3925 #[cfg(feature = "parallel")]
3926 let config = {
3927 let mut config = config;
3928 if config.compaction_pool.is_none() && config.compaction_threads > 1 {
3929 config.compaction_pool = Some(Arc::new(
3930 crate::table::writer::RayonSpawner::with_threads(config.compaction_threads)?,
3931 ));
3932 }
3933 config
3934 };
3935
3936 // Gate on the `page_ecc` cargo feature: caller asked for ECC
3937 // but the build does not link the Reed-Solomon codec. We have
3938 // no way to verify or recover RS parity without the codec, so
3939 // refuse to open rather than silently downgrade integrity.
3940 // Two surfaces to check:
3941 // - `Config::page_ecc(true)` → SST data-block ECC
3942 // - `Config::with_runtime_config(RuntimeConfig { page_ecc: true, .. })`
3943 // → manifest-Block ECC (consumed by manifest_blocks::writer)
3944 // Both silently no-op without the feature; refusing here is
3945 // the only place callers see a typed error.
3946 if (config.page_ecc || config.initial_runtime_config.page_ecc)
3947 && !cfg!(feature = "page_ecc")
3948 {
3949 return Err(crate::Error::PageEccUnsupported);
3950 }
3951
3952 // Acquire the cross-process directory lock BEFORE any manifest access
3953 // (the `CURRENT` probe + `has_existing_version_state` check below, and
3954 // the recover / create paths). Acquiring it here makes `open()`
3955 // exclusive end-to-end: a concurrent opener fails fast with
3956 // `Error::Locked` instead of racing through the probe and observing a
3957 // peer's half-created directory (which would surface as the InvalidData
3958 // "half-written checkpoint" path rather than `Locked`). The `LOCK` file
3959 // needs its directory to exist, so create the root directory first
3960 // (idempotent; `create_new` re-creates the `tables/` subtree). The lock
3961 // is threaded into the constructor so it lives for the tree's lifetime.
3962 #[cfg(feature = "std")]
3963 let directory_lock = {
3964 config.fs.create_dir_all(&config.path)?;
3965 crate::config::acquire_directory_lock(&*config.fs, &config.path, config.directory_lock)?
3966 };
3967
3968 // Check for old version
3969 if config.fs.exists(&config.path.join("version"))? {
3970 log::error!(
3971 "refusing to open: this directory has a `version` marker file, which only the \
3972 retired V1 layout wrote. V5 is the only on-disk format THIS engine decodes, and \
3973 it ships no conversion tooling. If the directory is a V1 database the data is \
3974 not lost: open it with the engine that wrote it, or convert it there. If the \
3975 file is unrelated to this store, move it aside and retry"
3976 );
3977 // The marker's contents are deliberately not read: no legacy
3978 // decode path exists to act on them, so parsing it could only
3979 // change the wording of this error. That makes the presence of
3980 // the file evidence, not proof, hence "has a marker" rather than
3981 // "is a V1 database", and the second escape hatch for a directory
3982 // that merely happens to carry the name.
3983 //
3984 // Scoped to this engine on purpose. The operator reads this line
3985 // while deciding what to do with the directory, and "no conversion
3986 // path" full stop would read as "unrecoverable" — which is wrong,
3987 // and the opposite of what the repair gate documents (an
3988 // unsupported version needs offline conversion or a matching
3989 // binary; see `is_repairable_open_error`).
3990 //
3991 // Literal discriminant: V1 is a retired format and FormatVersion
3992 // carries no legacy variants (V5-only contract).
3993 return Err(crate::Error::InvalidVersion(1));
3994 }
3995
3996 // Decide between recovery and fresh creation atomically by attempting
3997 // to read the CURRENT version file. This avoids a TOCTOU race that
3998 // would occur if we probed with exists() first.
3999 let tree = match crate::version::recovery::get_current_version(
4000 &config.path,
4001 &*config.fs,
4002 config.encryption.clone(),
4003 ) {
4004 Ok(_) => Self::recover(
4005 config,
4006 #[cfg(feature = "std")]
4007 directory_lock,
4008 ),
4009 Err(crate::Error::Io(e)) if e.kind() == crate::io::ErrorKind::NotFound => {
4010 // Missing CURRENT MUST coincide with a directory that
4011 // has no version artifacts; otherwise we are looking at
4012 // a half-written checkpoint (or other interrupted
4013 // sealing). Silently calling `create_new` in that case
4014 // would overwrite the partial state with an empty tree,
4015 // turning a recoverable failure into data loss.
4016 if has_existing_version_state(&config.path, &*config.fs)? {
4017 // Return Error::Io(InvalidData, ...) rather than
4018 // Error::Unrecoverable so callers that don't read
4019 // logs still get a programmatic surface with the
4020 // path and remediation hint embedded. `log::error!`
4021 // stays for human ops who DO watch logs and want
4022 // the full context at the moment of failure (the
4023 // structured error is what propagates up the call
4024 // chain; the log line records the diagnosis next
4025 // to the timestamp).
4026 let msg = format!(
4027 "Tree::open: refusing to recover {} — `current` pointer is missing \
4028 but the directory still holds version artifacts (tables/, blobs/, \
4029 or vN). This is the on-disk signature of a half-written checkpoint \
4030 or interrupted sealing. Remove the partial directory and retry the \
4031 checkpoint, or restore `current` from a backup before reopening.",
4032 config.path.display(),
4033 );
4034 log::error!("{msg}");
4035 return Err(crate::Error::from(crate::io::Error::new(
4036 crate::io::ErrorKind::InvalidData,
4037 msg,
4038 )));
4039 }
4040 Self::create_new(
4041 config,
4042 #[cfg(feature = "std")]
4043 directory_lock,
4044 )
4045 }
4046 Err(e) => Err(e),
4047 }?;
4048
4049 Ok(tree)
4050 }
4051
4052 /// Returns `true` if there are some tables that are being compacted.
4053 #[doc(hidden)]
4054 #[must_use]
4055 pub fn is_compacting(&self) -> bool {
4056 !self.compaction_state.lock().hidden_set().is_empty()
4057 }
4058
4059 /// Computed storage admission predicate backing
4060 /// [`AbstractTree::write_admission`].
4061 ///
4062 /// Cheap: reads in-memory size accounting only (no syscall). Returns
4063 /// `Ok(())` unless admission control is enabled AND a budget is set AND the
4064 /// live footprint plus reserved headroom exceeds it.
4065 /// Best-effort minimum free space across every filesystem this tree writes
4066 /// to: the primary data path AND each per-level route (`Config::level_routes`
4067 /// can place cold-level SSTs on separate volumes). The admission gate must
4068 /// reflect the tightest volume, since a full routed disk fails compaction /
4069 /// flush targeting it even while the primary still has room.
4070 ///
4071 /// A backend that cannot report free space (or an I/O hiccup) yields
4072 /// `u64::MAX` = "no disk pressure", so a probe failure never falsely drives
4073 /// the tree read-only.
4074 fn probe_disk_free(&self) -> u64 {
4075 self.0.config.min_available_space()
4076 }
4077
4078 /// Disk-aware capacity figures for [`AbstractTree::storage_stats`], given the
4079 /// live footprint `used`: `(capacity, available, compaction_possible)`.
4080 ///
4081 /// `capacity` is the tighter of the configured quota and the physical disk
4082 /// headroom (`free + used`) — the same effective limit
4083 /// [`Self::compute_write_admission`] gates against — reported regardless of
4084 /// whether the admission gate is enabled (introspection is always available).
4085 /// `None` capacity/available means unbounded (no quota AND the backend
4086 /// cannot report free space). `compaction_possible` is `true` when unbounded
4087 /// or when at least [`MIN_RESERVED_HEADROOM`] of working room remains.
4088 pub(crate) fn admission_capacity(&self, used: u64) -> (Option<u64>, Option<u64>, bool) {
4089 let quota = self
4090 .0
4091 .runtime_config
4092 .load()
4093 .storage_limit_bytes
4094 .unwrap_or(u64::MAX);
4095 let free = self.probe_disk_free();
4096 // `free == u64::MAX` is the "backend can't report free space" sentinel:
4097 // adding `used` would overflow, so treat capacity as quota-only (the
4098 // explicit branch avoids the overflow without masking it with saturation).
4099 // Otherwise `free + used` ≤ ~2× disk capacity and cannot overflow u64.
4100 let capacity = if free == u64::MAX {
4101 quota
4102 } else {
4103 quota.min(free + used)
4104 };
4105 if capacity == u64::MAX {
4106 return (None, None, true);
4107 }
4108 // `available = max(0, capacity - used)`: an operator quota set below the
4109 // live footprint makes `capacity < used`, and available space cannot be
4110 // negative. The clamp-to-zero IS the intended semantics here.
4111 let available = capacity.saturating_sub(used);
4112 (
4113 Some(capacity),
4114 Some(available),
4115 available >= MIN_RESERVED_HEADROOM,
4116 )
4117 }
4118
4119 /// The logical partition-quota headroom for the two-layer space model:
4120 /// `max(0, storage_limit_bytes - used)`, or `u64::MAX` when no quota is set.
4121 ///
4122 /// This is Layer 1 (volume-agnostic) of [`crate::compaction::worker::space_fits_two_layer`];
4123 /// the physical free-space probe is Layer 2. An operator quota set below the
4124 /// live footprint leaves zero headroom — the clamp-to-zero is the intended
4125 /// min-semantics, not masking.
4126 pub(crate) fn quota_headroom(&self, used: u64) -> u64 {
4127 self.0
4128 .runtime_config
4129 .load()
4130 .storage_limit_bytes
4131 .map_or(u64::MAX, |limit| limit.saturating_sub(used))
4132 }
4133
4134 /// Whether the opt-in storage admission gate is active (a near-full disk or
4135 /// configured quota can drive the tree read-only and gate compaction space).
4136 /// Capacity introspection figures are reported regardless; this only governs
4137 /// whether the gate actually enforces.
4138 pub(crate) fn storage_admission_enabled(&self) -> bool {
4139 self.0.runtime_config.load().storage_admission_check
4140 }
4141
4142 #[expect(
4143 clippy::significant_drop_tightening,
4144 reason = "the admission cache lock intentionally spans the recompute \
4145 (stat + disk-free probe) so concurrent admission checks \
4146 coalesce on a single probe rather than each issuing a syscall"
4147 )]
4148 fn compute_write_admission(&self) -> crate::Result<()> {
4149 let rc = self.0.runtime_config.load();
4150 if !rc.storage_admission_check {
4151 return Ok(());
4152 }
4153
4154 // Take ONE coherent snapshot of the latest super-version and derive
4155 // BOTH the on-disk footprint and the pending-memtable bytes from it.
4156 // Reading them from two separate `latest_version()` loads would be a
4157 // TOCTOU bug: a flush installing a new version between the two reads
4158 // could pair an old (larger) disk usage with new (smaller) pending
4159 // bytes — or vice versa — and open the gate incorrectly.
4160 let super_version = self.version_history.read().latest_version();
4161 let vid = super_version.version.id();
4162
4163 // True physical footprint, including blob files — the SAME basis
4164 // `storage_stats()` reports, so the gate and the reported usage agree.
4165 // NOT `disk_space()` (metadata Level::size, which omits blob files and
4166 // undercounts the physical file by the meta block / footer).
4167 //
4168 // Cached so gated writes don't re-stat every live file or re-probe disk
4169 // on every call. `used_bytes` only changes when a new version is
4170 // installed (flush / compaction), so it is recomputed on a version
4171 // change. `disk_free` can change under us (another process writing the
4172 // same filesystem), so it is ALSO re-probed once its sample is older
4173 // than `ADMISSION_DISK_FREE_TTL` — bounding staleness without a syscall
4174 // per write. `update_runtime_config` resets the entry for an immediate
4175 // re-probe. The values live behind one mutex as a coherent unit (see
4176 // `TreeInner::admission_used_cache`).
4177 //
4178 // The TTL fast-path is std-only: under `no_std` there is no monotonic
4179 // clock (`crate::time::Instant::elapsed` is a zero stub), so an
4180 // elapsed-time window cannot bound staleness — a same-version sample
4181 // would otherwise look fresh forever and a filling disk would never be
4182 // re-probed. Under `no_std` the fast-path is skipped, so `disk_free` is
4183 // re-probed on every gated write (the `used` footprint stays cached by
4184 // version either way), keeping admission safe without a monotonic clock.
4185 let now = crate::time::Instant::now();
4186 let (used, disk_free) = {
4187 let mut cache = self.0.admission_used_cache.lock();
4188 match *cache {
4189 // Fresh: same version AND disk sample within the TTL. std-only —
4190 // `cfg!(feature = "std")` is `false` under `no_std`, so the guard
4191 // short-circuits there and the next arm re-probes every call.
4192 Some((cvid, used, free, at))
4193 if cvid == vid
4194 && cfg!(feature = "std")
4195 && at.elapsed() < ADMISSION_DISK_FREE_TTL =>
4196 {
4197 (used, free)
4198 }
4199 // Same version, stale disk sample: keep `used`, re-probe disk.
4200 Some((cvid, used, _, _)) if cvid == vid => {
4201 let free = self.probe_disk_free();
4202 *cache = Some((vid, used, free, now));
4203 (used, free)
4204 }
4205 // New version (or unset): recompute footprint and re-probe disk.
4206 _ => {
4207 let used = crate::storage_stats::compute_used_bytes(&super_version.version)?;
4208 let free = self.probe_disk_free();
4209 *cache = Some((vid, used, free, now));
4210 (used, free)
4211 }
4212 }
4213 };
4214
4215 // Effective limit is the tighter of the configured quota and the
4216 // physical disk headroom (free + what we already occupy): the disk can
4217 // fill from other processes even below a generous quota, and a tree with
4218 // no quota at all must still stop before ENOSPC. `None` quota = unbounded
4219 // by configuration; disk-free then alone bounds it.
4220 //
4221 // `disk_free` is the MINIMUM free across every volume the tree writes to
4222 // (`probe_disk_free` mins the primary path and all `level_routes`). The
4223 // `+ used` here is NOT an accounting of one volume's usage against
4224 // another's free space — it cancels out of the disk branch of the gate:
4225 // passing requires `used + reserved <= disk_free + used`, i.e.
4226 // `reserved <= disk_free`. So a passing gate guarantees the TIGHTEST
4227 // volume alone has at least `reserved` free — a conservative per-volume
4228 // headroom, never the sum of an empty routed volume's slack plus an
4229 // unrelated full volume's occupancy. A route that drops below `reserved`
4230 // free drives the whole tree read-only, exactly so a later flush /
4231 // compaction targeting that route cannot hit ENOSPC.
4232 let quota = rc.storage_limit_bytes.unwrap_or(u64::MAX);
4233 // `disk_free == u64::MAX` is the "backend can't report" sentinel; adding
4234 // `used` would overflow, so treat the limit as quota-only (explicit
4235 // branch, no saturation masking). Otherwise `disk_free + used` ≤ ~2× disk
4236 // capacity and cannot overflow u64.
4237 let limit = if disk_free == u64::MAX {
4238 quota
4239 } else {
4240 quota.min(disk_free + used)
4241 };
4242 // Both sources unbounded → nothing to gate.
4243 if limit == u64::MAX {
4244 return Ok(());
4245 }
4246
4247 // Reserved headroom keeps the soft budget from becoming a hard wall:
4248 // enough to flush every pending memtable (plus a margin for the
4249 // index/filter/footer overhead a flush adds) so a queued flush always
4250 // fits at the limit, with a floor for compaction working space.
4251 // Internal flush / compaction are never gated, so this band is the
4252 // engine's room to reclaim.
4253 //
4254 // Count ALL pending memtable bytes in this snapshot — the active one AND
4255 // any sealed (rotated) memtables awaiting flush — not just the active
4256 // one: after a rotation the active memtable is empty but the sealed
4257 // memtable's queued flush will still consume disk, so it must be
4258 // reserved for. Memtable sizes are bounded by RAM, so the sum (and the
4259 // +1/8 overhead margin below) cannot overflow u64 → plain arithmetic.
4260 let pending_memtable_bytes: u64 = super_version.active_memtable.size()
4261 + super_version
4262 .sealed_memtables
4263 .iter()
4264 .map(|m| m.size())
4265 .sum::<u64>();
4266
4267 let reserved =
4268 (pending_memtable_bytes + pending_memtable_bytes / 8).max(MIN_RESERVED_HEADROOM);
4269 // `used` (disk) + `reserved` (RAM-bounded) cannot realistically overflow,
4270 // but keep the comparison fail-closed with checked arithmetic: any
4271 // overflow means "definitely over budget", so deny.
4272 match used.checked_add(reserved) {
4273 Some(total) if total <= limit => Ok(()),
4274 _ => Err(crate::Error::StorageFull { used, limit }),
4275 }
4276 }
4277
4278 fn inner_compact(
4279 &self,
4280 strategy: Arc<dyn CompactionStrategy>,
4281 mvcc_gc_watermark: SeqNo,
4282 ) -> crate::Result<crate::compaction::CompactionResult> {
4283 use crate::compaction::worker::{Options, do_compaction};
4284
4285 let mut opts = Options::from_tree(self, strategy);
4286 opts.mvcc_gc_watermark = mvcc_gc_watermark;
4287
4288 let result = do_compaction(&opts)?;
4289
4290 log::debug!("Compaction run over");
4291
4292 Ok(result)
4293 }
4294
4295 /// Iterator over the whole tree at snapshot `seqno`.
4296 ///
4297 /// # Errors
4298 ///
4299 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4300 /// when the history no longer retains a version for `seqno`; the error is
4301 /// raised here, before any I/O, rather than as an iterator item.
4302 #[doc(hidden)]
4303 pub fn create_iter(
4304 &self,
4305 seqno: SeqNo,
4306 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4307 ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
4308 self.create_range::<UserKey, _>(&.., seqno, ephemeral)
4309 }
4310
4311 /// Iterator over `range` at snapshot `seqno`.
4312 ///
4313 /// # Errors
4314 ///
4315 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4316 /// when the history no longer retains a version for `seqno`; the error is
4317 /// raised here, before any I/O, rather than as an iterator item.
4318 #[doc(hidden)]
4319 pub fn create_range<'a, K: AsRef<[u8]> + 'a, R: RangeBounds<K> + 'a>(
4320 &self,
4321 range: &'a R,
4322 seqno: SeqNo,
4323 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4324 ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
4325 let super_version = self
4326 .version_history
4327 .read()
4328 .get_version_for_snapshot(seqno)?;
4329
4330 Ok(Self::create_internal_range(
4331 super_version,
4332 range,
4333 seqno,
4334 ephemeral,
4335 self.config.merge_operator.clone(),
4336 self.config.comparator.clone(),
4337 )
4338 .map(|item| match item {
4339 Ok(kv) => Ok((kv.key.user_key, kv.value)),
4340 Err(e) => Err(e),
4341 }))
4342 }
4343
4344 /// Build a [`SeekableTreeIter`](crate::range::SeekableTreeIter) over
4345 /// `[lo, hi)`. Source collection (Phase 1) runs once; repositions reuse it.
4346 ///
4347 /// # Errors
4348 ///
4349 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4350 /// when the history no longer retains a version for `seqno`.
4351 #[doc(hidden)]
4352 pub fn create_seekable_range_bounds(
4353 &self,
4354 lo: Bound<UserKey>,
4355 hi: Bound<UserKey>,
4356 seqno: SeqNo,
4357 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4358 ) -> crate::Result<crate::range::SeekableTreeIter> {
4359 use crate::range::{IterState, SeekableTreeIter};
4360
4361 let super_version = self
4362 .version_history
4363 .read()
4364 .get_version_for_snapshot(seqno)?;
4365
4366 let iter_state = IterState {
4367 version: super_version,
4368 ephemeral,
4369 merge_operator: self.config.merge_operator.clone(),
4370 comparator: self.config.comparator.clone(),
4371 prefix_hash: None,
4372 key_hash: None,
4373 bloom_key: None,
4374 #[cfg(feature = "metrics")]
4375 metrics: Some(self.0.metrics.clone()),
4376 };
4377
4378 Ok(SeekableTreeIter::create(iter_state, lo, hi, seqno))
4379 }
4380
4381 /// Iterator over the keys starting with `prefix` at snapshot `seqno`.
4382 ///
4383 /// # Errors
4384 ///
4385 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4386 /// when the history no longer retains a version for `seqno`; the error is
4387 /// raised here, before any I/O, rather than as an iterator item.
4388 #[doc(hidden)]
4389 pub fn create_prefix<'a, K: AsRef<[u8]> + 'a>(
4390 &self,
4391 prefix: K,
4392 seqno: SeqNo,
4393 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4394 ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
4395 use crate::prefix::compute_prefix_hash;
4396 use crate::range::{IterState, TreeIter, prefix_to_range};
4397
4398 let prefix_bytes = prefix.as_ref();
4399
4400 let prefix_hash = compute_prefix_hash(self.config.prefix_extractor.as_ref(), prefix_bytes);
4401
4402 let range = prefix_to_range(prefix_bytes);
4403
4404 let super_version = self
4405 .version_history
4406 .read()
4407 .get_version_for_snapshot(seqno)?;
4408
4409 let iter_state = IterState {
4410 version: super_version,
4411 ephemeral,
4412 merge_operator: self.config.merge_operator.clone(),
4413 comparator: self.config.comparator.clone(),
4414 prefix_hash,
4415 key_hash: None,
4416 bloom_key: None,
4417 #[cfg(feature = "metrics")]
4418 metrics: Some(self.0.metrics.clone()),
4419 };
4420
4421 Ok(
4422 TreeIter::create_range(iter_state, range, seqno).map(|item| match item {
4423 Ok(kv) => Ok((kv.key.user_key, kv.value)),
4424 Err(e) => Err(e),
4425 }),
4426 )
4427 }
4428
4429 /// Adds an item to the active memtable.
4430 ///
4431 /// Returns the added item's size and new size of the memtable.
4432 #[doc(hidden)]
4433 #[must_use]
4434 pub fn append_entry(&self, value: InternalValue) -> (u64, u64) {
4435 // Per-KV residence digest (KvChecksumComputePoint::AtInsert): compute
4436 // the entry's 4-byte logical-content digest now, so a RAM bit-flip
4437 // while it sits in the memtable is caught at flush. The digest covers
4438 // the OWNED `value` and is independent of which active memtable
4439 // receives it, so computing it before taking the version-history guard
4440 // is correct (a concurrent rotation just routes the same value+digest
4441 // into the new active memtable) AND keeps the hash out of the read-lock
4442 // critical section. The gate is one relaxed byte mirrored from the
4443 // runtime config (`TreeInner::kv_digest_at_insert`); under the default
4444 // `AtBlockCompile` (or `Off`) it is `0` and no digest is computed.
4445 let gate = self
4446 .0
4447 .kv_digest_at_insert
4448 .load(core::sync::atomic::Ordering::Relaxed);
4449 let kv_digest = inner::kv_digest_algo_from_gate(gate).and_then(|algo| {
4450 crate::table::block::kv_checksum::kv_digest(&value, algo).map(|d| {
4451 #[expect(
4452 clippy::cast_possible_truncation,
4453 reason = "AtInsert is config-validated to a 4-byte algorithm; the digest fits u32"
4454 )]
4455 let lo = d as u32;
4456 (lo, algo)
4457 })
4458 });
4459
4460 // The `.read()` guard is a temporary that lives until the end of this
4461 // statement, so the insert runs under the version-history read lock:
4462 // `value` + its digest land in the current active memtable atomically,
4463 // and a concurrent `rotate_memtable()` cannot seal it mid-insert.
4464 self.version_history
4465 .read()
4466 .latest_version_ref()
4467 .active_memtable
4468 .insert_with_kv_digest(value, kv_digest)
4469 }
4470
4471 /// Adds multiple items to the active memtable in bulk.
4472 ///
4473 /// Acquires the version-history lock once and delegates to
4474 /// [`Memtable::insert_batch`] for batch size accounting.
4475 ///
4476 /// Returns the total bytes added and new size of the memtable.
4477 #[doc(hidden)]
4478 #[must_use]
4479 pub(crate) fn append_batch(&self, items: Vec<InternalValue>) -> (u64, u64) {
4480 // Per-KV residence digest under AtInsert (see `append_entry`): pass the
4481 // algorithm so the bulk path fixes each entry's digest at insert. The
4482 // default path passes `None` and is unchanged.
4483 let kv_algo = inner::kv_digest_algo_from_gate(
4484 self.0
4485 .kv_digest_at_insert
4486 .load(core::sync::atomic::Ordering::Relaxed),
4487 );
4488
4489 // Hold the read guard for the entire insert to prevent rotate_memtable()
4490 // from sealing this memtable mid-batch (which could cause data loss if
4491 // a concurrent flush persists only a prefix of the batch).
4492 self.version_history
4493 .read()
4494 .latest_version_ref()
4495 .active_memtable
4496 .insert_batch_with_kv_algo(items, kv_algo)
4497 }
4498
4499 /// Recovers previous state, by loading the level manifest, tables and blob files.
4500 ///
4501 /// # Errors
4502 ///
4503 /// Returns error, if an IO error occurred.
4504 #[expect(
4505 clippy::too_many_lines,
4506 reason = "Tree::recover threads the whole open sequence (CURRENT validation, \
4507 Manifest decode, encryption + runtime plumbing, version recovery, \
4508 TreeInner assembly) — splitting it would create helper functions whose \
4509 only caller is this one site"
4510 )]
4511 fn recover(
4512 mut config: Config,
4513 // The cross-process directory lock acquired by `Tree::open` before the
4514 // manifest probe; held for the tree's lifetime via
4515 // `TreeInner::_directory_lock`.
4516 #[cfg(feature = "std")] directory_lock: Option<Box<dyn crate::fs::FsFile>>,
4517 ) -> crate::Result<Self> {
4518 use crate::stop_signal::StopSignal;
4519 use inner::get_next_tree_id;
4520
4521 log::info!("Recovering LSM-tree at {}", config.path.display());
4522
4523 // Validate manifest metadata (format version, comparator name)
4524 // BEFORE recover_levels, so a rejected open is side-effect free
4525 // — recover_levels loads tables and cleans up orphans.
4526 // Tree type is checked after recovery (needs the Version object).
4527 // NOTE: the version file is read twice (here for metadata, then inside
4528 // recover_levels for table/blob data). This is intentional — metadata
4529 // validation must complete before any disk-mutating recovery work.
4530 // Version id of the on-disk snapshot CURRENT references. This is the
4531 // base the edit log replays on top of; the live version id can be higher
4532 // (it has no `v{id}` file of its own). Threaded into the version history
4533 // so the next persist appends to / rotates the right snapshot's log.
4534 let snapshot_id = crate::version::recovery::get_current_version(
4535 &config.path,
4536 &*config.fs,
4537 config.encryption.clone(),
4538 )?;
4539 {
4540 let version_id = snapshot_id;
4541 let manifest_path = config.path.join(format!("v{version_id}"));
4542 // Open the manifest with a default runtime snapshot:
4543 // ECC awareness is captured per-Block via the header
4544 // (`ECC_PARITY` flag) so the reader doesn't actually
4545 // need to know which ECC mode the writer used. The
4546 // captured runtime here is a placeholder; once we want
4547 // runtime-driven decisions on the read path (e.g.
4548 // checksum_algo dispatch per #298) we'll seed it from
4549 // Config + persisted format-version fields.
4550 let mut archive_reader = crate::manifest_blocks::reader::ManifestArchiveReader::open(
4551 &manifest_path,
4552 &*config.fs,
4553 alloc::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
4554 config.encryption.clone(),
4555 )?;
4556 let manifest = Manifest::decode_from(&mut archive_reader)?;
4557
4558 // V5 is the only variant `FormatVersion` can decode to (the
4559 // engine reads exactly one on-disk format, no legacy paths), so
4560 // anything else already failed above: on its framing if the
4561 // manifest is not shaped like the current one, on the version
4562 // field if it is. This match stays as the explicit gate the
4563 // format contract documents — and as the compile-time hook that
4564 // forces a review of the open path when a new variant is added.
4565 match manifest.version {
4566 FormatVersion::V5 => {}
4567 }
4568
4569 let supplied_name = config.comparator.name();
4570 if manifest.comparator_name != supplied_name {
4571 log::warn!(
4572 "Comparator mismatch: tree was created with {:?} but opened with {:?}",
4573 manifest.comparator_name,
4574 supplied_name,
4575 );
4576 return Err(crate::Error::ComparatorMismatch {
4577 stored: manifest.comparator_name,
4578 supplied: supplied_name,
4579 });
4580 }
4581
4582 // IMPORTANT: Restore persisted config
4583 config.level_count = manifest.level_count;
4584 }
4585
4586 let tree_id = get_next_tree_id();
4587
4588 #[cfg(feature = "metrics")]
4589 let metrics = Arc::new(Metrics::default());
4590
4591 let version = Self::recover_levels(
4592 &config.path,
4593 tree_id,
4594 &config,
4595 #[cfg(feature = "metrics")]
4596 &metrics,
4597 )?;
4598
4599 {
4600 let requested_tree_type = match config.kv_separation_opts {
4601 Some(_) => crate::TreeType::Blob,
4602 None => crate::TreeType::Standard,
4603 };
4604
4605 if version.tree_type() != requested_tree_type {
4606 log::error!(
4607 "Tried to open a {requested_tree_type:?}Tree, but the existing tree is of type {:?}Tree. This indicates a misconfiguration or corruption.",
4608 version.tree_type(),
4609 );
4610 // A dedicated error, NOT `Unrecoverable`: the auto-repair path
4611 // answers `Unrecoverable` with a manifest rebuild, and a
4612 // rebuild under the mismatched type commits the wrong tree
4613 // shape (a Standard rebuild of a blob tree strands its blob
4614 // files for the orphan sweep). A configuration error must
4615 // propagate to the caller instead.
4616 return Err(crate::Error::TreeTypeMismatch {
4617 requested: requested_tree_type,
4618 actual: version.tree_type(),
4619 });
4620 }
4621 }
4622
4623 let highest_table_id = version
4624 .iter_tables()
4625 .map(Table::id)
4626 .max()
4627 .unwrap_or_default();
4628
4629 let comparator = config.comparator.clone();
4630
4631 let deletion_pause = crate::deletion_pause::DeletionPause::new_shared();
4632 #[cfg(feature = "std")]
4633 let background_deleter = Arc::new(crate::BackgroundDeleter::new(None));
4634 let heal_hints =
4635 crate::heal_hints::HealHints::new_shared(config.initial_runtime_config.auto_heal);
4636
4637 // Clone the seed snapshot BEFORE moving config into the Arc
4638 // below — the runtime handle initializer needs it after the
4639 // move.
4640 let initial_runtime = config.initial_runtime_config.clone();
4641 let sync_mode = config.sync_mode;
4642 let super_versions = SuperVersions::new(
4643 version,
4644 &comparator,
4645 sync_mode,
4646 snapshot_id,
4647 config.manifest_log_rotate_bytes,
4648 );
4649 #[cfg(feature = "std")]
4650 let latest_super_version = super_versions.latest_handle();
4651 let inner = TreeInner {
4652 id: tree_id,
4653 memtable_id_counter: SequenceNumberCounter::new(1),
4654 table_id_counter: SequenceNumberCounter::new(highest_table_id + 1),
4655 blob_file_id_counter: SequenceNumberCounter::default(),
4656 version_history: Arc::new(RwLock::new(super_versions)),
4657 #[cfg(feature = "std")]
4658 latest_super_version,
4659 stop_signal: StopSignal::default(),
4660 config: Arc::new(config),
4661 major_compaction_lock: RwLock::default(),
4662 flush_lock: Mutex::default(),
4663 #[cfg(feature = "std")]
4664 _directory_lock: directory_lock,
4665 compaction_state: Arc::new(Mutex::new(CompactionState::default())),
4666 deletion_pause: Arc::clone(&deletion_pause),
4667 #[cfg(feature = "std")]
4668 background_deleter: Arc::clone(&background_deleter),
4669 heal_hints: Arc::clone(&heal_hints),
4670 kv_digest_at_insert: portable_atomic::AtomicU8::new(inner::kv_digest_at_insert_gate(
4671 &initial_runtime,
4672 )),
4673 runtime_config: Arc::new(crate::runtime_config::handle::RuntimeConfigHandle::new(
4674 initial_runtime,
4675 )),
4676 admission_used_cache: Mutex::new(None),
4677
4678 #[cfg(feature = "metrics")]
4679 metrics,
4680
4681 #[cfg(test)]
4682 test_hooks: inner::TestHooks::default(),
4683 };
4684
4685 // Install the pause on every recovered table / blob file so their
4686 // Drop impls consult it when a checkpoint is in flight. Snapshot
4687 // the Arc handles into owned collections so the read lock is
4688 // released before iterating (avoids `significant_drop_tightening`).
4689 // Snapshot the version under the read lock, then drop the lock before
4690 // collecting so the version_history lock isn't held across the clones.
4691 let version = inner.version_history.read().latest_version().version;
4692 let recovered_tables: Vec<Table> = version.iter_tables().cloned().collect();
4693 let recovered_blobs: Vec<BlobFile> = version.blob_files.iter().cloned().collect();
4694
4695 let sinks = crate::table::TableSinks {
4696 deletion_pause: &deletion_pause,
4697 heal_hints: &heal_hints,
4698 #[cfg(feature = "std")]
4699 background_deleter: Some(&background_deleter),
4700 };
4701 for table in &recovered_tables {
4702 table.bind_to_tree(&sinks);
4703 }
4704 for blob_file in &recovered_blobs {
4705 blob_file.bind_to_tree(&sinks);
4706 }
4707
4708 // Re-arm the tight-space reclaims a previous session could not finish.
4709 // A reclaim deferred because a checkpoint still hard-linked the file
4710 // lived only in that session's queue, and the unrestricted view that
4711 // could re-arm it is gone once the process restarts, so the consumed
4712 // prefix would stay allocated for the table's lifetime. Nothing is
4713 // persisted for this: the intent is DERIVABLE, and the extent is
4714 // exactly the prefix the committed bound cuts away. A prefix already
4715 // punched re-punches as a no-op, so a completed reclaim costs one
4716 // call, and only on a restricted table.
4717 #[cfg(feature = "std")]
4718 {
4719 for table in &recovered_tables {
4720 let Some(bound) = table.restrict_lower_bound() else {
4721 continue;
4722 };
4723 // Reclaiming frees space; it never decides what the tree
4724 // serves. A table that opened cleanly must not be denied to
4725 // readers because its punch offset cannot be re-derived, so a
4726 // failure here is reported and skipped. An ENVIRONMENTAL fault
4727 // still propagates: it says nothing about this table, and a
4728 // retry can re-read it.
4729 let offset = match table.punch_offset_for(bound) {
4730 Ok(offset) => offset,
4731 Err(e) if e.is_environmental() => return Err(e),
4732 Err(e) => {
4733 log::warn!(
4734 "table {} carries a committed restriction whose punch offset \
4735 could not be re-derived ({e}); its consumed prefix stays \
4736 allocated until the next tight-space compaction",
4737 table.id(),
4738 );
4739 continue;
4740 }
4741 };
4742 if offset > 0 {
4743 deletion_pause.retain_reclaim(
4744 Arc::clone(&table.fs),
4745 (*table.path).clone(),
4746 alloc::vec![(0, offset)],
4747 );
4748 }
4749 }
4750 // Blob files carry the same deferred intent, in their committed
4751 // frontier rather than a restriction bound.
4752 for blob in &recovered_blobs {
4753 let extent = match blob.committed_reclaimable_prefix() {
4754 Ok(Some(extent)) => extent,
4755 Ok(None) => continue,
4756 Err(e) if e.is_environmental() => return Err(e),
4757 Err(e) => {
4758 log::warn!(
4759 "blob file {:?} carries a committed frontier whose reclaimable \
4760 extent could not be re-derived ({e}); its consumed prefix stays \
4761 allocated until the next relocation",
4762 blob.id(),
4763 );
4764 continue;
4765 }
4766 };
4767 deletion_pause.retain_reclaim(
4768 Arc::clone(&blob.0.fs),
4769 blob.0.path.clone(),
4770 alloc::vec![extent],
4771 );
4772 }
4773 deletion_pause.retry_pending_reclaims();
4774 }
4775
4776 Ok(Self(Arc::new(inner)))
4777 }
4778
4779 /// Creates a new LSM-tree in a directory.
4780 fn create_new(
4781 config: Config,
4782 // The cross-process directory lock acquired by `Tree::open`, held for
4783 // the tree's lifetime.
4784 #[cfg(feature = "std")] directory_lock: Option<Box<dyn crate::fs::FsFile>>,
4785 ) -> crate::Result<Self> {
4786 use crate::file::fsync_directory;
4787
4788 let path = config.path.clone();
4789 log::trace!("Creating LSM-tree at {}", path.display());
4790
4791 let sync_mode = config.sync_mode;
4792
4793 (*config.fs).create_dir_all(&path)?;
4794
4795 // Create tables directories for all configured paths (primary + routes).
4796 // create_dir_all may create both <route> and <route>/tables.
4797 // Fsync the tables dir, its parent (route dir), AND the route's parent
4798 // to make all newly-created directory entries durable on POSIX.
4799 for (table_folder_path, folder_fs) in config.all_tables_folders() {
4800 folder_fs.create_dir_all(&table_folder_path)?;
4801 fsync_directory(&table_folder_path, &*folder_fs, sync_mode)?;
4802 if let Some(parent) = table_folder_path.parent() {
4803 fsync_directory(parent, &*folder_fs, sync_mode)?;
4804 if let Some(grandparent) = parent.parent() {
4805 fsync_directory(grandparent, &*folder_fs, sync_mode)?;
4806 }
4807 }
4808 }
4809
4810 // IMPORTANT: fsync primary folder on Unix
4811 fsync_directory(&path, &*config.fs, sync_mode)?;
4812
4813 let inner = TreeInner::create_new(
4814 config,
4815 #[cfg(feature = "std")]
4816 directory_lock,
4817 )?;
4818 Ok(Self(Arc::new(inner)))
4819 }
4820
4821 /// Recovers the level manifest, loading all tables from disk.
4822 ///
4823 /// When [`level_routes`](Config::level_routes) is configured, all
4824 /// configured table folders are scanned so tables on different storage
4825 /// tiers are discovered correctly.
4826 #[expect(
4827 clippy::too_many_lines,
4828 reason = "recovery logic is inherently complex"
4829 )]
4830 fn recover_levels<P: AsRef<Path>>(
4831 tree_path: P,
4832 tree_id: TreeId,
4833 config: &Config,
4834 #[cfg(feature = "metrics")] metrics: &Arc<Metrics>,
4835 ) -> crate::Result<Version> {
4836 use crate::{TableId, file::fsync_directory};
4837
4838 let tree_path = tree_path.as_ref();
4839
4840 let recovery = recover(
4841 tree_path,
4842 &*config.fs,
4843 config.manifest_recovery_mode,
4844 config.encryption.clone(),
4845 )?;
4846
4847 // The on-disk snapshot CURRENT points at — the generation orphan cleanup
4848 // must preserve. Intermediate versions live only in the edit log, so the
4849 // latest version id (`version.id()`) has no `v{id}` file of its own.
4850 let snapshot_id = recovery.snapshot_id;
4851
4852 let mut table_map = {
4853 let mut result: crate::HashMap<TableId, (u8 /* Level index */, Checksum, SeqNo)> =
4854 crate::HashMap::default();
4855
4856 for (level_idx, table_ids) in recovery.table_ids.iter().enumerate() {
4857 for run in table_ids {
4858 for table in run {
4859 #[expect(
4860 clippy::expect_used,
4861 reason = "there are always less than 256 levels"
4862 )]
4863 result.insert(
4864 table.id,
4865 (
4866 level_idx
4867 .try_into()
4868 .expect("there are less than 256 levels"),
4869 table.checksum,
4870 table.global_seqno,
4871 ),
4872 );
4873 }
4874 }
4875 }
4876
4877 result
4878 };
4879
4880 let cnt = table_map.len();
4881
4882 // Immutable snapshot of every table id the manifest knows. `table_map`
4883 // is drained as tables are recovered below, so it cannot answer "is this
4884 // id live?" order-independently; this set can. Used to sweep an orphaned
4885 // `.heal-attest` sidecar whose SST was retired.
4886 let manifest_ids: crate::HashSet<TableId> = table_map.keys().copied().collect();
4887
4888 log::debug!("Recovering {cnt} tables from {}", tree_path.display());
4889
4890 let progress_mod = match cnt {
4891 _ if cnt <= 20 => 1,
4892 _ if cnt <= 100 => 10,
4893 _ => 100,
4894 };
4895
4896 let mut tables = vec![];
4897 // Track recovered table IDs so duplicate sightings (via symlinks,
4898 // junctions, or case-insensitive aliases of the same directory) are
4899 // skipped rather than orphan-deleted.
4900 let mut recovered_table_ids: crate::HashSet<TableId> = crate::HashSet::default();
4901 let mut orphaned_tables: Vec<(crate::path::PathBuf, Arc<dyn crate::fs::Fs>)> = vec![];
4902 // Copies the digest PROVED stale. Left on disk they are worse than
4903 // clutter: once the winning route is removed or temporarily unmounted,
4904 // such a file is the only sighting of its id, arbitration is skipped
4905 // for want of a duplicate, and the stale generation is served instead
4906 // of the missing route being reported. They are swept only after a
4907 // winner for that id is established, so a scan that never finds one
4908 // keeps every copy it has.
4909 let mut rejected_copies: Vec<(TableId, crate::path::PathBuf, Arc<dyn crate::fs::Fs>)> =
4910 Vec::new();
4911 // Where an ambiguous id was accepted from, so a LATER sighting of it can
4912 // be judged against the winner rather than merely skipped.
4913 let mut accepted_copies: crate::HashMap<
4914 TableId,
4915 (crate::path::PathBuf, Arc<dyn crate::fs::Fs>),
4916 > = crate::HashMap::default();
4917 // First recovery failure per manifest id, kept until a later routed
4918 // folder yields a copy that opens. Whatever is left once every folder
4919 // is scanned is a table the manifest names and no copy delivers, so
4920 // its error is the open's.
4921 let mut unrecovered_sightings: crate::HashMap<TableId, crate::Error> =
4922 crate::HashMap::default();
4923 // Repair replacements whose authority could not be settled against the
4924 // damaged original beside them. Held until every folder is scanned: a
4925 // later routed copy that opens against the manifest settles it (the temp
4926 // is then provably NOT what the manifest names, so it is swept there and
4927 // then), and only an id no copy delivers surfaces its ambiguity.
4928 // A LIST, not a per-id map: routing can put a temp for the same id in
4929 // more than one folder, and each one is a file that has to be settled.
4930 let mut deferred_temps: Vec<(
4931 TableId,
4932 crate::path::PathBuf,
4933 Arc<dyn crate::fs::Fs>,
4934 crate::Error,
4935 )> = Vec::new();
4936
4937 // Scan all configured table folders (primary + level routes).
4938 let all_folders = config.all_tables_folders();
4939
4940 // One listing per folder, taken up front. The recovery loop has to know
4941 // whether an id is sighted in MORE THAN ONE routed folder BEFORE it
4942 // accepts the first sighting, and answering that later would mean
4943 // listing every folder a second time.
4944 let mut folder_scans: Vec<(
4945 &crate::path::PathBuf,
4946 &Arc<dyn crate::fs::Fs>,
4947 Vec<crate::fs::FsDirEntry>,
4948 )> = Vec::with_capacity(all_folders.len());
4949 // Ids present in more than one folder. Only these are digest-arbitrated
4950 // at open: a post-commit sweep that failed leaves an INTACT stale twin,
4951 // and `Table::recover` parses structure without re-deriving the
4952 // manifest's digest, so it opens the stale generation just as happily.
4953 // Ids sighted once cannot be ambiguous, and hashing them would turn
4954 // every open into a full read of the tree.
4955 let mut folder_sightings: crate::HashMap<TableId, usize> = crate::HashMap::default();
4956 for (table_base_folder, folder_fs) in &all_folders {
4957 if !folder_fs.exists(table_base_folder)? {
4958 folder_fs.create_dir_all(table_base_folder)?;
4959 fsync_directory(table_base_folder, &**folder_fs, config.sync_mode)?;
4960 if let Some(parent) = table_base_folder.parent() {
4961 fsync_directory(parent, &**folder_fs, config.sync_mode)?;
4962 if let Some(grandparent) = parent.parent() {
4963 fsync_directory(grandparent, &**folder_fs, config.sync_mode)?;
4964 }
4965 }
4966 }
4967
4968 // Pending repair swaps resolve FIRST: a committed swap's `{id}` file
4969 // is the superseded source until the rename lands, so adopting it
4970 // before the temp entry is processed would hand this session a
4971 // handle onto bytes the finished swap then replaces on disk.
4972 let mut dirents = folder_fs.read_dir(table_base_folder)?;
4973 dirents.sort_by_key(|e| {
4974 !matches!(
4975 crate::file::TableDirEntry::classify(&e.file_name),
4976 crate::file::TableDirEntry::RepairTmp(_)
4977 )
4978 });
4979 for dirent in &dirents {
4980 if let crate::file::TableDirEntry::Table(id) =
4981 crate::file::TableDirEntry::classify(&dirent.file_name)
4982 {
4983 *folder_sightings.entry(id).or_insert(0) += 1;
4984 }
4985 }
4986 folder_scans.push((table_base_folder, folder_fs, dirents));
4987 }
4988 let ambiguous_ids: crate::HashSet<TableId> = folder_sightings
4989 .into_iter()
4990 .filter(|(_, count)| *count > 1)
4991 .map(|(id, _)| id)
4992 .collect();
4993
4994 for (table_base_folder, folder_fs, dirents) in folder_scans {
4995 for dirent in dirents {
4996 let crate::fs::FsDirEntry {
4997 path: table_file_path,
4998 file_name,
4999 is_dir,
5000 } = dirent;
5001
5002 let table_file_name = &file_name;
5003 if is_dir {
5004 log::warn!(
5005 "Skipping unexpected directory in tables folder: {}",
5006 table_file_path.display()
5007 );
5008 continue;
5009 }
5010
5011 // One grammar decides what each name IS (`TableDirEntry`, shared
5012 // with the repair scan so the two can never disagree on
5013 // ownership); this match is the open's POLICY for each kind.
5014 let table_id = match crate::file::TableDirEntry::classify(table_file_name) {
5015 // An in-place heal's detach copy, renamed over the live path
5016 // on success: a survivor is a crash leftover no manifest ever
5017 // references. Sweep it.
5018 crate::file::TableDirEntry::HealTmp(_) => {
5019 log::warn!(
5020 "Removing abandoned heal copy: {}",
5021 table_file_path.display()
5022 );
5023 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5024 continue;
5025 }
5026 // A crashed attestation publish: either the live sidecar it
5027 // would have replaced still bridges the crash window, or the
5028 // heal is re-run. Disposable.
5029 crate::file::TableDirEntry::HealAttestTmp(_) => {
5030 log::warn!(
5031 "Removing abandoned heal-attest temp: {}",
5032 table_file_path.display()
5033 );
5034 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5035 continue;
5036 }
5037 // A LIVE table's pending attestation is preserved (the next
5038 // scrub reconciles a crashed digest refresh through it). One
5039 // whose SST left the manifest is unreconcilable forever:
5040 // sweep the orphan rather than re-process it on every open.
5041 crate::file::TableDirEntry::HealAttest(attest_id) => {
5042 if !manifest_ids.contains(&attest_id) {
5043 log::warn!(
5044 "Removing orphaned heal attestation (its table is gone): {}",
5045 table_file_path.display()
5046 );
5047 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5048 }
5049 continue;
5050 }
5051 // A crashed bound publish. Disposable.
5052 crate::file::TableDirEntry::RestrictBoundTmp(_) => {
5053 log::warn!(
5054 "Removing abandoned restrict-bound temp: {}",
5055 table_file_path.display()
5056 );
5057 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5058 continue;
5059 }
5060 // A LIVE table's restriction bound is preserved (manifest
5061 // repair reads it); an ORPHAN one is swept so a reused id
5062 // cannot later pick up a stale restriction.
5063 crate::file::TableDirEntry::RestrictBound(bound_id) => {
5064 if !manifest_ids.contains(&bound_id) {
5065 log::warn!(
5066 "Removing orphaned restrict-bound sidecar (its table is gone): {}",
5067 table_file_path.display()
5068 );
5069 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5070 }
5071 continue;
5072 }
5073 // A repair's replacement still at its temp name. The manifest
5074 // is the authority on what it is — but it names the id in
5075 // BOTH crash cases (before the commit its entry still
5076 // describes the SOURCE beside the temp), so the entry's
5077 // checksum decides: only a committed repair recorded the
5078 // temp's digest, and it died before the swap — this file is
5079 // what the manifest describes, so the swap is finished.
5080 // Any other temp is an abandoned build (possibly truncated
5081 // mid-write), and swapping it in would destroy the source
5082 // the manifest names: it is garbage. An open resolves this
5083 // exactly as a re-run of the repair would, from the durable
5084 // manifest alone.
5085 crate::file::TableDirEntry::RepairTmp(tmp_id) => {
5086 #[cfg(feature = "std")]
5087 {
5088 let published = match table_map.get(&tmp_id) {
5089 Some(&(_, manifest_checksum, _)) => {
5090 match crate::repair::repair_tmp_is_published(
5091 config,
5092 folder_fs,
5093 &table_file_path,
5094 tmp_id,
5095 manifest_checksum,
5096 recovery.restrictions.get(&tmp_id),
5097 ) {
5098 Ok(published) => published,
5099 // A refused mount / missing key says
5100 // nothing about which copy the manifest
5101 // names: surface it.
5102 Err(e) if e.is_environmental() => return Err(e),
5103 // Neither this temp nor the damaged
5104 // original beside it can prove it is the
5105 // manifest's copy. A LATER routed folder
5106 // may still hold that copy, and deciding
5107 // here would end the open on a leftover a
5108 // failed post-commit sweep left behind.
5109 // Defer: leave the temp untouched, keep
5110 // the ambiguity, and let the end of the
5111 // scan decide (see `deferred_temps`).
5112 Err(e) => {
5113 log::warn!(
5114 "repair replacement {} is ambiguous ({e}); \
5115 deferring until every routed folder is scanned",
5116 table_file_path.display(),
5117 );
5118 deferred_temps.push((
5119 tmp_id,
5120 table_file_path,
5121 Arc::clone(folder_fs),
5122 e,
5123 ));
5124 continue;
5125 }
5126 }
5127 }
5128 None => false,
5129 };
5130 if published {
5131 log::warn!(
5132 "Finishing a repair's pending swap of table {tmp_id}: {}",
5133 table_file_path.display()
5134 );
5135 crate::repair::commit_repair_tmp(
5136 folder_fs.as_ref(),
5137 &table_file_path,
5138 &table_base_folder.join(tmp_id.to_string()),
5139 config.sync_mode,
5140 recovery.restrictions.contains_key(&tmp_id),
5141 )?;
5142 } else {
5143 log::warn!(
5144 "Removing abandoned repair replacement: {}",
5145 table_file_path.display()
5146 );
5147 // A RESTRICTED salvage also wrote
5148 // `{temp}.restrict-bound`; that companion must
5149 // go with the temp — its name classifies as
5150 // Foreign and would fail this very open.
5151 let companion =
5152 crate::restrict_bound::sidecar_path(&table_file_path);
5153 if folder_fs.exists(&companion)? {
5154 Self::sweep_artifact(folder_fs.as_ref(), &companion)?;
5155 }
5156 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5157 }
5158 }
5159 // Without the repair module nothing here can verify or
5160 // finish a swap the manifest may describe, and sweeping
5161 // the temp could destroy the only copy of what the
5162 // manifest names.
5163 #[cfg(not(feature = "std"))]
5164 {
5165 if manifest_ids.contains(&tmp_id) {
5166 log::error!(
5167 "Table {tmp_id} exists only as an unpublished repair \
5168 replacement; run a repair to finish it: {}",
5169 table_file_path.display()
5170 );
5171 return Err(crate::Error::Unrecoverable);
5172 }
5173 log::warn!(
5174 "Removing abandoned repair replacement: {}",
5175 table_file_path.display()
5176 );
5177 // Same companion rule as the std arm above (the
5178 // `restrict_bound` helper is std-gated, so the
5179 // name is spelled out).
5180 let companion = table_base_folder.join(alloc::format!(
5181 "{tmp_id}{}.restrict-bound",
5182 crate::file::REPAIR_TMP_SUFFIX
5183 ));
5184 if folder_fs.exists(&companion)? {
5185 Self::sweep_artifact(folder_fs.as_ref(), &companion)?;
5186 }
5187 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5188 }
5189 continue;
5190 }
5191 // The companion's fate followed its temp when that entry
5192 // resolved (first, by sort order): a finished swap renamed
5193 // it into place, an abandoned build's sweep removed it. A
5194 // survivor here is an orphan (its temp is gone) — remove
5195 // it rather than reject the whole open over it.
5196 crate::file::TableDirEntry::RepairTmpCompanion(_) => {
5197 if folder_fs.exists(&table_file_path)? {
5198 log::warn!(
5199 "Removing orphaned repair-replacement sidecar: {}",
5200 table_file_path.display()
5201 );
5202 Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5203 }
5204 continue;
5205 }
5206 // Not a shape the engine names, so not engine state: passed
5207 // over untouched. Refusing the store here would let any
5208 // stray file (an operator's note, a backup, a desktop
5209 // environment's directory metadata) make it unopenable,
5210 // which is why scanners used to carry a list of foreign
5211 // names to tolerate. The grammar answers it instead.
5212 crate::file::TableDirEntry::Foreign => {
5213 log::debug!(
5214 "Ignoring {table_file_name:?} in the tables folder: not an engine file"
5215 );
5216 continue;
5217 }
5218 crate::file::TableDirEntry::Table(id) => id,
5219 };
5220
5221 // Remove from map to prevent duplicate recovery if the same
5222 // table file exists in multiple scanned folders.
5223 if let Some(entry) = table_map.remove(&table_id) {
5224 let (level_idx, checksum, global_seqno) = entry;
5225 let pin_filter = config.filter_block_pinning_policy.get(level_idx.into());
5226 let pin_index = config.index_block_pinning_policy.get(level_idx.into());
5227
5228 let table = {
5229 let mut params = crate::table::RecoverParams::new(
5230 table_file_path.clone(),
5231 checksum,
5232 table_id,
5233 folder_fs.clone(),
5234 config.comparator.clone(),
5235 config.cache.clone(),
5236 );
5237 params.global_seqno = global_seqno;
5238 params.tree_id = tree_id;
5239 params.descriptor_table.clone_from(&config.descriptor_table);
5240 params.pin_filter = pin_filter;
5241 params.pin_index = pin_index;
5242 params.encryption.clone_from(&config.encryption);
5243 #[cfg(zstd_any)]
5244 {
5245 params.zstd_dictionary.clone_from(&config.zstd_dictionary);
5246 }
5247 #[cfg(feature = "metrics")]
5248 {
5249 params.metrics = metrics.clone();
5250 }
5251 match Table::recover(params) {
5252 Ok(table) => table,
5253 Err(e) => {
5254 // A routed tree can hold this id in more than
5255 // one folder, and repair's post-commit sweep of
5256 // the copy it displaced can fail after the
5257 // manifest is already durable. Ending the search
5258 // on the first sighting would then shut the tree
5259 // on a table that is present and intact one
5260 // folder over. Put the id back, remember the
5261 // failure, and keep scanning; if no folder
5262 // yields the manifest's copy, this error is what
5263 // the open reports.
5264 log::warn!(
5265 "table {table_id} at {} did not recover ({e}); \
5266 looking for another routed copy",
5267 table_file_path.display(),
5268 );
5269 table_map.insert(table_id, entry);
5270 unrecovered_sightings.entry(table_id).or_insert(e);
5271 continue;
5272 }
5273 }
5274 };
5275
5276 // Opening proves the file is STRUCTURALLY sound, never that
5277 // it is the generation the manifest committed: recover parses
5278 // metadata and loads data blocks lazily. Where two folders
5279 // hold this id, take the digest as the arbiter, exactly as
5280 // repair does, so a stale twin an interrupted sweep left
5281 // behind cannot win on scan order.
5282 if ambiguous_ids.contains(&table_id) {
5283 // The candidate is UNRESTRICTED here: the manifest's
5284 // restrictions are attached when the version is built,
5285 // which is after this scan. Digesting it whole would
5286 // compare against a live-suffix digest and reject every
5287 // copy of a restricted id, so the bound is supplied
5288 // explicitly.
5289 let live =
5290 match table.suffix_checksum_for(recovery.restrictions.get(&table_id)) {
5291 Ok(live) => live,
5292 // A fault in the ENVIRONMENT says nothing about
5293 // these bytes, so it propagates and a retry can
5294 // re-read them.
5295 Err(e) if e.is_environmental() => return Err(e),
5296 // Anything else is damage to THIS copy: metadata
5297 // can parse while a data extent is unreadable, and
5298 // ending the scan on it would let a damaged
5299 // displaced copy permanently block the intact one a
5300 // folder over. Same treatment as a failed recover.
5301 Err(e) => {
5302 log::warn!(
5303 "table {table_id} at {} could not be digested ({e}); \
5304 looking for another routed copy",
5305 table_file_path.display(),
5306 );
5307 table_map.insert(table_id, entry);
5308 unrecovered_sightings.entry(table_id).or_insert(e);
5309 continue;
5310 }
5311 };
5312 if live != checksum {
5313 log::warn!(
5314 "table {table_id} at {} is not the generation the manifest \
5315 committed; looking for another routed copy",
5316 table_file_path.display(),
5317 );
5318 table_map.insert(table_id, entry);
5319 unrecovered_sightings.entry(table_id).or_insert(
5320 crate::Error::ChecksumMismatch {
5321 got: live,
5322 expected: checksum,
5323 },
5324 );
5325 rejected_copies.push((
5326 table_id,
5327 table_file_path,
5328 Arc::clone(folder_fs),
5329 ));
5330 continue;
5331 }
5332 accepted_copies
5333 .insert(table_id, (table_file_path.clone(), Arc::clone(folder_fs)));
5334 }
5335
5336 tables.push(table);
5337 recovered_table_ids.insert(table_id);
5338 unrecovered_sightings.remove(&table_id);
5339
5340 if tables.len() % progress_mod == 0 {
5341 log::debug!("Recovered {}/{cnt} tables", tables.len());
5342 }
5343 } else if recovered_table_ids.contains(&table_id) {
5344 // Duplicate sighting of an already-recovered manifest table
5345 // (e.g., via symlink or case-insensitive alias). Never an
5346 // orphan: that would delete the live SST. But an id the
5347 // digest arbitrated has a KNOWN answer, so a later sighting
5348 // is judged rather than merely skipped, or a stale twin
5349 // scanned after the winner would outlive the open.
5350 let stale = match accepted_copies.get(&table_id) {
5351 Some((winner, winner_fs)) if *winner != table_file_path => {
5352 differs_byte_for_byte(
5353 &**winner_fs,
5354 winner,
5355 folder_fs.as_ref(),
5356 &table_file_path,
5357 )?
5358 }
5359 _ => false,
5360 };
5361 if stale {
5362 rejected_copies.push((
5363 table_id,
5364 table_file_path.clone(),
5365 Arc::clone(folder_fs),
5366 ));
5367 }
5368 log::warn!(
5369 "Skipping duplicate sighting of manifest table {table_id} in {}",
5370 table_file_path.display(),
5371 );
5372 } else {
5373 orphaned_tables.push((table_file_path, folder_fs.clone()));
5374 }
5375 }
5376 }
5377
5378 // Every folder is scanned, so an id that never recovered has no copy
5379 // left to try. Report the failure that stopped it — a missing-table
5380 // diagnostic would hide the reason the file on disk could not be read.
5381 if let Some((_, e)) = unrecovered_sightings
5382 .into_iter()
5383 .find(|(id, _)| table_map.contains_key(id))
5384 {
5385 return Err(e);
5386 }
5387
5388 // A deferred replacement whose id RECOVERED from some folder is settled:
5389 // that copy matched the manifest, so this temp is provably NOT what the
5390 // manifest names — an abandoned build, removed here exactly as the
5391 // in-scan branch removes one. One whose id never arrived is still
5392 // ambiguous: the temp may be the only copy of what the manifest
5393 // describes, so the ambiguity is what the open reports.
5394 #[cfg(feature = "std")]
5395 for (tmp_id, temp_path, temp_fs, ambiguity) in deferred_temps {
5396 if !recovered_table_ids.contains(&tmp_id) {
5397 return Err(ambiguity);
5398 }
5399 log::warn!(
5400 "Removing abandoned repair replacement for table {tmp_id} (the copy this open \
5401 recovered is what the manifest names): {}",
5402 temp_path.display(),
5403 );
5404 // The restricted-salvage companion classifies as Foreign and would
5405 // fail the next open, so it goes with the temp.
5406 let companion = crate::restrict_bound::sidecar_path(&temp_path);
5407 if temp_fs.exists(&companion)? {
5408 Self::sweep_artifact(temp_fs.as_ref(), &companion)?;
5409 }
5410 Self::sweep_artifact(temp_fs.as_ref(), &temp_path)?;
5411 }
5412
5413 if tables.len() < cnt {
5414 // Route configuration is NOT persisted. This is a best-effort
5415 // heuristic: it checks each missing table's level against the
5416 // current routes, but cannot detect same-level path changes
5417 // (e.g., L0 routed to /hot_old → /hot_new). Persisting route
5418 // provenance per-table in the manifest would enable exact
5419 // detection but requires a format change.
5420 //
5421 // - Level IS covered by a current route → its directory was scanned
5422 // and the file was not found → data corruption / deletion.
5423 // - Level is NOT covered → falls back to primary (always scanned).
5424 // If the table isn't there, it was likely in a route that has
5425 // since been removed from the config.
5426 //
5427 // Return RouteMismatch only when ALL missing tables are on levels
5428 // not covered by any current route. If ANY missing table is on a
5429 // covered level, at least one SST was genuinely lost.
5430 if let Some(routes) = &config.level_routes {
5431 let all_missing_uncovered = table_map
5432 .values()
5433 .all(|(level, _, _)| !routes.iter().any(|r| r.levels.contains(level)));
5434
5435 if all_missing_uncovered {
5436 let found = tables.len();
5437 let missing_ids: Vec<_> = table_map.keys().collect();
5438
5439 log::error!(
5440 "Route mismatch: expected {cnt} tables but found {found} — \
5441 level_routes do not cover all previously used levels. \
5442 Missing table IDs: {missing_ids:?}",
5443 );
5444 return Err(crate::Error::RouteMismatch {
5445 expected: cnt,
5446 found,
5447 });
5448 }
5449 }
5450
5451 log::error!(
5452 "Recovered less tables than expected: {:?}",
5453 table_map.keys(),
5454 );
5455 return Err(crate::Error::Unrecoverable);
5456 }
5457
5458 log::debug!("Successfully recovered {} tables", tables.len());
5459
5460 // Pair each blob file with its live-data frontier: a file whose consumed
5461 // prefix was reclaimed in place records a checksum over the suffix only,
5462 // so the recovered view must carry the offset that digest starts at.
5463 let blob_ids_with_frontier: Vec<(crate::vlog::BlobFileId, crate::Checksum, u64)> = recovery
5464 .blob_file_ids
5465 .iter()
5466 .map(|&(id, checksum)| {
5467 (
5468 id,
5469 checksum,
5470 recovery.blob_restrictions.get(&id).copied().unwrap_or(0),
5471 )
5472 })
5473 .collect();
5474 let (blob_files, orphaned_blob_files) = crate::vlog::recover_blob_files(
5475 &tree_path.join(crate::file::BLOBS_FOLDER),
5476 &blob_ids_with_frontier,
5477 tree_id,
5478 config.descriptor_table.as_ref(),
5479 &config.fs,
5480 )?;
5481
5482 let version = Version::from_recovery(recovery, &tables, &blob_files)?;
5483
5484 // Republish any restriction sidecar that never landed. The sidecar is
5485 // written AFTER its slice commits, so a failure (or a crash) in that
5486 // window leaves a committed restriction with no `.restrict-bound` file.
5487 // The manifest still describes it, so a normal open is unaffected — but
5488 // a later manifest-loss repair would find an unrestricted input beside
5489 // the slice output and publish BOTH histories, applying every merge
5490 // operand of the consumed prefix twice (operands are deliberately never
5491 // deduplicated across sources). The manifest is the authority for the
5492 // bound, so derive the missing sidecar from it here and the window
5493 // closes at the next open rather than staying open forever.
5494 #[cfg(feature = "std")]
5495 Self::republish_missing_restriction_sidecars(&version, config)?;
5496
5497 // NOTE: Cleanup old versions
5498 // But only after we definitely recovered the latest version.
5499 // Preserve the snapshot CURRENT references (and its `edits-` log) — the
5500 // latest version id has no file of its own under the incremental
5501 // manifest, so cleaning by it would delete the live snapshot.
5502 Self::cleanup_orphaned_version(tree_path, snapshot_id, &*config.fs)?;
5503
5504 // A copy the digest rejected goes only once its id has a winner: with
5505 // one it is provably superseded, without one it may be all that is
5506 // left. Its `.restrict-bound` companion goes with it, or the next scan
5507 // classifies that name as foreign and fails the open.
5508 for (table_id, path, copy_fs) in rejected_copies {
5509 if !recovered_table_ids.contains(&table_id) {
5510 continue;
5511 }
5512 log::warn!(
5513 "Removing superseded copy of table {table_id}: {}",
5514 path.display()
5515 );
5516 #[cfg(feature = "std")]
5517 {
5518 let companion = crate::restrict_bound::sidecar_path(&path);
5519 if copy_fs.exists(&companion)? {
5520 Self::sweep_artifact(copy_fs.as_ref(), &companion)?;
5521 }
5522 }
5523 Self::sweep_artifact(copy_fs.as_ref(), &path)?;
5524 }
5525
5526 for (table_path, orphan_fs) in orphaned_tables {
5527 log::debug!("Deleting orphaned table {}", table_path.display());
5528 orphan_fs.remove_file(&table_path)?;
5529 }
5530
5531 for blob_file_path in orphaned_blob_files {
5532 log::debug!("Deleting orphaned blob file {}", blob_file_path.display());
5533 (*config.fs).remove_file(&blob_file_path)?;
5534 }
5535
5536 Ok(version)
5537 }
5538
5539 /// Writes the `.restrict-bound` sidecar of every restricted table in
5540 /// `version` that has none, so the bound the manifest holds is recoverable
5541 /// without it. An existing sidecar is REREAD rather than assumed good: its
5542 /// mere presence proves nothing, and a repair that later trusts a corrupt
5543 /// one derives a conservative bound (dropping up to one live block), while
5544 /// a valid-but-STALE one — the shape a second slice leaves when its own
5545 /// write fails — restricts LESS than reality and resurrects consumed rows.
5546 /// A sidecar that already records this table and this bound is left alone,
5547 /// so a healthy tree does not churn the file on every open. Failures
5548 /// propagate — an unrecoverable restriction is exactly what this closes, so
5549 /// silently skipping it would keep the window open.
5550 ///
5551 /// The sidecar is a filesystem artifact of the tight-space reclaim path, so
5552 /// this is a no-op without `std` — a build without it never writes one.
5553 #[cfg(feature = "std")]
5554 fn republish_missing_restriction_sidecars(
5555 version: &Version,
5556 config: &Config,
5557 ) -> crate::Result<()> {
5558 for table in version.iter_tables() {
5559 let Some(bound) = table.restrict_lower_bound() else {
5560 continue;
5561 };
5562 let recorded =
5563 crate::restrict_bound::read(&*table.fs, &table.path, config.encryption.as_deref())?;
5564 let state = match &recorded {
5565 crate::restrict_bound::SidecarRead::Present(id, recorded_bound)
5566 if *id == table.metadata.id && recorded_bound.as_slice() == bound.as_ref() =>
5567 {
5568 continue;
5569 }
5570 crate::restrict_bound::SidecarRead::Present(..) => {
5571 "disagrees with the manifest (stale bound or another table)"
5572 }
5573 crate::restrict_bound::SidecarRead::Corrupt => "unreadable",
5574 crate::restrict_bound::SidecarRead::Missing => "absent",
5575 };
5576 log::warn!(
5577 "table {} carries a committed restriction whose sidecar is {state}; \
5578 republishing it from the manifest",
5579 table.id(),
5580 );
5581 table.write_restrict_sidecar(bound, config.sync_mode)?;
5582 }
5583 Ok(())
5584 }
5585
5586 /// Removes a recovery-swept artifact (an abandoned heal copy / temp / marker),
5587 /// treating a concurrent removal (`NotFound`) as success. A benign race (a
5588 /// retry, or another scanner that already swept it) must not fail recovery,
5589 /// matching [`Self::cleanup_orphaned_version`].
5590 fn sweep_artifact(fs: &dyn Fs, path: &Path) -> crate::Result<()> {
5591 match fs.remove_file(path) {
5592 Ok(()) => Ok(()),
5593 Err(e) if e.kind() == crate::io::ErrorKind::NotFound => Ok(()),
5594 Err(e) => Err(e.into()),
5595 }
5596 }
5597
5598 /// Removes stale manifest files left by older generations: every `v{id}`
5599 /// snapshot except the live one (`v{snapshot_id}`) and every `edits-{id}`
5600 /// log except the live snapshot's (`edits-{snapshot_id}`). A crashed
5601 /// rotation can leak an old snapshot or its log; this sweeps them on open.
5602 /// The live snapshot and log are exactly the generation `CURRENT` points at.
5603 ///
5604 /// # Behavior change vs pre-Fs-trait code
5605 ///
5606 /// The previous implementation used `std::fs::read_dir` + `to_string_lossy()`,
5607 /// which silently skipped non-UTF-8 filenames. `Fs::read_dir` returns
5608 /// `InvalidData` for such entries instead (see [`FsDirEntry`](crate::fs::FsDirEntry) docs), so this
5609 /// function now fails fast on non-UTF-8 names. This is intentional: version
5610 /// files are always `v{u64}` — any non-UTF-8 entry indicates filesystem
5611 /// corruption and should surface as an error rather than be silently ignored.
5612 fn cleanup_orphaned_version(
5613 path: &Path,
5614 snapshot_id: crate::version::VersionId,
5615 fs: &dyn crate::fs::Fs,
5616 ) -> crate::Result<()> {
5617 let snapshot_str = format!("v{snapshot_id}");
5618 let log_str = format!("edits-{snapshot_id}");
5619
5620 for dirent in fs.read_dir(path)? {
5621 if dirent.is_dir {
5622 continue;
5623 }
5624
5625 let name = &dirent.file_name;
5626 let is_orphan_snapshot = name.starts_with('v') && *name != snapshot_str;
5627 let is_orphan_log = name.starts_with("edits-") && *name != log_str;
5628 if is_orphan_snapshot || is_orphan_log {
5629 log::trace!("Cleanup orphaned manifest file {name}");
5630 match fs.remove_file(&dirent.path) {
5631 Ok(()) => {}
5632 Err(e) if e.kind() == crate::io::ErrorKind::NotFound => {}
5633 Err(e) => return Err(e.into()),
5634 }
5635 }
5636 }
5637
5638 Ok(())
5639 }
5640}
5641
5642/// Returns `true` if the directory contains version-related artifacts
5643/// (a `tables/` subdir, a `blobs/` subdir, or any `vN` manifest file).
5644///
5645/// Used by [`Tree::open`] to distinguish a genuinely fresh directory
5646/// (safe to `create_new`) from a half-written checkpoint or other
5647/// interrupted sealing (must error rather than silently overwrite).
5648///
5649/// A missing parent directory is treated as "no state" — `create_new`
5650/// is what creates the directory in the first place, so callers may
5651/// invoke `Tree::open` against a path that does not exist yet.
5652fn has_existing_version_state(folder: &Path, fs: &dyn Fs) -> crate::Result<bool> {
5653 if fs.exists(&folder.join(crate::file::TABLES_FOLDER))?
5654 || fs.exists(&folder.join(crate::file::BLOBS_FOLDER))?
5655 {
5656 return Ok(true);
5657 }
5658 let entries = match fs.read_dir(folder) {
5659 Ok(entries) => entries,
5660 Err(e) if e.kind() == crate::io::ErrorKind::NotFound => return Ok(false),
5661 Err(e) => return Err(e.into()),
5662 };
5663 for entry in entries {
5664 let name = &entry.file_name;
5665 if name.starts_with('v') && name.len() > 1 && name[1..].bytes().all(|c| c.is_ascii_digit())
5666 {
5667 return Ok(true);
5668 }
5669 }
5670 Ok(false)
5671}
5672
5673/// Whether two sightings of one table id PROVABLY hold different bytes.
5674///
5675/// Used to judge a duplicate scanned after its id was already accepted: the
5676/// winner matched the manifest, so a copy whose bytes differ from it cannot
5677/// also be the committed generation and is safe to sweep. Identical bytes mean
5678/// an alias or an exact copy, which is kept.
5679///
5680/// Only a completed comparison is proof. A read that fails on the BYTES proves
5681/// nothing about which generation this is, so it answers `false` and the file
5682/// stays; an ENVIRONMENTAL fault propagates, since a retry can re-read it.
5683///
5684/// # Errors
5685///
5686/// Propagates an environmental read failure of either file.
5687fn differs_byte_for_byte(
5688 winner_fs: &dyn crate::fs::Fs,
5689 winner: &Path,
5690 candidate_fs: &dyn crate::fs::Fs,
5691 candidate: &Path,
5692) -> crate::Result<bool> {
5693 let digest = |fs: &dyn crate::fs::Fs, path: &Path| -> crate::Result<Option<u128>> {
5694 match crate::file::checksum_from_with_overrides(fs, path, 0, &[]) {
5695 Ok(d) => Ok(Some(d)),
5696 Err(e) if e.is_environmental() => Err(e),
5697 Err(_) => Ok(None),
5698 }
5699 };
5700 let (Some(a), Some(b)) = (digest(winner_fs, winner)?, digest(candidate_fs, candidate)?) else {
5701 return Ok(false);
5702 };
5703 Ok(a != b)
5704}
5705
5706/// Raises a query's lower bound to a table's tight-space restriction, if any.
5707///
5708/// Keys below `restriction` are the punched-out prefix served by the
5709/// replacement table, so a range estimate must not charge them to the
5710/// restricted view. Returns `lo` unchanged when the table is unrestricted or
5711/// the restriction is at or below `lo`.
5712fn effective_lower_bound<'a>(
5713 lo: core::ops::Bound<&'a [u8]>,
5714 restriction: Option<&'a [u8]>,
5715 cmp: &dyn crate::comparator::UserComparator,
5716) -> core::ops::Bound<&'a [u8]> {
5717 use core::cmp::Ordering;
5718 use core::ops::Bound;
5719 match (lo, restriction) {
5720 (Bound::Unbounded, Some(rb)) => Bound::Included(rb),
5721 (Bound::Included(k) | Bound::Excluded(k), Some(rb))
5722 if cmp.compare(rb, k) == Ordering::Greater =>
5723 {
5724 Bound::Included(rb)
5725 }
5726 _ => lo,
5727 }
5728}
5729
5730#[cfg(test)]
5731mod cardinality_tests;
5732
5733#[cfg(test)]
5734#[expect(clippy::expect_used, reason = "test code")]
5735mod scan_since_freeze_tests;
5736
5737#[cfg(all(test, feature = "metrics"))]
5738mod cache_stats_tests;
5739
5740#[cfg(all(test, feature = "std"))]
5741#[expect(clippy::expect_used, reason = "test code")]
5742mod restricted_reclaim_tests;