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(crate) mod columnar_scan;
7mod data_stage;
8pub(crate) mod ingest;
9pub(crate) mod inner;
10mod level_resolve;
11mod level_stages;
12mod multi_get_read;
13pub(crate) mod read_job;
14pub mod resumable;
15pub(crate) mod sealed;
16mod tables_read;
17
18use crate::path::Path;
19use crate::{
20 AbstractTree, Checksum, KvPair, SeqNo, SequenceNumberCounter, TableId, UserKey, UserValue,
21 ValueType,
22 compaction::{CompactionStrategy, state::CompactionState},
23 config::Config,
24 format_version::FormatVersion,
25 fs::Fs,
26 iter_guard::{IterGuard, IterGuardImpl},
27 key::InternalKey,
28 manifest::Manifest,
29 memtable::Memtable,
30 range_tombstone::RangeTombstone,
31 scan_since::ScanSinceEvent,
32 slice::Slice,
33 table::{Table, probe_stats::PlanCounts},
34 value::InternalValue,
35 version::{SuperVersion, SuperVersions, Version, recovery::recover},
36 vlog::BlobFile,
37};
38use alloc::sync::Arc;
39#[cfg(not(feature = "std"))]
40use alloc::{boxed::Box, string::ToString, vec::Vec};
41use core::ops::{Bound, RangeBounds};
42use inner::{FlushGuard, TreeId, TreeInner, VersionsWriteGuard};
43// no-std: spin mirrors parking_lot's Mutex/RwLock API without an allocator.
44// parking_lot wins on the std hot path, so keep it for std.
45#[cfg(feature = "std")]
46use parking_lot::{Mutex, RwLock};
47#[cfg(not(feature = "std"))]
48use spin::{Mutex, RwLock};
49
50#[cfg(feature = "metrics")]
51use crate::metrics::Metrics;
52
53/// Floor for the storage-admission reserved headroom band (see
54/// [`Tree::compute_write_admission`]). Even with an empty active memtable the
55/// gate keeps at least this much room below the budget so the next writes and a
56/// space-reclaiming compaction have somewhere to land. 1 MiB.
57pub(crate) const MIN_RESERVED_HEADROOM: u64 = 1024 * 1024;
58
59/// How long a cached disk-free sample stays valid before the admission gate
60/// re-probes. Bounds how stale the physical free-space figure can be when the
61/// filesystem fills from another process between flushes, without issuing a
62/// `statfs`/`statvfs` syscall on every gated write. 1 second.
63const ADMISSION_DISK_FREE_TTL: core::time::Duration = core::time::Duration::from_secs(1);
64
65/// Iterator value guard
66pub struct Guard(crate::Result<(UserKey, UserValue)>);
67
68impl IterGuard for Guard {
69 fn into_inner_if(
70 self,
71 pred: impl Fn(&UserKey) -> bool,
72 ) -> crate::Result<(UserKey, Option<UserValue>)> {
73 let (k, v) = self.0?;
74
75 if pred(&k) {
76 Ok((k, Some(v)))
77 } else {
78 Ok((k, None))
79 }
80 }
81
82 fn key(self) -> crate::Result<UserKey> {
83 self.0.map(|(k, _)| k)
84 }
85
86 fn size(self) -> crate::Result<u32> {
87 #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
88 self.into_inner().map(|(_, v)| v.len() as u32)
89 }
90
91 fn into_inner(self) -> crate::Result<(UserKey, UserValue)> {
92 self.0
93 }
94}
95
96/// Trait for monomorphized table point-read results.
97///
98/// Allows `find_in_tables` to operate generically over `InternalValue` (for
99/// `get`) and `(InternalValue, Block)` (for `get_pinned`), generating optimal
100/// code for each path without runtime dispatch or extra refcount overhead.
101trait TablePointLookup: Sized {
102 fn lookup(
103 table: &Table,
104 key: &[u8],
105 seqno: SeqNo,
106 key_hash: u64,
107 ) -> crate::Result<Option<Self>>;
108 fn entry_seqno(&self) -> SeqNo;
109 fn filter_tombstone(self) -> Option<Self>;
110}
111
112/// Lookup result for standard `get()` — entry only, no block retained.
113type TableEntry = InternalValue;
114
115/// One covered key in a batched run resolution: `(input index, key hash,
116/// resolved item)`. Aliased to keep `resolve_run_batched`'s return readable.
117type CoveredKey = (usize, u64, Option<InternalValue>);
118
119/// `(miss_keys, duplicates)` from [`Tree::dedup_sorted_miss_keys`]: `miss_keys`
120/// is `(key_index, bloom_hash)` for the strictly-sorted-unique batched resolver,
121/// `duplicates` is `(duplicate_index, representative_index)` for the fan-out.
122type DedupedMissKeys = (Vec<(usize, u64)>, Vec<(usize, usize)>);
123
124/// The outcome of resolving a key batch against one run (see `resolve_run_batched`).
125struct RunResolve {
126 /// Covered, non-skipped keys with their resolved item, in input order.
127 covered: Vec<CoveredKey>,
128 /// Keys this run does not cover, in input order, for the next run or level.
129 not_covered: Vec<(usize, u64)>,
130}
131
132/// One data block the chunked `multi_get` resolver will read (see
133/// `resolve_level_chunked`): the block, the SST it lives in, the table-local
134/// read seqno, whether it needs the special load path (Page-ECC / columnar),
135/// and the ORIGINAL key indices that fall in this block, each with its key's
136/// hash. The table's file is opened when the block's chunk is read, so a
137/// level holds open only the files of the chunk it is reading.
138struct BlockTask<'a> {
139 table: &'a crate::Table,
140 /// The table's place in the version, by which a job names it.
141 at: read_job::TableAt,
142 handle: crate::table::BlockHandle,
143 table_seqno: SeqNo,
144 special: bool,
145 keys: Vec<(usize, u64)>,
146}
147
148/// A level's block tasks, and per table the filter probes planning them took,
149/// not yet counted.
150type LevelTasks<'a> = (Vec<BlockTask<'a>>, Vec<(&'a Table, PlanCounts)>);
151
152/// One table of a level being planned for a key batch: the keys it covers,
153/// and how it is read.
154struct LevelTable<'a> {
155 table: &'a Table,
156 /// The table's place in the version, by which a job names it.
157 at: read_job::TableAt,
158 /// Its keys: a span of the level's batch, which every run of the level
159 /// walks in the same order, so no table holds a copy of its keys.
160 keys: core::ops::Range<usize>,
161 read: LevelRead<'a>,
162 /// The file the first stage opened, held for every later stage: on a
163 /// level wider than the descriptor cache, the cache has evicted it by
164 /// then, and opening it again is a cold open per stage. It is let go once
165 /// the table's stages are over.
166 file: Option<Arc<dyn crate::fs::FsFile>>,
167}
168
169/// How a table of a level is read for its keys.
170enum LevelRead<'a> {
171 /// Not begun: nothing past its span of keys is built until it has a
172 /// place to read in, so a level wider than the descriptor cache holds the
173 /// key positions of no more tables than it holds files.
174 Pending,
175 /// Read in stages.
176 Staged(crate::table::staged::StagedRead<'a>),
177 /// To be planned serially: a Page-ECC table, whose every block needs the
178 /// load path's recovery, or a table a stage of which failed.
179 Serial,
180 /// Planned serially: its read seqno, the blocks it reads with the
181 /// positions of their keys in its span, and the filter probes planning
182 /// took.
183 Planned(
184 SeqNo,
185 Vec<(crate::table::BlockHandle, Vec<usize>)>,
186 PlanCounts,
187 ),
188 /// No key of the batch reads anything in it.
189 Nothing,
190}
191
192impl LevelRead<'_> {
193 /// Whether the table needs no more stages: planned, or left to the
194 /// serial planner, or with nothing to read.
195 fn is_over(&self) -> bool {
196 match self {
197 Self::Pending => false,
198 Self::Staged(read) => read.is_done(),
199 Self::Serial | Self::Planned(..) | Self::Nothing => true,
200 }
201 }
202}
203
204/// A data block task's block before its chunk is read.
205enum TaskBlock {
206 /// At hand: taken from the cache.
207 Held(crate::table::DataBlock),
208 /// To be read in the chunk's batch.
209 Read,
210 /// To be loaded when its chunk is read, through a Page-ECC or columnar
211 /// table's own path, which heals or reconstructs it; held only while its
212 /// keys are read.
213 Load,
214}
215
216/// The read queue of one backend a level's tables were opened through.
217struct LevelQueue<'a> {
218 fs: &'a Arc<dyn crate::fs::Fs>,
219 queue: Box<dyn crate::fs::ReadQueue + 'a>,
220 /// Whether the queue wakes the level's [`LevelWake`] as its reads come
221 /// back, so the level can sleep on all its queues at once.
222 wakes: bool,
223}
224
225/// Where the blocking driver takes a read's asks: a block read straight into
226/// the queue of its backend, so no list stands between the read and the
227/// queue; a job until the pass that asked for it is over, since running it
228/// hands its result back to the read.
229struct DriveSink<'a> {
230 /// One queue per backend, opened when a table first asks it for a block,
231 /// so a read answered from the cache opens none.
232 queues: Vec<LevelQueue<'a>>,
233 #[cfg(feature = "std")]
234 wake: Arc<LevelWake>,
235 jobs: Vec<read_job::Job>,
236 /// The cache of the first table a block was read for, which the waits
237 /// on the queues are timed into for the estimate that decides whether
238 /// misses share loads.
239 cache: Option<&'a crate::cache::Cache>,
240}
241
242impl<'a> read_job::ReadSink<'a> for DriveSink<'a> {
243 fn job(&mut self, job: read_job::Job) {
244 self.jobs.push(job);
245 }
246
247 /// The blocking driver completes every read itself, on the calling
248 /// thread, and runs its jobs there too.
249 fn leads_loads(&self) -> bool {
250 crate::inflight::batches_may_lead()
251 }
252
253 #[expect(clippy::indexing_slicing, reason = "a slot is a position in `queues`")]
254 fn read(&mut self, read: read_job::BlockRequest<'a>) {
255 if self.cache.is_none() {
256 self.cache = Some(read.cache);
257 }
258 let slot = if let Some(slot) = self.queues.iter().position(|q| Arc::ptr_eq(q.fs, read.fs)) {
259 slot
260 } else {
261 #[cfg_attr(not(feature = "std"), expect(unused_mut))]
262 let mut queue = read.fs.read_queue();
263 #[cfg(feature = "std")]
264 let wakes = queue.set_wake(Arc::clone(&self.wake) as Arc<dyn crate::fs::ReadWake>);
265 #[cfg(not(feature = "std"))]
266 let wakes = false;
267 self.queues.push(LevelQueue {
268 fs: read.fs,
269 queue,
270 wakes,
271 });
272 self.queues.len() - 1
273 };
274 self.queues[slot].queue.submit(crate::fs::QueuedRead {
275 tag: read.tag,
276 file: read.file,
277 offset: read.offset,
278 buf: read.buf,
279 });
280 }
281}
282
283/// The filter and index bytes each table of a staged level read holds, and
284/// their sum: the buffers of its blocks in flight, at the size they are read
285/// as, and the blocks its read holds, at their decoded size.
286struct MetaHeld {
287 in_flight: Vec<u64>,
288 held: Vec<u64>,
289 total: u64,
290}
291
292impl MetaHeld {
293 fn new(tables: usize) -> Self {
294 Self {
295 in_flight: vec![0; tables],
296 held: vec![0; tables],
297 total: 0,
298 }
299 }
300
301 /// Counts the table at `at` anew, its read holding `read_held` bytes.
302 #[expect(
303 clippy::indexing_slicing,
304 reason = "`at` indexes the level's tables, which both vectors are sized to"
305 )]
306 fn settle(&mut self, at: usize, read_held: u64) {
307 let now = self.in_flight[at] + read_held;
308 // `total` sums `held`, so it is at least the share replaced here.
309 self.total = self.total - self.held[at] + now;
310 self.held[at] = now;
311 }
312}
313
314/// Woken by any queue of a level when one of its reads is back: each wake
315/// moves the generation on, so a wake landing between the driver's look at
316/// the queues and its park is not lost, and unparks the driving thread only
317/// while it sleeps on the wake. A driver waiting inside one queue is parked
318/// by that queue's own channel, which an unpark of ours would only wake
319/// spuriously, a context switch per read for nothing.
320// no-std: without a thread to park, the level waits on one queue at a time.
321#[cfg(feature = "std")]
322struct LevelWake {
323 generation: core::sync::atomic::AtomicU64,
324 /// Whether the driver sleeps on this wake. Set before it looks at the
325 /// generation and read after a wake moves it, both sequentially
326 /// consistent: either the driver sees the new generation and does not
327 /// park, or the wake sees it asleep and unparks it.
328 sleeping: core::sync::atomic::AtomicBool,
329 thread: std::thread::Thread,
330}
331
332#[cfg(feature = "std")]
333impl crate::fs::ReadWake for LevelWake {
334 fn wake(&self) {
335 self.generation
336 .fetch_add(1, core::sync::atomic::Ordering::SeqCst);
337 if self.sleeping.load(core::sync::atomic::Ordering::SeqCst) {
338 self.thread.unpark();
339 }
340 }
341}
342
343impl TablePointLookup for TableEntry {
344 fn lookup(
345 table: &Table,
346 key: &[u8],
347 seqno: SeqNo,
348 key_hash: u64,
349 ) -> crate::Result<Option<Self>> {
350 table.get(key, seqno, key_hash)
351 }
352
353 fn entry_seqno(&self) -> SeqNo {
354 self.key.seqno
355 }
356
357 fn filter_tombstone(self) -> Option<Self> {
358 ignore_tombstone_value(self)
359 }
360}
361
362/// Lookup result for `get_pinned()` — entry + block for zero-copy pinning.
363type TableEntryWithBlock = (InternalValue, crate::table::Block);
364
365impl TablePointLookup for TableEntryWithBlock {
366 fn lookup(
367 table: &Table,
368 key: &[u8],
369 seqno: SeqNo,
370 key_hash: u64,
371 ) -> crate::Result<Option<Self>> {
372 table.get_with_block(key, seqno, key_hash)
373 }
374
375 fn entry_seqno(&self) -> SeqNo {
376 self.0.key.seqno
377 }
378
379 fn filter_tombstone(self) -> Option<Self> {
380 ignore_tombstone_value(self.0).map(|iv| (iv, self.1))
381 }
382}
383
384/// Lookup result for the value-returning `get()` path: `(value_type, seqno,
385/// value)`, no key reconstruction (the caller has the needle).
386type TableValue = (ValueType, SeqNo, crate::Slice);
387
388impl TablePointLookup for TableValue {
389 fn lookup(
390 table: &Table,
391 key: &[u8],
392 seqno: SeqNo,
393 key_hash: u64,
394 ) -> crate::Result<Option<Self>> {
395 table.get_value(key, seqno, key_hash)
396 }
397
398 fn entry_seqno(&self) -> SeqNo {
399 self.1
400 }
401
402 fn filter_tombstone(self) -> Option<Self> {
403 if self.0.is_tombstone() {
404 None
405 } else {
406 Some(self)
407 }
408 }
409}
410
411fn ignore_tombstone_value(item: InternalValue) -> Option<InternalValue> {
412 if item.is_tombstone() {
413 None
414 } else {
415 Some(item)
416 }
417}
418
419/// A log-structured merge tree (LSM-tree/LSMT)
420///
421/// Clones share one tree. Background work of the tree (the table file pool,
422/// the file deleter) may still touch its directory after a handle drops; to
423/// remove the directory, [`close`](Self::close) the tree and wait for the
424/// returned [`TreeClose`](crate::TreeClose): once it fires, the tree writes
425/// nothing into its directory any more.
426#[derive(Clone)]
427pub struct Tree(#[doc(hidden)] pub Arc<TreeInner>);
428
429impl core::ops::Deref for Tree {
430 type Target = TreeInner;
431
432 fn deref(&self) -> &Self::Target {
433 &self.0
434 }
435}
436
437impl crate::abstract_tree::sealed::Sealed for Tree {}
438
439impl Tree {
440 /// A handle that does not keep the tree open; see [`WeakTree`](crate::WeakTree).
441 #[must_use]
442 pub fn downgrade(&self) -> crate::WeakTree {
443 crate::WeakTree::of(self, None)
444 }
445
446 /// Gives up this handle and returns the signal of the tree being closed.
447 ///
448 /// The tree is closed once nothing of it owns a file of its directory or
449 /// works in it any more: other strong handles, iterators, snapshots and
450 /// pins keep it open until they drop, and the table file pool and the
451 /// background deleter finish what they started. Once closed, the tree
452 /// writes nothing into its directory and removing the directory is safe.
453 ///
454 /// From this call on no [`WeakTree`](crate::WeakTree) raises the tree
455 /// again; strong handles already held keep working until dropped.
456 ///
457 /// # Examples
458 ///
459 /// ```
460 /// # let folder = tempfile::tempdir()?;
461 /// use lsm_tree::{AbstractTree, AnyTree};
462 ///
463 /// let AnyTree::Standard(tree) = lsm_tree::Config::new(
464 /// folder.path(),
465 /// lsm_tree::SequenceNumberCounter::default(),
466 /// lsm_tree::SequenceNumberCounter::default(),
467 /// )
468 /// .open()?
469 /// else {
470 /// unreachable!("no key-value separation configured");
471 /// };
472 /// let monitor = tree.downgrade();
473 ///
474 /// let closed = tree.close();
475 /// assert!(monitor.upgrade().is_none());
476 /// closed.wait();
477 /// std::fs::remove_dir_all(folder.path())?;
478 /// # Ok::<(), Box<dyn std::error::Error>>(())
479 /// ```
480 pub fn close(self) -> crate::TreeClose {
481 crate::close::close(self)
482 }
483}
484
485/// Maps a raw merge-pipeline item into a standard-tree iterator guard.
486fn standard_guard(item: crate::Result<InternalValue>) -> IterGuardImpl {
487 IterGuardImpl::Standard(Guard(item.map(|iv| (iv.key.user_key, iv.value))))
488}
489
490/// A guard carrying only an error: the single item an iterator surface yields
491/// when it fails before it can open a snapshot (no row, no version to bind a
492/// blob guard to), so the failure reaches the consumer through the same
493/// `Result` it already handles per row.
494pub(crate) fn error_guard(e: crate::Error) -> IterGuardImpl {
495 IterGuardImpl::Standard(Guard(Err(e)))
496}
497
498/// The bytes an install keeps, for each counter set they go to: the set of
499/// the backend each file was written through (see
500/// [`Fs::write_io_stats`]), so a route on
501/// another medium is charged to that medium's set. Files on a backend that
502/// counts nothing are not looked at.
503///
504/// Read before the install publishes the files, recorded once it has
505/// ([`Self::charge`]): an install that fails keeps nothing.
506#[derive(Default)]
507pub(crate) struct InstalledCharge(Vec<(Arc<crate::fs::WriteIoStats>, crate::fs::WriteOrigin, u64)>);
508
509impl InstalledCharge {
510 /// Adds the files of `tables`, written for `table_origin`, and of
511 /// `blob_files`, written for `blob_origin`.
512 ///
513 /// # Errors
514 ///
515 /// A counted blob file whose length cannot be read.
516 pub(crate) fn add(
517 &mut self,
518 tables: &[Table],
519 blob_files: &[crate::BlobFile],
520 table_origin: crate::fs::WriteOrigin,
521 blob_origin: crate::fs::WriteOrigin,
522 ) -> crate::Result<()> {
523 for table in tables {
524 if let Some(stats) = table.fs.write_io_stats() {
525 self.push(stats, table_origin, table.file_len());
526 }
527 }
528 for blob_file in blob_files {
529 if let Some(stats) = blob_file.0.fs.write_io_stats() {
530 self.push(stats, blob_origin, blob_file.physical_size()?);
531 }
532 }
533 Ok(())
534 }
535
536 fn push(
537 &mut self,
538 stats: &Arc<crate::fs::WriteIoStats>,
539 origin: crate::fs::WriteOrigin,
540 bytes: u64,
541 ) {
542 for (set, slot, sum) in &mut self.0 {
543 if *slot == origin && Arc::ptr_eq(set, stats) {
544 *sum += bytes;
545 return;
546 }
547 }
548 self.0.push((Arc::clone(stats), origin, bytes));
549 }
550
551 /// Records the bytes, once the install that keeps them is published.
552 pub(crate) fn charge(self) {
553 for (stats, origin, bytes) in self.0 {
554 stats.installed(origin, bytes);
555 }
556 }
557}
558
559/// The highest seqno the memtables of `snapshot` hold, active and sealed.
560fn highest_memtable_seqno(snapshot: &SuperVersion) -> Option<SeqNo> {
561 let sealed = snapshot
562 .sealed_memtables
563 .iter()
564 .map(|mt| mt.get_highest_seqno())
565 .max()
566 .flatten();
567 snapshot.active_memtable.get_highest_seqno().max(sealed)
568}
569
570/// Extract owned user-key bounds from any range.
571pub(crate) fn range_to_user_bounds<K: AsRef<[u8]>, R: RangeBounds<K>>(
572 range: &R,
573) -> (Bound<UserKey>, Bound<UserKey>) {
574 use core::ops::Bound::{Excluded, Included, Unbounded};
575 let lo = match range.start_bound() {
576 Included(x) => Included(x.as_ref().into()),
577 Excluded(x) => Excluded(x.as_ref().into()),
578 Unbounded => Unbounded,
579 };
580 let hi = match range.end_bound() {
581 Included(x) => Included(x.as_ref().into()),
582 Excluded(x) => Excluded(x.as_ref().into()),
583 Unbounded => Unbounded,
584 };
585 (lo, hi)
586}
587
588/// Wraps a [`SeekableTreeIter`](crate::range::SeekableTreeIter) so a standard
589/// tree can expose it as a [`SeekableGuardIter`](crate::iter_guard::SeekableGuardIter).
590struct StandardSeekable {
591 inner: crate::range::SeekableTreeIter,
592}
593
594impl Iterator for StandardSeekable {
595 type Item = IterGuardImpl;
596
597 fn next(&mut self) -> Option<Self::Item> {
598 self.inner.next().map(standard_guard)
599 }
600}
601
602impl DoubleEndedIterator for StandardSeekable {
603 fn next_back(&mut self) -> Option<Self::Item> {
604 self.inner.next_back().map(standard_guard)
605 }
606}
607
608impl crate::iter_guard::SeekableGuardIter for StandardSeekable {
609 fn seek_to(&mut self, key: &[u8]) {
610 self.inner.seek_to(key);
611 }
612
613 fn seek_to_for_prev(&mut self, key: &[u8]) {
614 self.inner.seek_to_for_prev(key);
615 }
616
617 fn peek_key(&mut self) -> Option<crate::Result<crate::UserKey>> {
618 self.inner.peek_key()
619 }
620}
621
622impl AbstractTree for Tree {
623 fn table_file_cache_size(&self) -> usize {
624 self.config
625 .descriptor_table
626 .as_ref()
627 .map_or(0, |dt| dt.len())
628 }
629
630 fn get_version_history_lock(&self) -> VersionsWriteGuard<'_> {
631 self.version_history.write()
632 }
633
634 fn persist_flushes(&self) -> crate::Result<()> {
635 self.0.persist_flushes()
636 }
637
638 fn next_table_id(&self) -> TableId {
639 self.0.table_id_counter.get()
640 }
641
642 fn id(&self) -> TreeId {
643 self.id
644 }
645
646 fn blob_file_count(&self) -> usize {
647 0
648 }
649
650 #[cfg(feature = "std")]
651 fn create_checkpoint(
652 &self,
653 target_path: &crate::path::Path,
654 ) -> crate::Result<crate::CheckpointInfo> {
655 crate::checkpoint::run_checkpoint(
656 self,
657 &crate::checkpoint::CheckpointParams {
658 target_root: target_path,
659 target_fs: &self.config.fs,
660 src_root: &self.config.path,
661 src_fs: &self.config.fs,
662 deletion_pause: &self.deletion_pause,
663 visible_seqno: &self.config.visible_seqno,
664 include_blobs: false,
665 runtime_config: self.0.runtime_config.load_full(),
666 encryption: self.0.config.encryption.clone(),
667 source: &self.0,
668 },
669 )
670 }
671
672 #[cfg(feature = "std")]
673 fn pin(&self) -> crate::PinnedVersion {
674 crate::PinnedVersion::take(self)
675 }
676
677 #[cfg(feature = "std")]
678 fn acknowledge_repair_report(&self, id: u128) -> crate::Result<bool> {
679 Self::acknowledge_repair_report(self, id)
680 }
681
682 fn print_trace(&self, key: &[u8]) -> crate::Result<()> {
683 let super_version = self.version_history.read().latest_version();
684
685 let key = Slice::from(key);
686
687 for kv in super_version.active_memtable.range_internal((
688 Bound::Included(InternalKey::bound(
689 key.clone(),
690 SeqNo::MAX,
691 ValueType::Value,
692 )),
693 Bound::Unbounded,
694 )) {
695 log::info!("[Active] {kv:?}");
696 }
697
698 for mt in super_version.sealed_memtables.iter().rev() {
699 for kv in mt.range_internal((
700 Bound::Included(InternalKey::bound(
701 key.clone(),
702 SeqNo::MAX,
703 ValueType::Value,
704 )),
705 Bound::Unbounded,
706 )) {
707 log::info!("[Sealed #{}] {kv:?}", mt.id());
708 }
709 }
710
711 for table in super_version
712 .version
713 .iter_levels()
714 .flat_map(|lvl| lvl.iter())
715 .filter_map(|run| run.get_for_key_cmp(&key, self.config.comparator.as_ref()))
716 {
717 for kv in table.range(..) {
718 let kv = kv?;
719
720 if kv.key.user_key != key {
721 break;
722 }
723
724 log::info!("[Table #{}] {kv:?}", table.id());
725 }
726 }
727
728 Ok(())
729 }
730
731 fn get_internal_entry(&self, key: &[u8], seqno: SeqNo) -> crate::Result<Option<InternalValue>> {
732 let super_version = self.snapshot_for_read(seqno)?;
733 // Counted where a caller asks, not in the lookup, which a read that
734 // counts itself makes as well.
735 super_version.version.read_mix().note_points(1);
736
737 Self::get_internal_entry_from_version(
738 &super_version,
739 key,
740 seqno,
741 self.config.comparator.as_ref(),
742 )
743 }
744
745 fn current_version(&self) -> Version {
746 self.version_history
747 .read()
748 .latest_version_ref()
749 .version
750 .clone()
751 }
752
753 #[cfg(feature = "std")]
754 fn refresh_table_checksum(
755 &self,
756 table_id: TableId,
757 checksum: crate::checksum::Checksum,
758 expected_restriction: Option<&crate::UserKey>,
759 ) -> crate::Result<crate::abstract_tree::ChecksumRefreshOutcome> {
760 use crate::abstract_tree::ChecksumRefreshOutcome;
761 // Same lock order as flush / compaction version installs: compaction
762 // state first, then the version history write lock. But the caller (the
763 // patrol reconcile) holds this table's HEAL LOCK across this call, and a
764 // concurrent tight-space compaction acquires that heal lock WHILE holding
765 // `compaction_state`. Blocking on `compaction_state` here would invert the
766 // order (heal_lock -> compaction_state on this path vs
767 // compaction_state -> heal_lock on the compaction path) and deadlock
768 // permanently. `try_lock` instead: a failed acquire means a compaction is
769 // mid-install, so skip this refresh — but report the skip as CONTENDED,
770 // not as a benign no-op: the healed bytes are durable while the manifest
771 // digest stays stale, so a "clean" report would mislead a later
772 // integrity check / checkpoint. The caller keeps the attestation and
773 // surfaces a finding; the next patrol retries once the compaction
774 // releases the state.
775 let Some(mut _compaction_state) = self.compaction_state.try_lock() else {
776 return Ok(ChecksumRefreshOutcome::Contended);
777 };
778 let mut version_lock = self.version_history.write();
779
780 // Under the install lock, resolve the CURRENT view of this table and
781 // reject the refresh if either it is gone (compacted away — nothing to
782 // refresh) or its restriction no longer matches the one `checksum` was
783 // computed for. A tight-space compaction can swap the captured view for a
784 // restricted same-id view (punching its prefix) between the caller's read
785 // and this lock; installing the caller's digest against a different
786 // restriction would record one the punched file can never match. Skipping
787 // leaves the current view's own (compaction-installed) digest in place for
788 // the next patrol to reconcile.
789 let restriction_matches = version_lock
790 .latest_version_ref()
791 .version
792 .iter_tables()
793 .find(|t| t.id() == table_id)
794 .is_some_and(|t| t.restrict_lower_bound() == expected_restriction);
795 if !restriction_matches {
796 // No-op: the manifest digest is unchanged, so the caller must keep the
797 // attestation for the next patrol.
798 return Ok(ChecksumRefreshOutcome::Stale);
799 }
800
801 version_lock
802 .upgrade_version(
803 &self.config.path,
804 |current| {
805 let mut copy = current.clone();
806 if let Some(next) = copy
807 .version
808 .with_refreshed_table_checksum(table_id, checksum)
809 {
810 copy.version = next;
811 }
812 Ok(copy)
813 },
814 &self.config.seqno,
815 &self.config.visible_seqno,
816 &*self.config.fs,
817 self.0.runtime_config.load_full(),
818 self.0.config.encryption.clone(),
819 crate::version::RetentionEffect::Keep,
820 )
821 .map(|()| ChecksumRefreshOutcome::Refreshed)
822 }
823
824 fn sync_mode(&self) -> crate::fs::SyncMode {
825 self.config.sync_mode
826 }
827
828 fn wait_for_table_file_refills(&self) {
829 self.table_files.wait_for_refills();
830 }
831
832 fn prefix_extractor(&self) -> Option<alloc::sync::Arc<dyn crate::prefix::PrefixExtractor>> {
833 self.config.prefix_extractor.clone()
834 }
835
836 fn storage_stats(&self) -> crate::Result<crate::StorageStats> {
837 // One version snapshot reused for the footprint and the full-compaction
838 // estimate below: a second `current_version()` could race a concurrent
839 // flush / compaction and mix two snapshots.
840 let version = self.current_version();
841 // Standard tree: SST values ARE user values (no KV separation).
842 let mut stats =
843 crate::storage_stats::compute_storage_stats(&version, self.is_compacting(), true)?;
844 // Fill the disk-aware capacity figures (quota + free-space probe) the
845 // version-only computation can't know.
846 let (capacity, available, compaction_possible) = self.admission_capacity(stats.used_bytes);
847 stats.capacity_bytes = capacity;
848 stats.available_bytes = available;
849 stats.compaction_possible = compaction_possible;
850 // When admission gating is active and a compaction is not already
851 // running, surface whether a full compaction has working room through the
852 // SAME two-layer check the compaction space gate enforces (logical quota +
853 // physical free per destination volume), so the reported status matches
854 // what the gate will admit. With gating off the gate never runs, so the
855 // status stays `Healthy` even though the backend can report a finite
856 // capacity.
857 if self.storage_admission_enabled()
858 && capacity.is_some()
859 && stats.status == crate::StorageStatus::Healthy
860 {
861 // A full compaction's transient output is bounded by the largest
862 // level's on-disk size, but it LANDS in the last configured level's
863 // volume (`level_count - 1`), which under tiered routing can be a
864 // different filesystem than the largest level. A standard tree has no
865 // blob relocation. Using the per-volume gate (not `available >=
866 // full_compaction_bytes` against the min-volume free) keeps the status
867 // from reporting tight when a routed merge would actually be admitted.
868 let sst_need = crate::storage_stats::full_compaction_demand_bytes(&version)?;
869 // `saturating_sub`: `level_count >= 1` always, so this is the last
870 // level index; the clamp only guards a degenerate zero-level config.
871 let sst_dest_level = self.0.config.level_count.saturating_sub(1);
872 let quota_headroom = self.quota_headroom(stats.used_bytes);
873 let full_fits = crate::compaction::worker::merge_would_fit(
874 &self.0.compaction_capacity,
875 self.0.id,
876 &self.0.config,
877 quota_headroom,
878 (sst_need, 0),
879 sst_dest_level,
880 );
881 stats.status = if full_fits {
882 crate::StorageStatus::FullCompactionAvailable
883 } else {
884 crate::StorageStatus::TightCompactionAvailable
885 };
886 }
887 // A closed admission gate is the operator-actionable state, so it takes
888 // precedence over the others (a read-only tree may well be compacting to
889 // reclaim space).
890 if self.is_read_only() {
891 stats.status = crate::StorageStatus::ReadOnlyOutOfSpace;
892 }
893 Ok(stats)
894 }
895
896 fn write_admission(&self) -> crate::Result<()> {
897 self.compute_write_admission()
898 }
899
900 fn write_backpressure(
901 &self,
902 strategy: &dyn crate::compaction::CompactionStrategy,
903 ) -> crate::Backpressure {
904 // Copy the thresholds out (BackpressureThresholds is Copy) so the
905 // arc-swap guard drops immediately; the off check short-circuits before
906 // touching the version, keeping the disabled path free.
907 let thresholds = self.0.runtime_config.load().backpressure;
908 if thresholds.is_off() {
909 return crate::Backpressure::None;
910 }
911 let version = self.current_version();
912 let signals = crate::BackpressureSignals {
913 // L0's run count is what a read pays there and what the leveled
914 // trigger counts.
915 l0_runs: version.l0().run_count(),
916 pending_compaction_bytes: strategy.pending_compaction_bytes(&version),
917 // Summed over a shared budget's members, so read only when an
918 // axis compares it.
919 unflushed_memtable_bytes: if thresholds.memtable_slowdown.is_some()
920 || thresholds.memtable_stop.is_some()
921 {
922 self.unflushed_memtable_bytes()
923 } else {
924 0
925 },
926 };
927 crate::Backpressure::compute(&signals, &thresholds)
928 }
929
930 fn get_flush_lock(&self) -> FlushGuard<'_> {
931 self.flush_lock.lock()
932 }
933
934 #[cfg(feature = "metrics")]
935 fn metrics(&self) -> &Arc<crate::Metrics> {
936 &self.0.metrics
937 }
938
939 #[cfg(feature = "metrics")]
940 fn cache_stats(&self) -> crate::CacheStats {
941 let cache = &self.0.config.cache;
942 self.metrics().cache_stats(cache.size(), cache.capacity())
943 }
944
945 fn prefix<K: AsRef<[u8]>>(
946 &self,
947 prefix: K,
948 seqno: SeqNo,
949 index: Option<(Arc<Memtable>, SeqNo)>,
950 ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
951 match self.create_prefix(&prefix, seqno, index) {
952 Ok(iter) => Box::new(iter.map(|kv| IterGuardImpl::Standard(Guard(kv)))),
953 Err(e) => Box::new(core::iter::once(error_guard(e))),
954 }
955 }
956
957 fn range<K: AsRef<[u8]>, R: RangeBounds<K>>(
958 &self,
959 range: R,
960 seqno: SeqNo,
961 index: Option<(Arc<Memtable>, SeqNo)>,
962 ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
963 match self.create_range(&range, seqno, index) {
964 Ok(iter) => Box::new(iter.map(|kv| IterGuardImpl::Standard(Guard(kv)))),
965 Err(e) => Box::new(core::iter::once(error_guard(e))),
966 }
967 }
968
969 fn range_seekable<K: AsRef<[u8]>, R: RangeBounds<K>>(
970 &self,
971 range: R,
972 seqno: SeqNo,
973 index: Option<(Arc<Memtable>, SeqNo)>,
974 ) -> Box<dyn crate::iter_guard::SeekableGuardIter + 'static> {
975 let (lo, hi) = range_to_user_bounds(&range);
976 match self.create_seekable_range_bounds(lo, hi, seqno, index) {
977 Ok(inner) => Box::new(StandardSeekable { inner }),
978 Err(e) => Box::new(crate::iter_guard::FailedSeekable::new(e)),
979 }
980 }
981
982 fn batch_range_scan<K: AsRef<[u8]>, R: RangeBounds<K> + 'static, I: IntoIterator<Item = R>>(
983 &self,
984 intervals: I,
985 seqno: SeqNo,
986 index: Option<(Arc<Memtable>, SeqNo)>,
987 ) -> Box<dyn Iterator<Item = IterGuardImpl> + Send + 'static>
988 where
989 I::IntoIter: Send + 'static,
990 {
991 // Open the seekable iterator over the whole keyspace once; each interval
992 // is served by repositioning it (single per-SST setup, amortized).
993 let inner = match self.create_seekable_range_bounds(
994 Bound::Unbounded,
995 Bound::Unbounded,
996 seqno,
997 index,
998 ) {
999 Ok(inner) => inner,
1000 Err(e) => return Box::new(core::iter::once(error_guard(e))),
1001 };
1002 let intervals = intervals.into_iter().map(|r| range_to_user_bounds(&r));
1003 Box::new(crate::range::BatchRangeScan::new(inner, intervals).map(standard_guard))
1004 }
1005
1006 /// Returns the number of tombstones in the tree.
1007 fn tombstone_count(&self) -> u64 {
1008 self.current_version()
1009 .iter_tables()
1010 .map(Table::tombstone_count)
1011 .sum()
1012 }
1013
1014 /// Returns the number of weak tombstones (single deletes) in the tree.
1015 fn weak_tombstone_count(&self) -> u64 {
1016 self.current_version()
1017 .iter_tables()
1018 .map(Table::weak_tombstone_count)
1019 .sum()
1020 }
1021
1022 /// Returns the number of value entries that become reclaimable once weak tombstones can be GC'd.
1023 fn weak_tombstone_reclaimable_count(&self) -> u64 {
1024 self.current_version()
1025 .iter_tables()
1026 .map(Table::weak_tombstone_reclaimable)
1027 .sum()
1028 }
1029
1030 fn clear(&self) -> crate::Result<()> {
1031 let config = self.tree_config();
1032 let mut versions = self.get_version_history_lock();
1033
1034 // Pre-clear snapshot: every table + blob file it references becomes
1035 // garbage the moment the new empty version is installed.
1036 let prior = versions.latest_version();
1037
1038 #[cfg(zstd_any)]
1039 let still_written = self.write_referenced_dict_ids();
1040 #[cfg(not(zstd_any))]
1041 let still_written: Vec<crate::file::DictId> = Vec::new();
1042
1043 versions.upgrade_version(
1044 &config.path,
1045 |v| {
1046 let mut copy = v.clone();
1047 copy.active_memtable = Arc::new(Memtable::new(
1048 self.memtable_id_counter.next(),
1049 self.config.comparator.clone(),
1050 ));
1051 copy.sealed_memtables = Arc::default();
1052 copy.version = v.version.cleared(&still_written);
1053 Ok(copy)
1054 },
1055 &config.seqno,
1056 &config.visible_seqno,
1057 &*config.fs,
1058 self.0.runtime_config.load_full(),
1059 self.0.config.encryption.clone(),
1060 // Every table goes: no snapshot up to this install is servable.
1061 crate::version::RetentionEffect::DropsData,
1062 )?;
1063 // `prior` still holds the replaced version, so nothing reaches refcount
1064 // zero yet.
1065 drop(versions); // release the version-history lock before any fs work
1066
1067 // Mark every obsolete table / blob file deleted so the file is
1068 // reclaimed (Inner::Drop) once its last reference is released. A
1069 // concurrent reader still holding the pre-clear snapshot keeps its own
1070 // clone alive, deferring physical deletion until it finishes — the
1071 // version-history Arc refcount is the MVCC guard, so reclaim never
1072 // races a live read. Tables with no other live reference are reclaimed
1073 // as `prior` drops at the end of this call.
1074 for table in prior.version.iter_tables() {
1075 table.mark_as_deleted();
1076 }
1077 for blob_file in prior.version.blob_files.iter() {
1078 blob_file.mark_as_deleted();
1079 }
1080
1081 Ok(())
1082 }
1083
1084 #[doc(hidden)]
1085 fn major_compact(
1086 &self,
1087 target_size: u64,
1088 gc_watermark: SeqNo,
1089 ) -> crate::Result<crate::compaction::CompactionResult> {
1090 let strategy = Arc::new(crate::compaction::major::Strategy::new(target_size));
1091
1092 // IMPORTANT: Write lock so we can be the only compaction going on
1093 let _lock = self.0.major_compaction_lock.write();
1094
1095 log::info!("Starting major compaction");
1096 self.inner_compact(strategy, gc_watermark)
1097 }
1098
1099 fn l0_run_count(&self) -> usize {
1100 self.current_version()
1101 .level(0)
1102 .map(|x| x.run_count())
1103 .unwrap_or_default()
1104 }
1105
1106 fn size_of<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<u32>> {
1107 #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
1108 Ok(self.get(key, seqno)?.map(|x| x.len() as u32))
1109 }
1110
1111 fn filter_size(&self) -> u64 {
1112 self.current_version()
1113 .iter_tables()
1114 .map(Table::filter_size)
1115 .map(u64::from)
1116 .sum()
1117 }
1118
1119 fn pinned_filter_size(&self) -> usize {
1120 self.current_version()
1121 .iter_tables()
1122 .map(Table::pinned_filter_size)
1123 .sum()
1124 }
1125
1126 fn filter_memory(&self) -> crate::FilterMemory {
1127 let version = self.current_version();
1128 let serialised_bytes = version
1129 .iter_tables()
1130 .map(|table| u64::from(table.filter_size()))
1131 .sum();
1132 let budget_bytes = self
1133 .config
1134 .filter_advisor
1135 .as_ref()
1136 .map(crate::config::FilterAdvisor::budget_bytes);
1137 crate::FilterMemory {
1138 serialised_bytes,
1139 resident_bytes: version
1140 .iter_tables()
1141 .map(Table::resident_filter_bytes)
1142 .sum(),
1143 budget_bytes,
1144 over_budget: budget_bytes.is_some_and(|budget| serialised_bytes > budget),
1145 }
1146 }
1147
1148 fn pinned_block_index_size(&self) -> usize {
1149 self.current_version()
1150 .iter_tables()
1151 .map(Table::pinned_block_index_size)
1152 .sum()
1153 }
1154
1155 fn sealed_memtable_count(&self) -> usize {
1156 self.version_history
1157 .read()
1158 .latest_version()
1159 .sealed_memtables
1160 .len()
1161 }
1162
1163 fn flush_to_tables_with_rt(
1164 &self,
1165 stream: impl Iterator<Item = crate::Result<InternalValue>>,
1166 range_tombstones: Vec<crate::range_tombstone::RangeTombstone>,
1167 keys: u64,
1168 hashes: u64,
1169 // A standard tree separates no values, so it has nothing to place.
1170 _overwritten: Option<&crate::blob_tree::overwritten::OverwrittenKeys>,
1171 ) -> crate::Result<
1172 Option<(
1173 Vec<Table>,
1174 Option<Vec<BlobFile>>,
1175 crate::runtime_config::WritePin,
1176 )>,
1177 > {
1178 use crate::table::multi_writer::MultiWriter;
1179 use crate::time::Instant;
1180
1181 let start = Instant::now();
1182
1183 let (folder, level_fs) = self.config.tables_folder_for_level(0);
1184
1185 let data_block_size = self.config.data_block_size_policy.get(0);
1186
1187 let data_block_restart_interval = self.config.data_block_restart_interval_policy.get(0);
1188 let index_block_restart_interval = self.config.index_block_restart_interval_policy.get(0);
1189
1190 // One runtime-config snapshot for the whole flush writer setup. The
1191 // compression policies, the index spill threshold, `seqno_in_index`,
1192 // and the per-KV checksum policy are all live (toggleable via
1193 // `update_runtime_config`); reading `load_full()` per field could
1194 // straddle a concurrent update and mix two snapshots into one SST.
1195 // Compaction is the migration mechanism, so a toggle takes effect on
1196 // the next flush / compaction.
1197 let rc = self.0.runtime_config.load_full();
1198
1199 let data_block_compression = rc.data_block_compression_policy.get(0);
1200 let index_block_compression = rc.index_block_compression_policy.get(0);
1201
1202 let data_block_hash_ratio = self.config.data_block_hash_ratio_policy.get(0);
1203
1204 let index_partitioning = self.config.index_block_partitioning_policy.get(0);
1205 let filter_partitioning = self.config.filter_block_partitioning_policy.get(0);
1206
1207 log::debug!(
1208 "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:?}",
1209 folder.display(),
1210 );
1211
1212 let table_writer = MultiWriter::from_pool(
1213 folder.clone(),
1214 Arc::clone(&self.table_files),
1215 None,
1216 self.config.table_target_size,
1217 0,
1218 level_fs.clone(),
1219 crate::fs::WriteOrigin::FlushTable,
1220 )?;
1221 // The flush's L0 recency, taken once its first file is: flushes are
1222 // serialized, so it is above every table already flushed and below
1223 // every one flushed later. A pooled file's id was taken when its batch
1224 // was made and says nothing about when the table was written; a file
1225 // made now carries an id taken now, which serves.
1226 let recency = table_writer
1227 .first_made_now()
1228 .unwrap_or_else(|| self.table_id_counter.next());
1229 let mut table_writer = table_writer
1230 .set_comparator(self.config.comparator.clone())
1231 .use_recency(Some(recency))
1232 .use_data_block_restart_interval(data_block_restart_interval)
1233 .use_index_block_restart_interval(index_block_restart_interval)
1234 .use_data_block_compression(data_block_compression)
1235 .use_index_block_compression(index_block_compression)
1236 .use_data_block_size(data_block_size)
1237 .use_index_partition_size(self.config.index_block_partition_size_policy.get(0))
1238 .use_filter_partition_size(self.config.filter_block_partition_size_policy.get(0))
1239 .use_row_group_size(self.config.columnar_row_group_size_policy.get(0))
1240 .use_columnar_page_size(self.config.columnar_page_size_policy.get(0))
1241 .use_column_encoding(self.config.column_encoding_policy.get(0))
1242 .use_data_block_hash_ratio(data_block_hash_ratio);
1243
1244 let bloom_policy = {
1245 use crate::config::FilterPolicyEntry::{Bloom, None};
1246 use crate::table::filter::BloomConstructionPolicy;
1247
1248 match self.config.filter_policy.get(0) {
1249 Bloom(policy) => policy,
1250 None => BloomConstructionPolicy::BitsPerKey(0.0),
1251 }
1252 };
1253 table_writer = table_writer.use_bloom_policy(bloom_policy);
1254 let filter_sizing = self.new_data_filter_sizing(
1255 0,
1256 bloom_policy,
1257 crate::filter_budget::FilterCount { keys, hashes },
1258 rc.ecc_scheme,
1259 );
1260 table_writer = table_writer.use_filter_sizing(filter_sizing.clone());
1261
1262 if index_partitioning {
1263 // Size-adaptive: single-level index for small SSTs (where pinning
1264 // the whole index is cheap and a two-level lookup is pure overhead),
1265 // spilling to a partitioned index only once the index grows past the
1266 // threshold. Recovers the point-read cost of an unconditional
1267 // two-level index on small/medium SSTs.
1268 table_writer = table_writer.use_adaptive_index(rc.index_partition_spill_threshold);
1269 }
1270 if filter_partitioning {
1271 table_writer = table_writer.use_partitioned_filter();
1272 }
1273
1274 table_writer = table_writer.use_prefix_extractor(self.config.prefix_extractor.clone());
1275 table_writer = table_writer.use_key_domains(self.config.key_domains.clone());
1276 table_writer = table_writer.use_encryption(self.config.encryption.clone());
1277 table_writer = table_writer.use_clock(self.config.clock.clone());
1278 // ECC scheme from the live runtime snapshot (same as `seqno_in_index`
1279 // / `kv_checksums` below), so a flush after a scheme change writes the
1280 // SST with the current scheme rather than the startup one.
1281 table_writer = table_writer.use_page_ecc(self.config.page_ecc, rc.ecc_scheme);
1282 table_writer = table_writer
1283 .use_sync_mode(self.config.sync_mode)
1284 .use_durability(self.config.flush_durability)
1285 .use_writeback_bytes(self.config.writeback_bytes);
1286
1287 table_writer = table_writer.use_seqno_in_index(rc.seqno_in_index);
1288 table_writer = table_writer.use_zone_map(rc.zone_map);
1289 table_writer = table_writer.use_columnar(rc.columnar);
1290 table_writer = table_writer.use_disable_cow_on_sst(rc.disable_cow_on_sst_files);
1291 // `Off` (default) emits no per-KV footer and leaves the data-block
1292 // payload encoding unchanged (the V5 header carries a block_flags byte
1293 // and the meta block a descriptor key regardless, so the on-disk bytes
1294 // are not identical to a pre-V5 table).
1295 table_writer = table_writer.use_kv_checksums(rc.kv_checksums, rc.kv_checksum_algo);
1296 // Flush writes level 0; resolve that level's locator policy entry.
1297 table_writer = table_writer.use_locator(self.config.locator_policy.get(0));
1298
1299 // The dictionary THIS snapshot's policy names. The flush holds the
1300 // snapshot until its tables are installed: a collection spares what a
1301 // held snapshot names, however the policy has changed since.
1302 #[cfg(zstd_any)]
1303 {
1304 table_writer = table_writer
1305 .use_zstd_dictionary(
1306 self.config
1307 .current_zstd_dictionaries()
1308 .for_compression(data_block_compression)?,
1309 )
1310 .use_zstd_two_pass_seed(rc.zstd_two_pass_seed);
1311 }
1312 let write_pin = crate::runtime_config::WritePin::new(&rc).with_filter_sizing(filter_sizing);
1313
1314 // Parallel block compression for the flush writer, on the same pool the
1315 // compaction writers use. Engaged only when the per-block transform does
1316 // real CPU work (a codec, encryption, or page ECC): with the identity
1317 // transform the pipeline's owned buffer + queue hop per block buys
1318 // nothing over the serial reusable-buffer path. Safe wherever the host
1319 // runs this flush — even on a thread of an injected pool — because the
1320 // pipeline's help-first drain executes queued jobs inline instead of
1321 // waiting on a saturated pool (see `parallel_compressor`).
1322 #[cfg(feature = "std")]
1323 {
1324 let transform_does_work = data_block_compression != crate::CompressionType::None
1325 || self.config.encryption.is_some()
1326 || self.config.page_ecc;
1327 if transform_does_work {
1328 table_writer =
1329 table_writer.use_parallel_compression(self.config.parallel_compression());
1330 }
1331 }
1332
1333 // Set range tombstones BEFORE writing KV items so that if MultiWriter
1334 // rotates to a new table during the write loop, earlier tables already
1335 // carry the RT metadata.
1336 table_writer.set_range_tombstones(range_tombstones);
1337
1338 for item in stream {
1339 table_writer.write(item?)?;
1340 }
1341
1342 let written = table_writer.finish()?;
1343 let entries_durable = written.entries_durable;
1344
1345 log::debug!("Flushed memtable(s) in {:?}", start.elapsed());
1346
1347 let pin_filter = self.config.filter_block_pinning_policy.get(0);
1348 let pin_index = self.config.index_block_pinning_policy.get(0);
1349
1350 // Load tables
1351 let tables = written
1352 .tables
1353 .into_iter()
1354 .map(|(table_id, checksum)| -> crate::Result<Table> {
1355 let mut params = crate::table::RecoverParams::new(
1356 folder.join(table_id.to_string()),
1357 checksum,
1358 table_id,
1359 level_fs.clone(),
1360 self.config.comparator.clone(),
1361 self.config.cache.clone(),
1362 );
1363 params.tree_id = self.id;
1364 params
1365 .descriptor_table
1366 .clone_from(&self.config.descriptor_table);
1367 params.pin_filter = pin_filter;
1368 params.pin_index = pin_index;
1369 params.encryption.clone_from(&self.config.encryption);
1370 params
1371 .prefix_extractor
1372 .clone_from(&self.config.prefix_extractor);
1373 #[cfg(zstd_any)]
1374 {
1375 params.zstd_dictionaries = self.config.current_zstd_dictionaries();
1376 }
1377 #[cfg(feature = "metrics")]
1378 {
1379 params.metrics = self.metrics.clone();
1380 }
1381 let table = Table::recover(params)?;
1382 if entries_durable {
1383 table.mark_entry_durable();
1384 }
1385 Ok(table)
1386 })
1387 .collect::<crate::Result<Vec<_>>>()?;
1388
1389 // Return Some even when tables is empty (RT-only flush): the caller
1390 // (AbstractTree::flush) handles empty tables by re-inserting RTs into
1391 // the active memtable and still needs to delete sealed memtables.
1392 Ok(Some((tables, None, write_pin)))
1393 }
1394
1395 #[expect(clippy::significant_drop_tightening)]
1396 fn register_tables(
1397 &self,
1398 tables: &[Table],
1399 blob_files: Option<&[BlobFile]>,
1400 frag_map: Option<crate::blob_tree::FragmentationMap>,
1401 sealed_memtables_to_delete: &[crate::tree::inner::MemtableId],
1402 gc_watermark: SeqNo,
1403 collected_below_watermark: bool,
1404 ) -> crate::Result<()> {
1405 log::trace!(
1406 "Registering {} tables, {} blob files",
1407 tables.len(),
1408 blob_files.map(<[BlobFile]>::len).unwrap_or_default(),
1409 );
1410
1411 // Wire the tree-wide deletion pause into every fresh table / blob
1412 // file so an in-flight checkpoint defers their cleanup if they
1413 // later get marked `is_deleted` by compaction.
1414 let sinks = crate::table::TableSinks {
1415 deletion_pause: &self.deletion_pause,
1416 heal_hints: &self.heal_hints,
1417 read_budget: self.config.columnar_read_budget,
1418 #[cfg(feature = "std")]
1419 background_deleter: &self.background_deleter,
1420 track_filter_probes: self.config.filter_advisor.is_some(),
1421 };
1422 for table in tables {
1423 table.bind_to_tree(&sinks);
1424 }
1425 for bf in blob_files.unwrap_or(&[]) {
1426 bf.bind_to_tree(&sinks);
1427 }
1428
1429 let mut installed = InstalledCharge::default();
1430 installed.add(
1431 tables,
1432 blob_files.unwrap_or(&[]),
1433 crate::fs::WriteOrigin::FlushTable,
1434 crate::fs::WriteOrigin::FlushBlob,
1435 )?;
1436
1437 let mut _compaction_state = self.compaction_state.lock();
1438 let mut version_lock = self.version_history.write();
1439
1440 // A sealed memtable that left the version while it was flushed was
1441 // removed with its data (a `clear`): the tables written from it hold
1442 // that data, and are dropped rather than installed.
1443 let sealed = &version_lock.latest_version_ref().sealed_memtables;
1444 if !sealed_memtables_to_delete
1445 .iter()
1446 .all(|&id| sealed.iter().any(|mt| mt.id() == id))
1447 {
1448 log::warn!(
1449 "Flushed memtables left the version before the install, declining the flush"
1450 );
1451 drop(version_lock);
1452 for table in tables {
1453 table.mark_as_deleted();
1454 }
1455 for blob_file in blob_files.unwrap_or(&[]) {
1456 blob_file.mark_as_deleted();
1457 }
1458 return Ok(());
1459 }
1460
1461 // A deferred flush wrote its files unsynced: installed in memory only,
1462 // they are synced and written to the manifest by the next persist.
1463 let deferred = (self.config.flush_durability == crate::config::FlushDurability::Deferred)
1464 .then(|| {
1465 tables
1466 .iter()
1467 .cloned()
1468 .map(crate::version::UnsyncedFile::Table)
1469 .chain(
1470 blob_files
1471 .unwrap_or(&[])
1472 .iter()
1473 .cloned()
1474 .map(crate::version::UnsyncedFile::Blob),
1475 )
1476 .collect()
1477 });
1478 version_lock.upgrade_version_deferring(
1479 &self.config.path,
1480 |current| {
1481 let mut copy = current.clone();
1482
1483 let ctx = crate::version::TransformContext::new(self.config.comparator.as_ref());
1484 copy.version = copy.version.with_new_l0_run(
1485 tables,
1486 blob_files,
1487 frag_map.filter(|x| !x.is_empty()),
1488 &ctx,
1489 );
1490
1491 for &table_id in sealed_memtables_to_delete {
1492 log::trace!("releasing sealed memtable #{table_id}");
1493 copy.sealed_memtables = Arc::new(copy.sealed_memtables.remove(table_id));
1494 }
1495
1496 Ok(copy)
1497 },
1498 &self.config.seqno,
1499 &self.config.visible_seqno,
1500 &*self.config.fs,
1501 self.0.runtime_config.load_full(),
1502 self.0.config.encryption.clone(),
1503 // A flush adds a run, but it does not only add: `AbstractTree::flush`
1504 // feeds the sealed memtables through the same `CompactionStream`
1505 // with this same watermark, so it collects the versions below it
1506 // exactly as a compaction does, and owes older snapshots the same
1507 // accounting. Same rule, evaluated in the same place, because a
1508 // flush and a compaction disagreeing about it is how a floor came
1509 // to promise data the output no longer held.
1510 //
1511 // `false` for the filter: the flush stream carries no user
1512 // compaction filter, so `DropsData` is unreachable here.
1513 crate::version::RetentionEffect::of_run(false, collected_below_watermark, gc_watermark),
1514 deferred,
1515 )?;
1516 installed.charge();
1517
1518 Ok(())
1519 }
1520
1521 fn clear_active_memtable(&self) {
1522 use crate::tree::sealed::SealedMemtables;
1523
1524 let mut version_history_lock = self.version_history.write();
1525 let super_version = version_history_lock.latest_version();
1526
1527 if super_version.active_memtable.is_empty() {
1528 return;
1529 }
1530
1531 let mut copy = version_history_lock.latest_version();
1532 copy.active_memtable = Arc::new(Memtable::new(
1533 self.memtable_id_counter.next(),
1534 self.config.comparator.clone(),
1535 ));
1536 copy.sealed_memtables = Arc::new(SealedMemtables::default());
1537
1538 // Rotate does not modify the memtable, so it cannot break snapshots
1539 copy.seqno = super_version.seqno;
1540
1541 version_history_lock.replace_latest_version(copy);
1542
1543 log::trace!("cleared active memtable");
1544 }
1545
1546 fn compact(
1547 &self,
1548 strategy: Arc<dyn CompactionStrategy>,
1549 gc_watermark: SeqNo,
1550 ) -> crate::Result<crate::compaction::CompactionResult> {
1551 // NOTE: Read lock major compaction lock
1552 // That way, if a major compaction is running, we cannot proceed
1553 // But in general, parallel (non-major) compactions can occur
1554 let _lock = self.0.major_compaction_lock.read();
1555
1556 self.inner_compact(strategy, gc_watermark)
1557 }
1558
1559 fn get_next_table_id(&self) -> TableId {
1560 self.0.get_next_table_id()
1561 }
1562
1563 fn tree_config(&self) -> &Config {
1564 &self.config
1565 }
1566
1567 fn active_memtable(&self) -> Arc<Memtable> {
1568 self.version_history
1569 .read()
1570 .latest_version_ref()
1571 .active_memtable
1572 .clone()
1573 }
1574
1575 // Its size says so without the range-tombstone lock `Memtable::is_empty`
1576 // takes, which a tombstone insert holds across the tree's comparator.
1577 #[cfg(feature = "std")]
1578 fn active_memtable_is_empty(&self) -> bool {
1579 self.latest_super_version.load().active_memtable.size() == 0
1580 }
1581
1582 #[cfg(not(feature = "std"))]
1583 fn active_memtable_is_empty(&self) -> bool {
1584 self.version_history
1585 .read()
1586 .latest_version_ref()
1587 .active_memtable
1588 .size()
1589 == 0
1590 }
1591
1592 fn rotate_memtable(&self) -> Option<Arc<Memtable>> {
1593 self.rotate_memtable_locked(&mut self.version_history.write())
1594 }
1595
1596 fn table_count(&self) -> usize {
1597 self.current_version().table_count()
1598 }
1599
1600 fn level_table_count(&self, idx: usize) -> Option<usize> {
1601 self.current_version().level(idx).map(|x| x.table_count())
1602 }
1603
1604 fn approximate_len(&self) -> usize {
1605 let super_version = self.version_history.read().latest_version();
1606
1607 let tables_item_count = self
1608 .current_version()
1609 .iter_tables()
1610 .map(|x| x.metadata.item_count)
1611 .sum::<u64>();
1612
1613 let memtable_count = super_version.active_memtable.len() as u64;
1614 let sealed_count = super_version
1615 .sealed_memtables
1616 .iter()
1617 .map(|mt| mt.len())
1618 .sum::<usize>() as u64;
1619
1620 #[expect(clippy::expect_used, reason = "result should fit into usize")]
1621 (memtable_count + sealed_count + tables_item_count)
1622 .try_into()
1623 .expect("approximate_len too large for usize")
1624 }
1625
1626 fn disk_space(&self) -> u64 {
1627 self.current_version()
1628 .iter_levels()
1629 .map(super::version::Level::size)
1630 .sum()
1631 }
1632
1633 fn approximate_range_stats<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
1634 &self,
1635 range: R,
1636 seqno: SeqNo,
1637 ) -> crate::Result<crate::ApproximateRangeStats> {
1638 use core::ops::Bound;
1639
1640 // A query planner asks for this estimate as part of the query it
1641 // plans, so the index walks below are the query's reads and stay
1642 // counted. A monitoring report's reads are the ones kept out of the
1643 // counters (see `Table::live_item_count`).
1644 let lo: Bound<&[u8]> = match range.start_bound() {
1645 Bound::Included(k) => Bound::Included(k.as_ref()),
1646 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1647 Bound::Unbounded => Bound::Unbounded,
1648 };
1649 let hi: Bound<&[u8]> = match range.end_bound() {
1650 Bound::Included(k) => Bound::Included(k.as_ref()),
1651 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1652 Bound::Unbounded => Bound::Unbounded,
1653 };
1654 let bounds = (lo, hi);
1655
1656 let mut bytes: u64 = 0;
1657 let mut key_count: u64 = 0;
1658
1659 // Use ONE snapshot at the requested seqno for both the SST and memtable
1660 // contributions, so the estimate is taken against a read's view at
1661 // `seqno`: one consistent TABLE set even during a concurrent flush or
1662 // compaction, and memtable entries filtered by the snapshot. Neither
1663 // half is exact, and the two paragraphs below say how.
1664 //
1665 // What "the same visibility as a read" does and does not mean here.
1666 //
1667 // It uses the FILE SET a read at `seqno` resolves to, which is the
1668 // current one for every snapshot above the floor, and filters the
1669 // memtables by `seqno`. It is not a per-row mask: a table whose seqno
1670 // range straddles `seqno` is classified `Partial` and then apportioned
1671 // whole by byte offsets, so its share can include rows the snapshot
1672 // cannot read. The estimate is an estimate, and this is one of the ways
1673 // it is approximate.
1674 //
1675 // Nor does resolving once freeze the tree. The clone pins the version's
1676 // tables, but the active memtable behind it is the live one every
1677 // writer mutates, so a write landing mid-call can be counted by the
1678 // memtable half after the table half was read. And a compaction
1679 // installing after the clone is what the NEXT read at that seqno
1680 // resolves to: the layout described here is one compaction is free to
1681 // change the moment this returns.
1682 let comparator = self.config.comparator.as_ref();
1683 let super_version = self.get_version_for_snapshot(seqno)?;
1684
1685 // SST contribution: interpolate data-block offsets at the boundaries
1686 // (block granularity), no data-block reads. For a KV-separated SST the
1687 // referenced blob bytes are apportioned by the same in-range fraction.
1688 for table in super_version.version.iter_tables() {
1689 // Comparator-aware overlap: a custom user comparator orders keys
1690 // differently from raw bytes, so use the same comparison the read
1691 // path does instead of default byte ordering.
1692 if !table
1693 .metadata
1694 .key_range
1695 .overlaps_with_bounds_cmp(&bounds, comparator)
1696 {
1697 continue;
1698 }
1699 // The block index is keyed by the table-LOCAL seqno; a bulk-ingested
1700 // table carries a non-zero global seqno, so translate the snapshot
1701 // seqno the same way the read path does before seeking it. A snapshot
1702 // below the table's base means the table postdates it and contributes
1703 // nothing to the estimate, so skip it (`checked_sub` yields `None`).
1704 let Some(table_seqno) = seqno.checked_sub(table.global_seqno()) else {
1705 continue;
1706 };
1707 // The translation alone is not the visibility rule: a table with
1708 // `global_seqno == 0` passes it whatever its entries hold, so one
1709 // holding nothing but seqno 100 would charge rows and bytes to a
1710 // query at seqno 50 that reads none of them. Ask the same
1711 // classification the read path uses.
1712 if table.seqno_visibility(seqno) == crate::table::SeqnoVisibility::None {
1713 continue;
1714 }
1715
1716 // A restricted view's punched-out prefix is served by its
1717 // replacement table, so the span starts past it and the prefix is
1718 // not double-counted (matching how scans skip it).
1719 let Some(span) = table.data_span(bounds, table_seqno, crate::table::SpanEdge::Whole)?
1720 else {
1721 continue;
1722 };
1723 let (idx_bytes, data_end) = (span.covered, span.data_end);
1724 if idx_bytes == 0 {
1725 continue;
1726 }
1727
1728 // fraction = idx_bytes / data_end, in u128 to avoid overflow. For a
1729 // standard tree `idx_bytes` already includes the inline values. For a
1730 // KV-separated SST it covers only the key + pointer bytes, so the
1731 // SST's referenced blob bytes (recorded per-SST at both flush and
1732 // compaction) are apportioned by the same in-range fraction; blob
1733 // files are not key-indexed, so this fraction is the finest estimate
1734 // possible without reading data blocks.
1735 let num = u128::from(idx_bytes);
1736 let den = u128::from(data_end);
1737 let blob_bytes = table.referenced_blob_bytes()?;
1738 // `num <= den` (both offsets are bounded by `data_end`), so
1739 // `x * num / den <= x` and the u128 -> u64 narrowing is total —
1740 // no fallback value to mask a range error with.
1741 #[expect(
1742 clippy::cast_possible_truncation,
1743 reason = "num <= den, so the quotient never exceeds the u64 input"
1744 )]
1745 let sst_blob = (u128::from(blob_bytes) * num / den) as u64;
1746 // Round up to at least one entry: a non-empty byte span over a
1747 // non-empty SST always covers at least one row, so a narrow range
1748 // never reports bytes with a zero key count.
1749 #[expect(
1750 clippy::cast_possible_truncation,
1751 reason = "num <= den, so the quotient never exceeds the u64 input"
1752 )]
1753 let in_range_entries =
1754 ((u128::from(table.metadata.item_count) * num / den) as u64).max(1);
1755 bytes = bytes.saturating_add(idx_bytes).saturating_add(sst_blob);
1756 key_count = key_count.saturating_add(in_range_entries);
1757 }
1758
1759 // Memtable contribution: the in-range fraction of each memtable's
1760 // approximate size. Built from the SAME snapshot and the SAME
1761 // `range_internal` + internal-key bounds the read path uses (range.rs),
1762 // so the counted slice matches what a read at `seqno` would traverse.
1763 let mt_range =
1764 crate::range::internal_bounds(&lo.map(UserKey::from), &hi.map(UserKey::from));
1765 let estimate = |mt: &crate::Memtable| -> (u64, u64) {
1766 let total = mt.len() as u64;
1767 if total == 0 {
1768 return (0, 0);
1769 }
1770 // Count only entries visible at the snapshot (the same seqno cutoff
1771 // reads apply), so the estimate excludes writes newer than `seqno`.
1772 let count = mt
1773 .range_internal(mt_range.clone())
1774 .filter(|kv| kv.key.seqno < seqno)
1775 .count() as u64;
1776 if count == 0 {
1777 return (0, 0);
1778 }
1779 // `count <= total` (the counted entries are a filtered subset of
1780 // the memtable), so the quotient never exceeds the u64 `size()`
1781 // and the narrowing is total.
1782 #[expect(
1783 clippy::cast_possible_truncation,
1784 reason = "count <= total, so the quotient never exceeds the u64 input"
1785 )]
1786 let mt_bytes = (u128::from(mt.size()) * u128::from(count) / u128::from(total)) as u64;
1787 (mt_bytes, count)
1788 };
1789 let (b, c) = estimate(&super_version.active_memtable);
1790 bytes = bytes.saturating_add(b);
1791 key_count = key_count.saturating_add(c);
1792 for mt in super_version.sealed_memtables.iter() {
1793 let (b, c) = estimate(mt);
1794 bytes = bytes.saturating_add(b);
1795 key_count = key_count.saturating_add(c);
1796 }
1797
1798 Ok(crate::ApproximateRangeStats { bytes, key_count })
1799 }
1800
1801 fn approximate_range_cardinality<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
1802 &self,
1803 range: R,
1804 seqno: SeqNo,
1805 ) -> crate::Result<crate::RangeCardinality> {
1806 use crate::table::block_index::BlockIndex;
1807 use core::cmp::Ordering;
1808 use core::ops::Bound;
1809
1810 let lo: Bound<&[u8]> = match range.start_bound() {
1811 Bound::Included(k) => Bound::Included(k.as_ref()),
1812 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1813 Bound::Unbounded => Bound::Unbounded,
1814 };
1815 let hi: Bound<&[u8]> = match range.end_bound() {
1816 Bound::Included(k) => Bound::Included(k.as_ref()),
1817 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1818 Bound::Unbounded => Bound::Unbounded,
1819 };
1820 let bounds = (lo, hi);
1821 let comparator = self.config.comparator.as_ref();
1822 let super_version = self.get_version_for_snapshot(seqno)?;
1823
1824 let mut rows: u64 = 0;
1825 let mut total_rows: u64 = 0;
1826
1827 for table in super_version.version.iter_tables() {
1828 // Snapshot visibility is settled BEFORE the denominator grows. A
1829 // table no read at `seqno` can see is not part of the dataset the
1830 // selectivity describes, and counting it there while excluding it
1831 // from `rows` reports a full-keyspace query as selecting half the
1832 // tree. Out-of-RANGE tables do belong in the denominator, which is
1833 // why that check stays below it.
1834 //
1835 // A snapshot below the table's base means the table postdates it
1836 // (`checked_sub` yields `None`).
1837 let Some(table_seqno) = seqno.checked_sub(table.global_seqno()) else {
1838 continue;
1839 };
1840 // Wholly above the snapshot: invisible to a read at `seqno`, so it
1841 // contributes nothing here either (mirrors approximate_range_stats).
1842 if table.seqno_visibility(seqno) == crate::table::SeqnoVisibility::None {
1843 continue;
1844 }
1845 // What this VIEW serves, not what the file holds: a tight-space
1846 // restricted table's metadata still counts the punched-out prefix
1847 // its replacement now owns, while the numerator below starts at the
1848 // restriction — so charging the prefix here would report a
1849 // full-keyspace query as selecting a fraction of the tree.
1850 // The planner's estimate is part of the query it plans, so the
1851 // reads it makes are the query's and are counted, like the index
1852 // walks below.
1853 total_rows = total_rows
1854 .saturating_add(table.live_item_count(crate::table::util::ReadCharge::Foreground)?);
1855 if !table
1856 .metadata
1857 .key_range
1858 .overlaps_with_bounds_cmp(&bounds, comparator)
1859 {
1860 continue;
1861 }
1862 // Honor a tight-space restricted view: keys below
1863 // `restrict_lower_bound()` are the punched-out prefix served by the
1864 // replacement table, so raise this table's effective lower bound to
1865 // it (mirrors approximate_range_stats) and never charge that prefix.
1866 let eff_lo = effective_lower_bound(
1867 lo,
1868 table.restrict_lower_bound().map(AsRef::as_ref),
1869 comparator,
1870 );
1871 let zone_map = &table.zone_map;
1872 // A COLUMNAR block's per-column bounds are recorded in BYTE order,
1873 // which is the only ordering a value column has. Comparing them
1874 // with a non-lexicographic user comparator reads the recorded
1875 // minimum as a comparator maximum, so the walk stops before blocks
1876 // that overlap the query: the shortcut is skipped for such trees
1877 // and the byte-fraction estimate below answers instead. A row
1878 // block's bounds are the block's first and last key, already in
1879 // comparator order, so the default path is unaffected.
1880 let zone_bounds_ordered = comparator.is_lexicographic() || !table.metadata.columnar;
1881 if !zone_map.is_empty() && zone_bounds_ordered {
1882 // Zone map present: sum the per-block row counts of blocks whose
1883 // key range overlaps the query. A block is past the range once its
1884 // minimum key is above the upper bound; the boundary block at the
1885 // effective lower bound is counted in full (block granularity). A
1886 // range that lands in a key-space gap legitimately yields zero, so
1887 // this path is authoritative and never falls back to the byte fraction.
1888 let reader = match eff_lo {
1889 Bound::Included(k) | Bound::Excluded(k) => {
1890 table.block_index.forward_reader(k, table_seqno)
1891 }
1892 Bound::Unbounded => Some(table.block_index.iter()),
1893 };
1894 if let Some(reader) = reader {
1895 for handle in reader {
1896 let handle = handle?;
1897 let Some(col) = zone_map
1898 .columns_for(*handle.offset())
1899 .and_then(|c| c.first())
1900 else {
1901 continue;
1902 };
1903 let above_hi = match hi {
1904 Bound::Included(hk) => {
1905 comparator.compare(&col.min, hk) == Ordering::Greater
1906 }
1907 Bound::Excluded(hk) => {
1908 comparator.compare(&col.min, hk) != Ordering::Less
1909 }
1910 Bound::Unbounded => false,
1911 };
1912 if above_hi {
1913 break;
1914 }
1915 rows = rows.saturating_add(u64::from(col.row_count));
1916 }
1917 }
1918 } else if let Some(last) = table.block_index.iter().next_back() {
1919 // No zone map: apportion item_count by the in-range
1920 // data-block byte fraction, mirroring approximate_range_stats.
1921 let last = last?;
1922 let data_end = *last.offset() + u64::from(last.size());
1923 if data_end > 0 {
1924 let off = |key: &[u8], end: bool| -> crate::Result<u64> {
1925 match table.block_index.forward_reader(key, table_seqno) {
1926 Some(mut it) => match it.next() {
1927 Some(h) => {
1928 let h = h?;
1929 Ok(if end {
1930 (*h.offset() + u64::from(h.size())).min(data_end)
1931 } else {
1932 *h.offset()
1933 })
1934 }
1935 None => Ok(data_end),
1936 },
1937 None => Ok(data_end),
1938 }
1939 };
1940 let off_lo = match eff_lo {
1941 Bound::Included(k) | Bound::Excluded(k) => off(k, false)?,
1942 Bound::Unbounded => 0,
1943 };
1944 let off_hi = match hi {
1945 Bound::Included(k) | Bound::Excluded(k) => off(k, true)?,
1946 Bound::Unbounded => data_end,
1947 };
1948 let idx_bytes = off_hi.saturating_sub(off_lo);
1949 if idx_bytes > 0 {
1950 // `idx_bytes <= data_end` (both offsets are bounded by
1951 // `data_end`), so the quotient never exceeds the u64
1952 // `item_count` and the narrowing is total.
1953 #[expect(
1954 clippy::cast_possible_truncation,
1955 reason = "idx_bytes <= data_end, so the quotient never exceeds the u64 input"
1956 )]
1957 let est = ((u128::from(table.metadata.item_count) * u128::from(idx_bytes)
1958 / u128::from(data_end)) as u64)
1959 .max(1);
1960 rows = rows.saturating_add(est);
1961 }
1962 }
1963 }
1964 }
1965
1966 // Memtables: count the in-range, snapshot-visible entries and add them to
1967 // both the matched rows and the total (matching the SST accounting).
1968 let mt_range =
1969 crate::range::internal_bounds(&lo.map(UserKey::from), &hi.map(UserKey::from));
1970 let mut add_memtable = |mt: &crate::Memtable| {
1971 // The denominator counts what a read at this snapshot can SEE, the
1972 // same rule the SST loop applies: an entry at or above `seqno` is
1973 // filtered out of the numerator below, so counting it here would
1974 // report a full-keyspace query as selecting half of what it sees.
1975 let visible = mt.iter().filter(|kv| kv.key.seqno < seqno).count() as u64;
1976 total_rows = total_rows.saturating_add(visible);
1977 let in_range = mt
1978 .range_internal(mt_range.clone())
1979 .filter(|kv| kv.key.seqno < seqno)
1980 .count() as u64;
1981 rows = rows.saturating_add(in_range);
1982 };
1983 add_memtable(&super_version.active_memtable);
1984 for mt in super_version.sealed_memtables.iter() {
1985 add_memtable(mt);
1986 }
1987
1988 // selectivity is an approximate ratio; u64 row counts are well within
1989 // f64's exact-integer range (2^52) for any realistic table.
1990 #[expect(
1991 clippy::cast_precision_loss,
1992 reason = "row counts never approach 2^52; the ratio is approximate anyway"
1993 )]
1994 let selectivity = if total_rows == 0 {
1995 0.0
1996 } else {
1997 (rows.min(total_rows) as f64) / (total_rows as f64)
1998 };
1999 Ok(crate::RangeCardinality { rows, selectivity })
2000 }
2001
2002 fn get_highest_memtable_seqno(&self) -> Option<SeqNo> {
2003 highest_memtable_seqno(&self.version_history.read().latest_version())
2004 }
2005
2006 fn get_highest_seqno(&self) -> Option<SeqNo> {
2007 // One snapshot for the tables and the memtables: a flush installed
2008 // between two reads would leave its writes in neither. Its current
2009 // version, not the one on disk: a deferred flush moves its writes out
2010 // of the memtable before any persist names them.
2011 let snapshot = self.version_history.read().latest_version();
2012 let tables = snapshot
2013 .version
2014 .iter_tables()
2015 .map(Table::get_highest_seqno)
2016 .max();
2017 highest_memtable_seqno(&snapshot).max(tables)
2018 }
2019
2020 fn get_highest_persisted_seqno(&self) -> Option<SeqNo> {
2021 // The version on disk, not the current one: a deferred flush is not
2022 // persisted until a persist writes it, and a caller trimming its log
2023 // by this seqno must keep what such a flush holds.
2024 self.version_history
2025 .read()
2026 .persisted_version()
2027 .iter_tables()
2028 .map(Table::get_highest_seqno)
2029 .max()
2030 }
2031
2032 fn oldest_retained_seqno(&self) -> SeqNo {
2033 // The read boundary is the persisted floor, live and after a reopen.
2034 self.retention_floor()
2035 }
2036
2037 fn retention_floor(&self) -> SeqNo {
2038 self.version_history
2039 .read()
2040 .latest_version_ref()
2041 .version
2042 .retention_floor()
2043 }
2044
2045 fn lower_retention_floor(&self, floor: SeqNo) -> crate::Result<()> {
2046 let config = self.tree_config();
2047 let mut versions = self.get_version_history_lock();
2048 // Checked under the lock the install takes, so no install raising the
2049 // floor can land between the check and the edit.
2050 let current = versions.latest_version_ref().version.retention_floor();
2051 if floor > current {
2052 return Err(crate::Error::RetentionFloorRaise {
2053 current,
2054 requested: floor,
2055 });
2056 }
2057 if floor == current {
2058 return Ok(());
2059 }
2060 // A counter reset on reopen can stand below the current floor.
2061 let seqno = config.seqno.get();
2062 if floor > seqno {
2063 return Err(crate::Error::RetentionFloorAboveSeqno {
2064 seqno,
2065 requested: floor,
2066 });
2067 }
2068 versions.upgrade_version(
2069 &config.path,
2070 |v| {
2071 let mut copy = v.clone();
2072 copy.version = v.version.with_lowered_retention_floor(floor);
2073 Ok(copy)
2074 },
2075 &config.seqno,
2076 &config.visible_seqno,
2077 &*config.fs,
2078 self.0.runtime_config.load_full(),
2079 self.0.config.encryption.clone(),
2080 // The floor is the mutator's: `Keep` lets the install raise none.
2081 crate::version::RetentionEffect::Keep,
2082 )
2083 }
2084
2085 fn compaction_rate_limiter(&self) -> Arc<crate::rate_limiter::RateLimiter> {
2086 Arc::clone(&self.compaction_rate_limiter)
2087 }
2088
2089 fn write_rate_limiter(&self) -> Arc<crate::rate_limiter::RateLimiter> {
2090 Arc::clone(&self.write_rate_limiter)
2091 }
2092
2093 fn get<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<UserValue>> {
2094 let key = key.as_ref();
2095
2096 let super_version = self.snapshot_for_read(seqno)?;
2097 // Counted here, not in the lookup: a columnar scan resolving merge
2098 // operands looks keys up too, as part of the scan it counts.
2099 super_version.version.read_mix().note_points(1);
2100
2101 Self::resolve_or_passthrough(
2102 &super_version,
2103 key,
2104 seqno,
2105 self.config.merge_operator.as_ref(),
2106 self.config.comparator.as_ref(),
2107 || self.blob_source(),
2108 )
2109 }
2110
2111 // Existence needs the newest visible version, not its value: a merge
2112 // chain always resolves to some value, so its operands are not merged and
2113 // a separated base is not read.
2114 fn contains_key<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<bool> {
2115 let super_version = self.snapshot_for_read(seqno)?;
2116 super_version.version.read_mix().note_points(1);
2117 Ok(Self::get_value(
2118 &super_version,
2119 key.as_ref(),
2120 seqno,
2121 self.config.comparator.as_ref(),
2122 )?
2123 .is_some())
2124 }
2125
2126 // Key-only like `contains_key`: the scan runs without the merge operator,
2127 // which yields a chain's newest operand in place of its merged value, the
2128 // same key either way.
2129 fn contains_prefix<K: AsRef<[u8]>>(
2130 &self,
2131 prefix: K,
2132 seqno: SeqNo,
2133 index: Option<(Arc<Memtable>, SeqNo)>,
2134 ) -> crate::Result<bool> {
2135 Ok(self
2136 .create_internal_prefix(prefix.as_ref(), seqno, index, None, None)?
2137 .next()
2138 .transpose()?
2139 .is_some())
2140 }
2141
2142 fn is_empty(&self, seqno: SeqNo, index: Option<(Arc<Memtable>, SeqNo)>) -> crate::Result<bool> {
2143 Ok(self
2144 .create_key_only_iter(seqno, index)?
2145 .next()
2146 .transpose()?
2147 .is_none())
2148 }
2149
2150 fn len(&self, seqno: SeqNo, index: Option<(Arc<Memtable>, SeqNo)>) -> crate::Result<usize> {
2151 let mut count = 0;
2152 for item in self.create_key_only_iter(seqno, index)? {
2153 item?;
2154 count += 1;
2155 }
2156 Ok(count)
2157 }
2158
2159 fn get_pinned<K: AsRef<[u8]>>(
2160 &self,
2161 key: K,
2162 seqno: SeqNo,
2163 ) -> crate::Result<Option<crate::PinnableSlice>> {
2164 let key = key.as_ref();
2165
2166 let super_version = self.snapshot_for_read(seqno)?;
2167 super_version.version.read_mix().note_points(1);
2168
2169 Self::resolve_or_passthrough_pinned(
2170 &super_version,
2171 key,
2172 seqno,
2173 self.config.merge_operator.as_ref(),
2174 self.config.comparator.as_ref(),
2175 || self.blob_source(),
2176 )
2177 }
2178
2179 fn multi_get<K: AsRef<[u8]>>(
2180 &self,
2181 keys: impl IntoIterator<Item = K>,
2182 seqno: SeqNo,
2183 ) -> crate::Result<Vec<Option<UserValue>>> {
2184 let super_version = self.snapshot_for_read(seqno)?;
2185 let comparator = self.config.comparator.as_ref();
2186 let merge_operator = self.config.merge_operator.as_ref();
2187
2188 let keys: Vec<_> = keys.into_iter().collect();
2189 let n = keys.len();
2190 if n == 0 {
2191 return Ok(Vec::new());
2192 }
2193
2194 // For small batches, use the simple per-key path
2195 if n <= 2 {
2196 super_version
2197 .version
2198 .read_mix()
2199 .note_points(u64::try_from(n).unwrap_or(u64::MAX));
2200 // One blob source for every key of the batch, taken when a merge
2201 // first needs it, so both keys read under one fill policy.
2202 let blob_source = core::cell::OnceCell::new();
2203 return keys
2204 .iter()
2205 .map(|key| {
2206 Self::resolve_or_passthrough(
2207 &super_version,
2208 key.as_ref(),
2209 seqno,
2210 merge_operator,
2211 comparator,
2212 || blob_source.get_or_init(|| self.blob_source()).clone(),
2213 )
2214 })
2215 .collect();
2216 }
2217
2218 Self::read_many(&read_job::ReadCtx {
2219 super_version,
2220 keys,
2221 seqno,
2222 comparator: Arc::clone(&self.config.comparator),
2223 merge_operator: self.config.merge_operator.clone(),
2224 merge_base: self.blob_source(),
2225 values: read_job::Values::Inline,
2226 metadata_budget: self.config.multi_get_metadata_budget,
2227 })
2228 }
2229
2230 fn apply_batch(&self, batch: crate::WriteBatch, seqno: SeqNo) -> crate::Result<(u64, u64)> {
2231 if batch.is_empty() {
2232 return Ok((0, self.active_memtable().size()));
2233 }
2234 Ok(self.append_batch(batch.materialize(seqno)?))
2235 }
2236
2237 fn insert<K: Into<UserKey>, V: Into<UserValue>>(
2238 &self,
2239 key: K,
2240 value: V,
2241 seqno: SeqNo,
2242 ) -> (u64, u64) {
2243 let value = InternalValue::from_components(key, value, seqno, ValueType::Value);
2244 self.append_entry(value)
2245 }
2246
2247 fn merge<K: Into<UserKey>, V: Into<UserValue>>(
2248 &self,
2249 key: K,
2250 operand: V,
2251 seqno: SeqNo,
2252 ) -> (u64, u64) {
2253 let value = InternalValue::new_merge_operand(key, operand, seqno);
2254 self.append_entry(value)
2255 }
2256
2257 fn remove<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
2258 let value = InternalValue::new_tombstone(key, seqno);
2259 self.append_entry(value)
2260 }
2261
2262 fn remove_weak<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
2263 let value = InternalValue::new_weak_tombstone(key, seqno);
2264 self.append_entry(value)
2265 }
2266
2267 fn remove_range<K: Into<UserKey>>(&self, start: K, end: K, seqno: SeqNo) -> u64 {
2268 let (start, end) = (start.into(), end.into());
2269 #[cfg(feature = "metrics")]
2270 self.0
2271 .metrics
2272 .accepted_range_deletion(start.len() + end.len());
2273 // The read guard is held through the insert, like `append_entry`: the
2274 // CDC scan's capture (write side of this lock) must exclude every
2275 // in-flight memtable write, or a backdated range deletion could land
2276 // after the capture yet below the returned watermark and be lost; the
2277 // guard also keeps a concurrent `rotate_memtable()` from sealing the
2278 // memtable mid-insert.
2279 let history = self.version_history.read();
2280
2281 #[cfg(test)]
2282 inner::TestHooks::fire(&self.test_hooks.range_write);
2283
2284 history
2285 .latest_version_ref()
2286 .active_memtable
2287 .insert_range_tombstone(start, end, seqno)
2288 }
2289}
2290
2291impl Tree {
2292 /// The filter plan of new data written into tables under the policies of
2293 /// `level`, as a flush or an ingestion writes it: at most `count` keys and
2294 /// filter hashes (zero when unknown) under `bloom_policy`. `None` without
2295 /// an advisor.
2296 /// New data has no probe history of its own: its filters are sized by the
2297 /// load the live tables draw per key, and room is kept for every table the
2298 /// write fills. The plan is held until the tables are installed (see
2299 /// `WritePin`).
2300 ///
2301 /// `ecc_scheme` is the one the write's runtime snapshot frames its blocks
2302 /// under.
2303 pub(crate) fn new_data_filter_sizing(
2304 &self,
2305 level: usize,
2306 bloom_policy: crate::table::filter::BloomConstructionPolicy,
2307 count: crate::filter_budget::FilterCount,
2308 ecc_scheme: crate::runtime_config::EccScheme,
2309 ) -> Option<crate::filter_budget::FilterPlan> {
2310 let advisor = self.config.filter_advisor.as_ref()?;
2311 let version = self.current_version();
2312 crate::filter_budget::plan(
2313 advisor,
2314 &self.filter_budget,
2315 &crate::filter_budget::live(&version),
2316 crate::filter_budget::Rewrite {
2317 count,
2318 framing: crate::filter_budget::Framing {
2319 encryption: self.config.encryption.clone(),
2320 ecc: crate::table::writer::resolve_ecc(self.config.page_ecc, ecc_scheme),
2321 },
2322 ..crate::filter_budget::Rewrite::default()
2323 },
2324 bloom_policy,
2325 self.config
2326 .filter_block_partitioning_policy
2327 .get(level)
2328 .then(|| self.config.filter_block_partition_size_policy.get(level)),
2329 )
2330 }
2331
2332 /// The encoded report of a repair the tree still owes, if any.
2333 #[cfg(feature = "std")]
2334 pub(crate) fn owed_repair_report(&self) -> Option<alloc::sync::Arc<[u8]>> {
2335 self.version_history
2336 .read()
2337 .latest_version_ref()
2338 .version
2339 .repair_report()
2340 .map(alloc::sync::Arc::from)
2341 }
2342
2343 /// See [`AbstractTree::acknowledge_repair_report`].
2344 #[cfg(feature = "std")]
2345 pub(crate) fn acknowledge_repair_report(&self, id: u128) -> crate::Result<bool> {
2346 let mut version_lock = self.version_history.write();
2347 let owes_it = version_lock
2348 .latest_version_ref()
2349 .version
2350 .repair_report()
2351 .is_some_and(|owed| crate::repair::owed_report_id(owed) == id);
2352 if !owes_it {
2353 return Ok(false);
2354 }
2355 let installed = version_lock.upgrade_version(
2356 &self.config.path,
2357 |current| {
2358 let mut copy = current.clone();
2359 copy.version = copy.version.without_repair_report();
2360 Ok(copy)
2361 },
2362 &self.config.seqno,
2363 &self.config.visible_seqno,
2364 &*self.config.fs,
2365 self.0.runtime_config.load_full(),
2366 self.0.config.encryption.clone(),
2367 // Only the report goes: every snapshot reads as before.
2368 crate::version::RetentionEffect::Keep,
2369 );
2370 drop(version_lock);
2371 installed.map(|()| true)
2372 }
2373
2374 /// Stores `dict` in the tree and records it in the current version, so it
2375 /// resolves after a reopen with no dictionary supplied in the config.
2376 ///
2377 /// Additive: tables already written keep resolving to the dictionary they
2378 /// were written against, which is what makes it safe to introduce a
2379 /// dictionary on a tree that already holds data, and to replace one (both
2380 /// are this same operation). Registering an id the tree already holds is a
2381 /// no-op, since the id is derived from the bytes.
2382 ///
2383 /// The file is written BEFORE the version edit that references it. The
2384 /// other order would publish an id whose bytes a crash could still lose,
2385 /// and the tables written against it would then fail to open; this order
2386 /// can only leave an unreferenced file, which the next collection removes.
2387 ///
2388 /// Both steps happen under the version lock, which is what serializes this
2389 /// against [`Self::collect_unreferenced_dictionaries`]. Taking the lock only
2390 /// for the edit would leave the file published in the window the collector
2391 /// scans, and it would be unlinked as an orphan before the edit that names
2392 /// it ever landed.
2393 ///
2394 /// # Errors
2395 ///
2396 /// Propagates the store's write failures and the version install's.
2397 #[cfg(zstd_any)]
2398 pub fn register_zstd_dictionary(
2399 &self,
2400 dict: alloc::sync::Arc<crate::compression::ZstdDictionary>,
2401 ) -> crate::Result<()> {
2402 let id = dict.id();
2403 let mut version_lock = self.version_history.write();
2404
2405 let folder = self.config.path.join(crate::file::DICTS_FOLDER);
2406 crate::dicts::write(
2407 &*self.config.fs,
2408 &folder,
2409 &dict,
2410 self.config.encryption.as_deref(),
2411 self.config.sync_mode,
2412 )?;
2413
2414 // Resolvable from this moment on rather than from the next open: the
2415 // tables this registration was made for are written right after it, and
2416 // reading them back needs the dictionary in the LIVE set.
2417 let dicts = self.config.current_zstd_dictionaries().with(dict);
2418 self.config
2419 .zstd_dictionaries
2420 .store(alloc::sync::Arc::new(dicts));
2421
2422 if version_lock
2423 .latest_version_ref()
2424 .version
2425 .dicts()
2426 .contains(&id)
2427 {
2428 return Ok(());
2429 }
2430 version_lock.upgrade_version(
2431 &self.config.path,
2432 |current| {
2433 let mut copy = current.clone();
2434 copy.version = copy.version.with_dict(id);
2435 Ok(copy)
2436 },
2437 &self.config.seqno,
2438 &self.config.visible_seqno,
2439 &*self.config.fs,
2440 self.0.runtime_config.load_full(),
2441 self.0.config.encryption.clone(),
2442 // Registering a dictionary adds a file and drops nothing, so no
2443 // snapshot loses anything it could read before.
2444 crate::version::RetentionEffect::Keep,
2445 )
2446 }
2447
2448 /// The dictionaries this tree can decompress against, as of this call.
2449 #[cfg(zstd_any)]
2450 #[must_use]
2451 pub fn zstd_dictionaries(&self) -> crate::compression::ZstdDictionaries {
2452 self.config.current_zstd_dictionaries()
2453 }
2454
2455 /// The dictionary ids new files may still be written against: every one
2456 /// the live snapshot's policies name, and every one a replaced snapshot a
2457 /// writer still holds names (see `TreeInner::retired_write_snapshots`).
2458 #[cfg(zstd_any)]
2459 pub(crate) fn write_referenced_dict_ids(&self) -> Vec<crate::file::DictId> {
2460 let kv_separated = self.config.kv_separation_opts.is_some();
2461 let mut ids: Vec<crate::file::DictId> = self
2462 .0
2463 .runtime_config
2464 .load()
2465 .write_dict_ids(kv_separated)
2466 .collect();
2467 self.0.retired_write_snapshots.lock().retain(|snapshot| {
2468 let Some(held) = snapshot.upgrade() else {
2469 return false;
2470 };
2471 ids.extend(held.write_dict_ids(kv_separated));
2472 true
2473 });
2474 ids.sort_unstable();
2475 ids.dedup();
2476 ids
2477 }
2478
2479 /// Drops every dictionary nothing references any more, returning how many
2480 /// files were removed.
2481 ///
2482 /// The current version first stops REGISTERING an id nothing references
2483 /// (a version edit, costing an install and nothing on disk), and the file
2484 /// goes in the same pass once the installed version no longer names it. A
2485 /// reader still holding an older version is unaffected: its tables and
2486 /// blob files pinned their dictionaries when they were opened.
2487 ///
2488 /// REFERENCED means both directions: the files that already exist (tables
2489 /// and blob files alike), and the ids the compression policies name for
2490 /// what is written next. Counting only the first would unregister a
2491 /// dictionary before its first table is written, and nothing puts the id
2492 /// back afterwards, so a later pass would unlink the file while live
2493 /// tables are compressed against it. What is written next includes the
2494 /// writers already running: a flush or compaction started under a policy
2495 /// [`Self::update_runtime_config`] has since replaced still writes against
2496 /// the dictionary that policy named, so it is spared until their files are
2497 /// installed and name it themselves.
2498 ///
2499 /// The whole pass holds the version lock. Registration takes the same lock
2500 /// before it writes anything, so the two cannot interleave: without that, a
2501 /// registration's file could land between this scan and its removals and be
2502 /// unlinked as an orphan while the edit naming it was still to come.
2503 ///
2504 /// # Errors
2505 ///
2506 /// Propagates the version install and the store's removal failures.
2507 #[cfg(zstd_any)]
2508 pub fn collect_unreferenced_dictionaries(&self) -> crate::Result<usize> {
2509 let mut version_lock = self.version_history.write();
2510
2511 // Unregister what neither the current version's tables nor the write
2512 // policy needs.
2513 let latest = version_lock.latest_version_ref().version.clone();
2514 let mut referenced = latest.referenced_dicts();
2515 referenced.extend(self.write_referenced_dict_ids());
2516 let stale: Vec<crate::file::DictId> = latest
2517 .dicts()
2518 .iter()
2519 .copied()
2520 .filter(|id| !referenced.contains(id))
2521 .collect();
2522
2523 if !stale.is_empty() {
2524 version_lock.upgrade_version(
2525 &self.config.path,
2526 |current| {
2527 let mut copy = current.clone();
2528 copy.version = copy.version.without_dicts(&stale);
2529 Ok(copy)
2530 },
2531 &self.config.seqno,
2532 &self.config.visible_seqno,
2533 &*self.config.fs,
2534 self.0.runtime_config.load_full(),
2535 self.0.config.encryption.clone(),
2536 // Dropping a dictionary nothing reads takes away no row a
2537 // snapshot could observe: the current version no longer holds
2538 // a table that used it.
2539 crate::version::RetentionEffect::Keep,
2540 )?;
2541 }
2542
2543 // Unlink the files the current version no longer names. Still under
2544 // the version lock, so no registration can publish a file into the
2545 // window between the listing and the removals.
2546 let still_owed = version_lock.registered_dicts();
2547
2548 let folder = self.config.path.join(crate::file::DICTS_FOLDER);
2549 let mut removed = 0;
2550 for id in crate::dicts::list_ids(&*self.config.fs, &folder)? {
2551 if still_owed.contains(&id) || referenced.contains(&id) {
2552 continue;
2553 }
2554 // A held deletion pause SKIPS the dictionary entirely: it is not
2555 // queued for later. A checkpoint holds its captured version as a
2556 // LOCAL clone, so a concurrent install can replace that version
2557 // while the checkpoint is still going to link the dictionaries it
2558 // names; removing one now would fail
2559 // `link_dictionaries` on a missing file and abort a checkpoint that
2560 // was otherwise valid.
2561 //
2562 // Skipping rather than DEFERRING (which is what tables and blob
2563 // files do) because a queued removal fires unconditionally when the
2564 // pause drains, and by then the id may have been registered again:
2565 // registration finds the file already there, makes its write a
2566 // no-op, durably records the id, and the drain then deletes the
2567 // dictionary that registration just published. A table's removal
2568 // cannot be undone by re-creating the table, so deferring is right
2569 // for it; a dictionary collection is an explicit operation that can
2570 // simply be run again, so waiting for the next pass costs nothing
2571 // and leaves no queued action to fire against a re-registered file.
2572 if self.deletion_pause.is_active() {
2573 continue;
2574 }
2575 // A pin lists the dictionary for its copy: skipped the same way,
2576 // and the next pass after the pin drops removes it. Pins register
2577 // under the version lock held here, so none can slip in between.
2578 #[cfg(feature = "std")]
2579 if self
2580 .deletion_pause
2581 .is_pinned(&crate::dicts::path_of(&folder, id))
2582 {
2583 continue;
2584 }
2585 crate::dicts::remove(&*self.config.fs, &folder, id, self.config.sync_mode)?;
2586
2587 // Out of the LIVE set too, not just off the disk. The registry holds
2588 // each dictionary's raw bytes and its prepared decoder state, so a
2589 // tree that rotates dictionaries over a long life would otherwise
2590 // accumulate every generation it ever held until it is dropped,
2591 // and `zstd_dictionaries()` would keep naming ids the tree no
2592 // longer owns.
2593 self.config.zstd_dictionaries.store(alloc::sync::Arc::new(
2594 self.config.current_zstd_dictionaries().without(id),
2595 ));
2596 removed += 1;
2597 }
2598 // Explicitly, and only here: the guard is what excludes a concurrent
2599 // registration from the removals above, so releasing it at its last READ
2600 // (which is what tightening the scope would do) is precisely the race.
2601 //
2602 // This drop is also what satisfies `clippy::significant_drop_tightening`
2603 // — it makes the guard's last use its own release, so the lint does not
2604 // fire and the function needs no expectation. Adding one anyway is a
2605 // BUILD FAILURE here, not dead weight: `unfulfilled_lint_expectations`
2606 // is an error under the crate's `-D warnings`.
2607 drop(version_lock);
2608 Ok(removed)
2609 }
2610
2611 /// Maps a raw internal entry to its change-data-capture event, routing
2612 /// `Indirection` (KV-separated) values through `resolve_indirection`.
2613 ///
2614 /// A standard tree never stores `Indirection` and supplies a resolver that
2615 /// errors; the blob-tree scan path supplies one that reads the blob and
2616 /// returns an [`ScanSinceEvent::Insert`].
2617 fn map_event<F>(
2618 entry: InternalValue,
2619 version: &Version,
2620 resolve_indirection: &F,
2621 ) -> crate::Result<ScanSinceEvent>
2622 where
2623 F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
2624 {
2625 // A cell row may reference blob objects, so it resolves where an
2626 // indirection does.
2627 if matches!(
2628 entry.key.value_type,
2629 ValueType::Indirection | ValueType::CellRow
2630 ) {
2631 return resolve_indirection(version, entry);
2632 }
2633 let seqno = entry.key.seqno;
2634 let key = entry.key.user_key;
2635 Ok(match entry.key.value_type {
2636 ValueType::Value => ScanSinceEvent::Insert {
2637 key,
2638 value: entry.value,
2639 seqno,
2640 },
2641 ValueType::MergeOperand => ScanSinceEvent::MergeOperand {
2642 key,
2643 operand: entry.value,
2644 seqno,
2645 },
2646 // Weak (single-delete) tombstones keep their own event kind: a
2647 // weak tombstone annihilates exactly its matching put during
2648 // compaction and can then expose an older value, while a regular
2649 // tombstone keeps hiding it — collapsing both into one event
2650 // would make a replica replay a weak delete as a full delete and
2651 // diverge from the source.
2652 ValueType::Tombstone => ScanSinceEvent::PointTombstone { key, seqno },
2653 ValueType::WeakTombstone => ScanSinceEvent::WeakTombstone { key, seqno },
2654 ValueType::Indirection | ValueType::CellRow => {
2655 unreachable!("Indirection and CellRow handled above")
2656 }
2657 })
2658 }
2659
2660 /// Shared CDC aggregation behind [`Self::scan_since_seqno`] and the
2661 /// blob-tree scan path: gathers qualifying entries (`seqno >= target`) plus
2662 /// range tombstones from the active + sealed memtables and every SST (with
2663 /// block-skip), maps each entry to a [`ScanSinceEvent`] — routing
2664 /// `Indirection` values through `resolve_indirection` against the same
2665 /// version snapshot — and returns them in increasing seqno order.
2666 ///
2667 /// # Panics
2668 ///
2669 /// Panics if the internal version-history lock is poisoned.
2670 ///
2671 /// # Errors
2672 ///
2673 /// Returns `Err` if reading the index or a data block fails, or if
2674 /// `resolve_indirection` errors.
2675 pub(crate) fn scan_since_seqno_with<F>(
2676 &self,
2677 target_seqno: SeqNo,
2678 block_skip: bool,
2679 resolve_indirection: F,
2680 ) -> crate::Result<alloc::vec::IntoIter<ScanSinceEvent>>
2681 where
2682 F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
2683 {
2684 self.scan_since_seqno_scoped(target_seqno, block_skip, resolve_indirection, None)
2685 }
2686
2687 /// As [`Self::scan_since_seqno_with`], optionally scoped to a key range
2688 /// (in the tree comparator's order): point events are delivered only for
2689 /// keys INSIDE the bounds, range tombstones when their span OVERLAPS them
2690 /// (a tombstone reaching into the range affects replay within it), and
2691 /// SSTs whose key range cannot intersect the bounds are skipped without
2692 /// being read — which is what makes a post-repair reconciliation over
2693 /// [`RepairReport::lost_coverage`](crate::RepairReport) affordable.
2694 pub(crate) fn scan_since_seqno_scoped<F>(
2695 &self,
2696 target_seqno: SeqNo,
2697 block_skip: bool,
2698 resolve_indirection: F,
2699 key_range: Option<&(Bound<UserKey>, Bound<UserKey>)>,
2700 ) -> crate::Result<alloc::vec::IntoIter<ScanSinceEvent>>
2701 where
2702 F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
2703 {
2704 use core::cmp::Ordering;
2705
2706 let cmp = self.config.comparator.clone();
2707 let in_key_range = |key: &[u8]| -> bool {
2708 let Some((lo, hi)) = key_range else {
2709 return true;
2710 };
2711 (match lo {
2712 Bound::Included(b) => cmp.compare(key, b.as_ref()) != Ordering::Less,
2713 Bound::Excluded(b) => cmp.compare(key, b.as_ref()) == Ordering::Greater,
2714 Bound::Unbounded => true,
2715 }) && (match hi {
2716 Bound::Included(b) => cmp.compare(key, b.as_ref()) != Ordering::Greater,
2717 Bound::Excluded(b) => cmp.compare(key, b.as_ref()) == Ordering::Less,
2718 Bound::Unbounded => true,
2719 })
2720 };
2721 // A range tombstone covers `[start, end)`; it is delivered when that
2722 // span overlaps the scope, since a deletion reaching into the range
2723 // affects replay within it.
2724 let rt_in_range = |rt: &RangeTombstone| -> bool {
2725 let Some((lo, hi)) = key_range else {
2726 return true;
2727 };
2728 (match lo {
2729 // `rt.end` is EXCLUSIVE: the tombstone reaches keys strictly
2730 // below it, so it clears the lower bound only when its end is
2731 // ABOVE the bound key (for an excluded bound this over-includes
2732 // the touching case, which is harmless: replaying an extra
2733 // idempotent deletion event cannot corrupt a consumer).
2734 Bound::Included(b) | Bound::Excluded(b) => {
2735 cmp.compare(rt.end.as_ref(), b.as_ref()) == Ordering::Greater
2736 }
2737 Bound::Unbounded => true,
2738 }) && (match hi {
2739 Bound::Included(b) => {
2740 cmp.compare(rt.start.as_ref(), b.as_ref()) != Ordering::Greater
2741 }
2742 Bound::Excluded(b) => cmp.compare(rt.start.as_ref(), b.as_ref()) == Ordering::Less,
2743 Bound::Unbounded => true,
2744 })
2745 };
2746 // The active memtable is the one source a writer can still change, and
2747 // the seqno cap alone does not exclude that: a caller may commit with an
2748 // explicit seqno at or BELOW the cap (`apply_batch` takes the seqno from
2749 // the caller), and a live lock-free walk would then see that write or
2750 // miss it depending on where the node lands relative to the cursor —
2751 // even splitting one batch. A consumer that advanced past the returned
2752 // watermark would lose the change for good.
2753 //
2754 // So freeze it: a batch or range write holds the version-history READ
2755 // guard for its whole insert, and a single-entry insert holds its shard
2756 // of the insert gate instead, so taking the WRITE guard and closing the
2757 // gate excludes both, in the order a version change takes them. The cap
2758 // and the active memtable's raw entries are captured under them;
2759 // everything else (sealed memtables, tables) is immutable and needs
2760 // no coordination.
2761 //
2762 // Mapping runs AFTER the guard drops: it resolves blob indirections,
2763 // which reads a blob file, and no I/O may happen with writers blocked.
2764 // The memtables are walked over the scope only, as a range read walks
2765 // them.
2766 let memtable_keys = key_range.map_or(
2767 (Bound::Unbounded, Bound::Unbounded),
2768 crate::range::user_to_internal_bounds,
2769 );
2770 let (super_version, end_seqno, active_entries, active_range_tombstones) = {
2771 let guard = self.version_history.write();
2772 #[cfg(feature = "std")]
2773 let gate = self.insert_gate.close();
2774 let super_version = guard.latest_version();
2775 #[cfg(test)]
2776 inner::TestHooks::fire(&self.test_hooks.scan_freeze);
2777 let end_seqno = {
2778 let active = super_version.active_memtable.get_highest_seqno();
2779 let sealed = super_version
2780 .sealed_memtables
2781 .iter()
2782 .map(|mt| mt.get_highest_seqno())
2783 .max()
2784 .flatten();
2785 let tables = super_version
2786 .version
2787 .iter_tables()
2788 .map(Table::get_highest_seqno)
2789 .max();
2790 active.max(sealed).max(tables)
2791 };
2792 let entries: Vec<InternalValue> = end_seqno.map_or_else(Vec::new, |cap| {
2793 super_version
2794 .active_memtable
2795 .range_internal(memtable_keys.clone())
2796 .filter(|e| e.key.seqno >= target_seqno && e.key.seqno <= cap)
2797 .collect()
2798 });
2799 let rts = super_version.active_memtable.range_tombstones_sorted();
2800 // Explicit: the gate and the guard must outlive the capture above,
2801 // and writers resume the moment they go.
2802 #[cfg(feature = "std")]
2803 drop(gate);
2804 drop(guard);
2805 (super_version, end_seqno, entries, rts)
2806 };
2807 let version = &super_version.version;
2808 // No entries anywhere ⇒ nothing qualifies, regardless of target.
2809 let Some(end_seqno) = end_seqno else {
2810 return Ok(Vec::new().into_iter());
2811 };
2812
2813 // Events are gathered PER SOURCE, not into one flat list: copies of a
2814 // change across two sources are the same change and collapse, but a
2815 // single source may legitimately hold a byte-identical event more than
2816 // once (a write batch may carry the same merge operand for a key
2817 // twice; both are stored under the batch's shared seqno and both are
2818 // applied on read). See `merge_source_events`.
2819 let mut sources: Vec<Vec<ScanSinceEvent>> = Vec::new();
2820
2821 let in_window = |seqno: SeqNo| seqno >= target_seqno && seqno <= end_seqno;
2822 let range_tombstone_event = |rt: &RangeTombstone| {
2823 in_window(rt.seqno).then(|| ScanSinceEvent::RangeTombstone {
2824 start_key: rt.start.clone(),
2825 end_key: rt.end.clone(),
2826 seqno: rt.seqno,
2827 })
2828 };
2829
2830 // The scope bounds as borrowed slices, for SST key-range pruning.
2831 fn as_ref_bound(b: &Bound<UserKey>) -> Bound<&[u8]> {
2832 match b {
2833 Bound::Included(k) => Bound::Included(k.as_ref()),
2834 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
2835 Bound::Unbounded => Bound::Unbounded,
2836 }
2837 }
2838 let ref_bounds = key_range.map(|(lo, hi)| (as_ref_bound(lo), as_ref_bound(hi)));
2839
2840 // Active memtable — mapped from the frozen capture above, not walked
2841 // again: a second walk would reintroduce exactly the race the freeze
2842 // closed.
2843 let mut source = Vec::new();
2844 for entry in active_entries {
2845 if !in_key_range(&entry.key.user_key) {
2846 continue;
2847 }
2848 source.push(Self::map_event(entry, version, &resolve_indirection)?);
2849 }
2850 for rt in active_range_tombstones {
2851 if rt_in_range(&rt) {
2852 source.extend(range_tombstone_event(&rt));
2853 }
2854 }
2855 sources.push(source);
2856
2857 // Sealed memtables, NEWEST first: the list is kept in seal order, and
2858 // every source below must be older than the one before it, because the
2859 // merge derives replay precedence from that position.
2860 for memtable in super_version.sealed_memtables.iter().rev() {
2861 let mut source = Vec::new();
2862 // The walk is bounded by internal keys; the user bounds decide at
2863 // their own key, whatever version sits there.
2864 for entry in memtable.range_internal(memtable_keys.clone()) {
2865 if in_window(entry.key.seqno) && in_key_range(&entry.key.user_key) {
2866 source.push(Self::map_event(entry, version, &resolve_indirection)?);
2867 }
2868 }
2869 for rt in memtable.range_tombstones_sorted() {
2870 if rt_in_range(&rt) {
2871 source.extend(range_tombstone_event(&rt));
2872 }
2873 }
2874 sources.push(source);
2875 }
2876
2877 // SSTs. A table whose key range cannot intersect the scope is not read
2878 // — the point of the scoped variant. Its tombstones still are: the key
2879 // range does not bound them, since a compaction cuts its outputs'
2880 // tombstones at the next output's first key without widening the
2881 // range up to it.
2882 for table in version.iter_tables() {
2883 let keys_in_scope = ref_bounds.as_ref().is_none_or(|bounds| {
2884 table
2885 .metadata
2886 .key_range
2887 .overlaps_with_bounds_cmp(bounds, cmp.as_ref())
2888 });
2889 if !keys_in_scope {
2890 let mut source = Vec::new();
2891 for rt in table.visible_range_tombstones() {
2892 if rt_in_range(&rt) {
2893 source.extend(range_tombstone_event(&rt));
2894 }
2895 }
2896 if !source.is_empty() {
2897 sources.push(source);
2898 }
2899 continue;
2900 }
2901 // An RT-only table's synthetic weak-tombstone sentinel (the
2902 // writer's `finish`) is deliberately NOT filtered out here. It is
2903 // a real on-disk entry the READ path sees: at a seqno TIE between
2904 // the range deletion and an older source's write at the range's
2905 // start key, the sentinel is what makes the read converge to a
2906 // deletion — so the event stream must carry it too, or a consumer
2907 // replaying the stream keeps a value the tree itself does not
2908 // serve (this stream's one rule is mirroring the tree's reads,
2909 // see `merge_source_events`). It surfaces as the weak-tombstone
2910 // event it is on disk; away from that tie a replayed weak delete
2911 // at the range's start under the range deletion's own seqno is a
2912 // no-op.
2913 let mut source = Vec::new();
2914 let table_keys = ref_bounds.unwrap_or((Bound::Unbounded, Bound::Unbounded));
2915 for entry in table.scan_seqno_range(target_seqno, end_seqno, block_skip, table_keys)? {
2916 if !in_key_range(&entry.key.user_key) {
2917 continue;
2918 }
2919 source.push(Self::map_event(entry, version, &resolve_indirection)?);
2920 }
2921 // Clamped to the view's tight-space restriction: the punched
2922 // prefix's deletions are re-emitted by the slice output that
2923 // superseded it, so the raw list would duplicate those events.
2924 for rt in table.visible_range_tombstones() {
2925 if rt_in_range(&rt) {
2926 source.extend(range_tombstone_event(&rt));
2927 }
2928 }
2929 sources.push(source);
2930 }
2931
2932 Ok(Self::merge_source_events(sources).into_iter())
2933 }
2934
2935 /// Merge per-source event lists into one replay-ordered stream, `sources`
2936 /// ordered NEWEST first.
2937 ///
2938 /// Replay order is increasing seqno, then — for events sharing one seqno —
2939 /// increasing source AGE reversed, so the newest source's event is applied
2940 /// LAST. That matters because two sources can hold different values for one
2941 /// key at one seqno ([`AbstractTree::apply_batch`] takes a caller-chosen
2942 /// seqno and does not require it to be unique), the tree serves the newer
2943 /// one, and a consumer keeps whatever it applies last. Sorting such ties by
2944 /// payload instead would decide precedence by byte order.
2945 ///
2946 /// What happens to byte-identical copies follows ONE rule: the stream must
2947 /// mirror what a read of the same tree does, because a consumer replaying
2948 /// it has to reach the state the tree itself serves.
2949 ///
2950 /// - **Merge operands are all kept.** The read path collects every
2951 /// physically stored operand for a key and applies them in order — it
2952 /// never deduplicates by seqno — so two operands are two applications
2953 /// whether they sit in one source or in two. Seqnos do not disambiguate
2954 /// here: [`AbstractTree::apply_batch`] takes a caller-chosen seqno and
2955 /// does not require it to be unique, and one batch may carry the same
2956 /// operand for a key twice.
2957 /// - **Idempotent events collapse across sources.** A write, a deletion or
2958 /// a range deletion replayed twice reaches the same state, and the read
2959 /// path shadows the copies by seqno rather than compounding them. One
2960 /// committed change can physically live in two published tables — a
2961 /// manifest-loss repair publishes every surviving SST as its own L0 run,
2962 /// including both the inputs and the outputs of a compaction that crashed
2963 /// before deleting its inputs — and delivering it twice would be noise.
2964 /// Repeats WITHIN one source are still kept: they are separate entries
2965 /// the source genuinely holds.
2966 fn merge_source_events(sources: Vec<Vec<ScanSinceEvent>>) -> Vec<ScanSinceEvent> {
2967 // Per source, collapse equal neighbours into runs, each tagged with
2968 // its source's recency (0 = newest) and the position of EVERY copy it
2969 // carries.
2970 //
2971 // The POSITIONS are what keep an order-sensitive merge operator
2972 // correct: a source applies the operands of one batch in the order
2973 // they were added, all at one seqno, and grouping them by payload
2974 // would replay them in a different order — an append or a list push
2975 // then converges somewhere the tree never was. Grouping still SORTS
2976 // (equal events have to meet), so each run remembers where each of its
2977 // members sat: one shared position would fold a repeated operand's
2978 // copies onto its twin's slot and replay `B, A, B` as `A, B, B`.
2979 struct Run {
2980 event: ScanSinceEvent,
2981 recency: usize,
2982 /// Original scan position of every copy, ascending.
2983 positions: Vec<usize>,
2984 }
2985 let mut runs: Vec<Run> = Vec::new();
2986 for (recency, source) in sources.into_iter().enumerate() {
2987 let mut indexed: Vec<(usize, ScanSinceEvent)> =
2988 source.into_iter().enumerate().collect();
2989 indexed.sort_by(|(_, a), (_, b)| ScanSinceEvent::grouping_order(a, b));
2990 let mut iter = indexed.into_iter();
2991 let Some((position, mut current)) = iter.next() else {
2992 continue;
2993 };
2994 let mut positions = alloc::vec![position];
2995 for (index, event) in iter {
2996 if event == current {
2997 positions.push(index);
2998 } else {
2999 positions.sort_unstable();
3000 runs.push(Run {
3001 event: core::mem::replace(&mut current, event),
3002 recency,
3003 positions: core::mem::replace(&mut positions, alloc::vec![index]),
3004 });
3005 }
3006 }
3007 positions.sort_unstable();
3008 runs.push(Run {
3009 event: current,
3010 recency,
3011 positions,
3012 });
3013 }
3014
3015 // Merge the per-source runs. Operands are never collapsed — every copy
3016 // is an application the read path makes, so each keeps ITS source's
3017 // recency and ITS scan position, and the replay order below (oldest
3018 // source first, then position) applies them exactly as the tree does.
3019 // Idempotent events collapse across sources to the count a single
3020 // source holds; the collapsed event keeps the recency of the NEWEST
3021 // source holding it (and that source's positions), which is the slot
3022 // the tree's own precedence gives it.
3023 //
3024 // WEAK tombstones belong to the idempotent class even though a weak
3025 // delete annihilates one put at compaction: its documented contract
3026 // pairs it with a key written at most once, byte-identical copies
3027 // meeting in one compaction stream drain to a single survivor, and a
3028 // consumer cannot materialize multiplicity anyway — replaying the
3029 // same `remove_weak` twice at one seqno lands on ONE internal key in
3030 // its memtable, so a preserved duplicate would replay to the same
3031 // physical state the collapsed stream does.
3032 runs.sort_by(|a, b| ScanSinceEvent::grouping_order(&a.event, &b.event));
3033 let mut merged: Vec<(ScanSinceEvent, usize, usize)> = Vec::new();
3034 let mut iter = runs.into_iter().peekable();
3035 while let Some(run) = iter.next() {
3036 if matches!(run.event, ScanSinceEvent::MergeOperand { .. }) {
3037 // Equal operand runs from OTHER sources follow as their own
3038 // iterations and emit their own copies — no draining here.
3039 for &position in &run.positions {
3040 merged.push((run.event.clone(), run.recency, position));
3041 }
3042 continue;
3043 }
3044 let Run {
3045 event,
3046 mut recency,
3047 mut positions,
3048 } = run;
3049 let mut count = positions.len();
3050 while let Some(other) = iter.next_if(|next| next.event == event) {
3051 debug_assert_eq!(other.event, event, "next_if matched the same event");
3052 count = count.max(other.positions.len());
3053 if other.recency < recency {
3054 recency = other.recency;
3055 positions = other.positions;
3056 }
3057 }
3058 // The winning source may hold fewer copies than the count another
3059 // source did; the extras repeat its last position (they are
3060 // byte-identical, so their relative order carries no information).
3061 while positions.len() < count {
3062 let last = positions.last().copied().unwrap_or_default();
3063 positions.push(last);
3064 }
3065 for &position in positions.iter().take(count) {
3066 merged.push((event.clone(), recency, position));
3067 }
3068 }
3069
3070 // Finally, replay order: seqno, then range deletions, then oldest source
3071 // first, then the position that source gave the event — which is how an
3072 // order-sensitive merge operator converges to what the tree serves.
3073 //
3074 // The range-deletion step comes BEFORE source recency, not after: a tied
3075 // deletion does not suppress the writes it spans (suppression is
3076 // strictly `entry.seqno < tombstone.seqno`), so the tree keeps them, and
3077 // a replay that applied the deletion last would drop them. Ordering by
3078 // recency first would do exactly that whenever the deletion sits in the
3079 // newer source.
3080 merged.sort_by(|(a, a_recency, a_pos), (b, b_recency, b_pos)| {
3081 fn deletion_first(e: &ScanSinceEvent) -> u8 {
3082 u8::from(!matches!(e, ScanSinceEvent::RangeTombstone { .. }))
3083 }
3084 a.seqno()
3085 .cmp(&b.seqno())
3086 .then_with(|| deletion_first(a).cmp(&deletion_first(b)))
3087 .then_with(|| b_recency.cmp(a_recency))
3088 .then_with(|| {
3089 // Within one source and one seqno, a scan yields a key's
3090 // versions NEWEST first, and the read path reverses that run
3091 // to apply them chronologically. Mirror it: same key ⇒ the
3092 // later scan position replays FIRST. Distinct keys at one
3093 // seqno touch different state, so their relative order is
3094 // free — keep it deterministic by scan position.
3095 //
3096 // Identity is the engine's one relation (byte equality, see
3097 // `same_user_key`), which is what the read path this mirrors
3098 // uses to group a key's versions. Asking the comparator here
3099 // would answer the same question — its contract makes
3100 // `Equal` imply byte equality — while letting the two paths
3101 // disagree the moment a comparator broke that.
3102 if crate::comparator::same_user_key(a.key(), b.key()) {
3103 b_pos.cmp(a_pos)
3104 } else {
3105 a_pos.cmp(b_pos)
3106 }
3107 })
3108 });
3109 merged.into_iter().map(|(event, ..)| event).collect()
3110 }
3111
3112 /// Iterate change events with `seqno >= target_seqno`.
3113 ///
3114 /// Returns every change committed at or after `target_seqno` as a stream
3115 /// of [`ScanSinceEvent`]s in increasing seqno order. This is the canonical
3116 /// change-data-capture primitive: a downstream consumer (replica, Kafka
3117 /// connector, Debezium-style pipeline) replays the events in order to
3118 /// reconstruct the source's history. Superseded versions are not collapsed
3119 /// (a key written three times after the target yields three events).
3120 ///
3121 /// # Concurrency
3122 ///
3123 /// The result is a snapshot of the tree as of the call: the active memtable
3124 /// is captured with writers excluded, so a batch committed concurrently is
3125 /// either wholly in the result or wholly absent, never split across it. This
3126 /// holds even for a caller-chosen sequence number at or below the reported
3127 /// watermark, which the seqno bound alone would not exclude. Writers are
3128 /// blocked only while that capture runs — the sealed memtables and SSTs the
3129 /// scan then reads are immutable.
3130 ///
3131 /// # History retention
3132 ///
3133 /// The stream carries what the tree still PHYSICALLY HOLDS. A compaction
3134 /// run with a GC watermark (the `gc_watermark` passed to
3135 /// [`compact`](crate::AbstractTree::compact) /
3136 /// [`major_compact`](crate::AbstractTree::major_compact)) drops shadowed
3137 /// versions and evicted tombstones below that watermark and may fold
3138 /// merge chains and zero bottommost seqnos — history a later scan cannot
3139 /// resurrect. The result is therefore complete only for a `target_seqno`
3140 /// at or above the highest GC watermark ever applied: a deployment
3141 /// replaying this stream (an external-WAL consumer, a CDC replica) must
3142 /// keep its compaction watermark at or below the lowest cursor it may
3143 /// still rewind to, exactly as `docs/external-wal.md` section 4's
3144 /// GC-coordination rules require. The watermark is caller-supplied and
3145 /// not persisted, so this method cannot detect a violation for you.
3146 ///
3147 /// # Block-skip
3148 ///
3149 /// On SSTs written with the `seqno_bounds` section (`seqno_in_index`), data
3150 /// blocks whose bounds cannot overlap the target window are skipped without
3151 /// being read; SSTs without the section are read and filtered per entry, so
3152 /// mixed trees are handled transparently.
3153 ///
3154 /// # KV-separation
3155 ///
3156 /// Standard trees never store blob-indirected values. On the inner tree of
3157 /// a KV-separated (blob) tree this returns an `Err` for indirected entries:
3158 /// blob resolution into [`ScanSinceEvent::Insert`] is provided by the
3159 /// blob-tree scan path, which owns the blob files.
3160 ///
3161 /// # Corruption resilience
3162 ///
3163 /// The per-block seqno-bounds used for skipping live in the optional
3164 /// `seqno_bounds` SST section, a Block covered by XXH3-128 (+ optional Page
3165 /// ECC) and verified when it is loaded at open, plus a decode that rejects
3166 /// non-ascending offsets and inverted bounds, so a corrupted bound is caught
3167 /// rather than trusted. Even in the impossible case of a fault bypassing
3168 /// those checks, a bad bound can only cause a *missed* record, never a wrong
3169 /// one. Callers who want defense against that hypothetical can use
3170 /// [`Self::scan_since_seqno_full_scan`], which reads every block (slower, no
3171 /// skip).
3172 ///
3173 /// # Panics
3174 ///
3175 /// Panics if the internal version-history lock is poisoned.
3176 ///
3177 /// # Errors
3178 ///
3179 /// Returns `Err` if reading the index or a data block fails, or if an entry
3180 /// is a KV-separated value (see above).
3181 pub fn scan_since_seqno(
3182 &self,
3183 target_seqno: SeqNo,
3184 ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
3185 // A standard tree never stores blob-indirected values; the resolver
3186 // errors so an indirected entry (only reachable via a blob tree's inner
3187 // index) surfaces as a clear error rather than a wrong event.
3188 self.scan_since_seqno_with(target_seqno, true, |_version, _entry| {
3189 Err(crate::Error::FeatureUnsupported(
3190 "scan_since_seqno on KV-separated values requires the blob-tree scan path",
3191 ))
3192 })
3193 }
3194
3195 /// Paranoid variant of [`Self::scan_since_seqno`] that disables the
3196 /// per-block seqno-bounds skip: every data block is read and filtered per
3197 /// entry, even on seqno-indexed SSTs.
3198 ///
3199 /// # When to use
3200 ///
3201 /// The fast [`Self::scan_since_seqno`] trusts each block's recorded
3202 /// `[seqno_min, seqno_max]` to skip blocks that cannot hold a qualifying
3203 /// record. Those bounds live in the `seqno_bounds` SST section, a Block
3204 /// covered by XXH3-128 (and optional Page ECC) and verified at open, so
3205 /// on-disk corruption is caught, not silently trusted. This method exists
3206 /// for callers who
3207 /// want defense even against a fault that somehow bypassed those checks: a
3208 /// corrupted `seqno_max` can only ever cause a *missed* record (never a
3209 /// wrong one), and a full scan cannot miss. It is slower (no skip), so
3210 /// prefer [`Self::scan_since_seqno`] unless you specifically need this
3211 /// guarantee.
3212 ///
3213 /// # Panics
3214 ///
3215 /// Panics if the internal version-history lock is poisoned.
3216 ///
3217 /// # Errors
3218 ///
3219 /// Same as [`Self::scan_since_seqno`].
3220 pub fn scan_since_seqno_full_scan(
3221 &self,
3222 target_seqno: SeqNo,
3223 ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
3224 self.scan_since_seqno_with(target_seqno, false, |_version, _entry| {
3225 Err(crate::Error::FeatureUnsupported(
3226 "scan_since_seqno on KV-separated values requires the blob-tree scan path",
3227 ))
3228 })
3229 }
3230
3231 /// Range-scoped variant of [`Self::scan_since_seqno`]: delivers only
3232 /// events whose key falls within `range` (in the tree comparator's
3233 /// order); range-deletion events are delivered when their span OVERLAPS
3234 /// it, since a tombstone reaching into the range affects replay within
3235 /// it. SSTs whose key range cannot intersect the bounds are skipped
3236 /// without a single block read.
3237 ///
3238 /// This is the presence-check primitive for reconciling an external
3239 /// write-ahead log after a repair: [`RepairReport::lost_coverage`] names
3240 /// the affected key ranges, and deciding which retained WAL records to
3241 /// re-apply (in particular, which merge operands SURVIVED and must not be
3242 /// folded twice) only needs the events inside those ranges. See
3243 /// `docs/external-wal.md` § Replay after repair.
3244 ///
3245 /// The history-retention caveat of [`Self::scan_since_seqno`] applies
3246 /// unchanged: the stream is complete only for a `target_seqno` at or
3247 /// above the highest compaction GC watermark ever applied.
3248 ///
3249 /// [`RepairReport::lost_coverage`]: crate::RepairReport::lost_coverage
3250 ///
3251 /// # Panics
3252 ///
3253 /// Panics if the internal version-history lock is poisoned.
3254 ///
3255 /// # Errors
3256 ///
3257 /// Same as [`Self::scan_since_seqno`].
3258 pub fn scan_since_seqno_in_range<K: AsRef<[u8]>, R: RangeBounds<K>>(
3259 &self,
3260 target_seqno: SeqNo,
3261 range: R,
3262 ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<K, R>> {
3263 let bounds = range_to_user_bounds(&range);
3264 self.scan_since_seqno_scoped(
3265 target_seqno,
3266 true,
3267 |_version, _entry| {
3268 Err(crate::Error::FeatureUnsupported(
3269 "scan_since_seqno on KV-separated values requires the blob-tree scan path",
3270 ))
3271 },
3272 Some(&bounds),
3273 )
3274 }
3275
3276 /// Update the live [`crate::runtime_config::RuntimeConfig`].
3277 ///
3278 /// Mutator runs on a clone of the current snapshot; the new snapshot
3279 /// is then atomically swapped in. Subsequent calls to
3280 /// [`Self::runtime_config`] observe the new snapshot.
3281 ///
3282 /// ## Semantics
3283 ///
3284 /// - Write paths (flush, compaction, ingestion) take one snapshot when
3285 /// they start and use it throughout, so a change applies to the next
3286 /// one and never splits a single file between two configurations.
3287 /// - Existing on-disk data remains in its original format and reads
3288 /// transparently: every block, manifest and blob file is
3289 /// self-describing via its own header or descriptor. That includes
3290 /// the compression policies, so changing one rewrites nothing.
3291 /// - Compaction acts as the live-migration mechanism: source blocks
3292 /// are rewritten per the current snapshot over subsequent cycles,
3293 /// so all data converges to the current settings without
3294 /// stop-the-world coordination.
3295 ///
3296 /// ## Concurrency
3297 ///
3298 /// **Reader atomicity:** concurrent readers observe either the old
3299 /// or the new snapshot, never a torn intermediate state.
3300 ///
3301 /// **Writer semantics: last-writer-wins.** Two `update` calls racing
3302 /// from the same starting snapshot will have the second `store`
3303 /// overwrite the first — the first writer's mutation is lost. There
3304 /// is no CAS / RCU merge. Callers that need lost-update avoidance
3305 /// (e.g. two threads concurrently toggling different fields) MUST
3306 /// serialize their `update_runtime_config` calls, typically via a
3307 /// `Mutex` around the call site.
3308 /// # Errors
3309 ///
3310 /// Returns [`crate::Error::PageEccUnsupported`] when the mutator
3311 /// leaves `page_ecc = true` on a binary built without the
3312 /// `page_ecc` cargo feature, and
3313 /// [`crate::Error::ZstdDictMismatch`] when a compression policy names a
3314 /// dictionary the tree does not hold (register it first with
3315 /// `Self::register_zstd_dictionary`). The live snapshot stays at its
3316 /// pre-mutation value on error.
3317 pub fn update_runtime_config<F>(&self, mutator: F) -> crate::Result<()>
3318 where
3319 F: FnOnce(&mut crate::runtime_config::RuntimeConfig),
3320 {
3321 // The mutator is the caller's code and may read the tree, so it runs
3322 // before any lock is taken: under the version lock a read of the
3323 // version history would wait on this thread forever.
3324 let mut next = (*self.0.runtime_config.load_full()).clone();
3325 mutator(&mut next);
3326
3327 // Under the version lock, which a dictionary collection holds for its
3328 // whole pass: checking a policy against the tree's dictionaries and
3329 // publishing it must not interleave with a collection that takes the
3330 // one it names.
3331 #[cfg(zstd_any)]
3332 let version_lock = self.version_history.write();
3333 #[cfg(zstd_any)]
3334 next.check_dictionaries(
3335 self.config.kv_separation_opts.is_some(),
3336 &self.config.current_zstd_dictionaries(),
3337 )?;
3338 // What the swap below replaces, read under the lock every update takes,
3339 // so it is the snapshot a writer may still hold, not the one the
3340 // mutator started from.
3341 #[cfg(zstd_any)]
3342 let replaced = self.0.runtime_config.load_full();
3343 // The heal gate below follows exactly the config THIS call commits,
3344 // rather than a separate `load_full()` that could observe a different
3345 // concurrent update's value. Concurrent `update_runtime_config` calls
3346 // must be serialized by the caller (see the last-writer-wins note
3347 // above); under that contract the gate and the committed config stay in
3348 // sync. On a validation error nothing is committed and `?` returns
3349 // before the gate is touched, so it keeps tracking the unchanged config.
3350 let auto_heal = next.auto_heal;
3351 // Route through the validating handle path so an invalid mutation
3352 // (e.g. `page_ecc = true` on a non-`page_ecc` build) is rejected at
3353 // update time, not silently swallowed at the next manifest write.
3354 self.0.runtime_config.try_update(|c| *c = next)?;
3355
3356 // A writer that took `replaced` before this swap may still be about to
3357 // resolve the dictionary it names; see `retired_write_snapshots`.
3358 #[cfg(zstd_any)]
3359 {
3360 let kv_separated = self.config.kv_separation_opts.is_some();
3361 if replaced.write_dict_ids(kv_separated).next().is_some() {
3362 let mut retired = self.0.retired_write_snapshots.lock();
3363 retired.retain(|snapshot| snapshot.strong_count() > 0);
3364 retired.push(Arc::downgrade(&replaced));
3365 }
3366 drop(replaced);
3367 drop(version_lock);
3368 }
3369 self.0.heal_hints.set_enabled(auto_heal);
3370 // Mirror the insert-time digest gate for the write hot path (see
3371 // `TreeInner::kv_digest_at_insert`). Relaxed: a toggle taking effect
3372 // on the next inserts is the documented contract (mixed inserts are
3373 // supported), so no ordering against other memory is needed.
3374 let gate = inner::kv_digest_at_insert_gate(&self.0.runtime_config.load());
3375 self.0
3376 .kv_digest_at_insert
3377 .store(gate, core::sync::atomic::Ordering::Relaxed);
3378 // Drop the cached admission footprint so the next check re-probes
3379 // disk-free: an operator who just raised the budget (or freed disk)
3380 // should see it promptly, not at the next flush.
3381 *self.0.admission_used_cache.lock() = None;
3382 Ok(())
3383 }
3384
3385 /// Snapshot of the current runtime config. Cheap atomic load —
3386 /// safe to call on hot paths.
3387 #[must_use]
3388 pub fn runtime_config(&self) -> Arc<crate::runtime_config::RuntimeConfig> {
3389 self.0.runtime_config.load_full()
3390 }
3391
3392 /// Shared handle to this tree's ECC heal-hint queue.
3393 ///
3394 /// A read that recovers a block from Page-ECC parity records the owning SST
3395 /// here (when the on-disk fault is confirmed persistent). Pass the handle to
3396 /// [`compaction::EccHeal`](crate::compaction::EccHeal) and run that strategy
3397 /// via [`Tree::compact`](crate::AbstractTree::compact) — leader-only in a
3398 /// clustered deployment — to rewrite the flagged SSTs clean. Check
3399 /// [`HealHints::is_empty`](crate::heal_hints::HealHints::is_empty) to skip
3400 /// the pass when nothing is queued.
3401 ///
3402 /// # Examples
3403 ///
3404 /// ```no_run
3405 /// use lsm_tree::{AbstractTree, AnyTree, Config, SequenceNumberCounter, compaction::EccHeal};
3406 /// use std::sync::Arc;
3407 /// # fn main() -> lsm_tree::Result<()> {
3408 /// let AnyTree::Standard(tree) = Config::new(
3409 /// "/tmp/db",
3410 /// SequenceNumberCounter::default(),
3411 /// SequenceNumberCounter::default(),
3412 /// )
3413 /// .open()?
3414 /// else {
3415 /// return Ok(());
3416 /// };
3417 ///
3418 /// // Opt into rewrite scheduling; reads that recover a block from parity now
3419 /// // flag its SST for healing.
3420 /// tree.update_runtime_config(|c| c.auto_heal = true)?;
3421 ///
3422 /// // Drain the queue, rewriting each flagged SST clean (leader-only in a
3423 /// // clustered deployment).
3424 /// let hints = tree.heal_hints();
3425 /// while !hints.is_empty() {
3426 /// tree.compact(Arc::new(EccHeal::new(tree.heal_hints(), u64::MAX)), 0)?;
3427 /// }
3428 /// # Ok(())
3429 /// # }
3430 /// ```
3431 #[must_use]
3432 pub fn heal_hints(&self) -> Arc<crate::heal_hints::HealHints> {
3433 Arc::clone(&self.0.heal_hints)
3434 }
3435
3436 /// Starts a multi-get of `keys` at `seqno` that its caller drives: the
3437 /// read hands out its block reads and jobs instead of carrying them out,
3438 /// so the calling thread is never parked inside it. It answers what
3439 /// [`AbstractTree::multi_get`] answers at the moment it starts: what is
3440 /// written while it is suspended is not part of its answer, even at a
3441 /// ceiling such as [`SeqNo::MAX`](crate::SeqNo).
3442 ///
3443 /// # Errors
3444 ///
3445 /// Returns [`crate::Error::SnapshotBelowRetention`] when `seqno` is below
3446 /// what the tree retains.
3447 ///
3448 /// # Examples
3449 ///
3450 /// Driving the read on the calling thread:
3451 ///
3452 /// ```
3453 /// use lsm_tree::resumable::Step;
3454 /// use lsm_tree::{AbstractTree, Config, SequenceNumberCounter};
3455 ///
3456 /// # let folder = tempfile::tempdir()?;
3457 /// let tree = Config::new(&folder, SequenceNumberCounter::default(), SequenceNumberCounter::default()).open()?;
3458 /// tree.insert("a", "1", 0);
3459 /// tree.flush_active_memtable(0)?;
3460 ///
3461 /// let mut step = tree.start_multi_get(["a", "b"], 1)?;
3462 /// let values = loop {
3463 /// let mut read = match step {
3464 /// Step::Done(values) => break values?,
3465 /// Step::Pending(read) => read,
3466 /// };
3467 /// for job in read.take_jobs() {
3468 /// let outcome = job.run();
3469 /// read.complete_job(outcome);
3470 /// }
3471 /// for mut block in read.take_reads() {
3472 /// let result = block.file.read_at(&mut block.buf, block.offset).and_then(|n| {
3473 /// if n == block.buf.len() { Ok(()) } else { Err(lsm_tree::io::ErrorKind::UnexpectedEof.into()) }
3474 /// });
3475 /// read.complete_read(block.tag, result, block.buf);
3476 /// }
3477 /// step = read.resume();
3478 /// };
3479 /// assert_eq!(values, [Some("1".as_bytes().into()), None]);
3480 /// # Ok::<(), lsm_tree::Error>(())
3481 /// ```
3482 pub fn start_multi_get<K: Into<UserKey>>(
3483 &self,
3484 keys: impl IntoIterator<Item = K>,
3485 seqno: SeqNo,
3486 ) -> crate::Result<resumable::Step> {
3487 let snapshot = self.snapshot_for_read(seqno)?;
3488 let seqno = Self::read_ceiling(&snapshot, seqno);
3489 let keys: Vec<UserKey> = keys.into_iter().map(Into::into).collect();
3490 // Each key a point lookup, counted once as the read starts, not on
3491 // every resume.
3492 snapshot
3493 .version
3494 .read_mix()
3495 .note_points(u64::try_from(keys.len()).unwrap_or(u64::MAX));
3496 Ok(resumable::ResumableMultiGet::start(read_job::ReadCtx {
3497 super_version: crate::version::SnapshotRef::Owned((*snapshot).clone()),
3498 keys,
3499 seqno,
3500 comparator: Arc::clone(&self.config.comparator),
3501 merge_operator: self.config.merge_operator.clone(),
3502 merge_base: self.blob_source(),
3503 values: read_job::Values::Inline,
3504 metadata_budget: self.config.multi_get_metadata_budget,
3505 }))
3506 }
3507
3508 /// The seqno a read started at `seqno` on `snapshot` reads at for its whole
3509 /// life: at most one past the newest seqno the snapshot holds. The active
3510 /// memtable the snapshot shares keeps taking writes, and a read that is
3511 /// suspended still checks range deletions and merges against it after
3512 /// it started; what lands there at a higher seqno is then not part of its
3513 /// answer, which stays what the tree held when it started.
3514 pub(crate) fn read_ceiling(snapshot: &SuperVersion, seqno: SeqNo) -> SeqNo {
3515 // Past the newest seqno; nothing is newer than the largest one.
3516 let next = |newest: SeqNo| newest.checked_add(1).unwrap_or(SeqNo::MAX);
3517 let memtables = snapshot.active_memtable.get_highest_seqno().max(
3518 snapshot
3519 .sealed_memtables
3520 .iter()
3521 .map(|mt| mt.get_highest_seqno())
3522 .max()
3523 .flatten(),
3524 );
3525 // A ceiling the memtables already reach admits nothing newer: the
3526 // tables need not be looked at.
3527 if memtables.is_some_and(|newest| next(newest) >= seqno) {
3528 return seqno;
3529 }
3530 let tables = snapshot
3531 .version
3532 .iter_tables()
3533 .map(Table::get_highest_seqno)
3534 .max();
3535 // An empty snapshot holds nothing a read could see.
3536 memtables
3537 .max(tables)
3538 .map_or(0, |newest| seqno.min(next(newest)))
3539 }
3540
3541 /// Shared point-read logic for `get()` and `multi_get()`: finds the newest
3542 /// entry, applies merge resolution or RT suppression, and returns the value.
3543 ///
3544 /// `blob_source` is asked for only by a merge, for a base kept in the
3545 /// value log.
3546 fn resolve_or_passthrough(
3547 super_version: &SuperVersion,
3548 key: &[u8],
3549 seqno: SeqNo,
3550 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
3551 comparator: &dyn crate::comparator::UserComparator,
3552 blob_source: impl FnOnce() -> Option<crate::blob_tree::BlobSource>,
3553 ) -> crate::Result<Option<UserValue>> {
3554 let entry = Self::get_value(super_version, key, seqno, comparator)?;
3555
3556 match entry {
3557 Some((ValueType::MergeOperand, entry_seqno, value)) => {
3558 if let Some(merge_op) = merge_operator {
3559 Self::resolve_point_merge(
3560 super_version,
3561 key,
3562 seqno,
3563 merge_op,
3564 blob_source().as_ref(),
3565 )
3566 } else if Self::is_suppressed_by_range_tombstones(
3567 super_version,
3568 key,
3569 entry_seqno,
3570 seqno,
3571 comparator,
3572 ) {
3573 Ok(None)
3574 } else {
3575 Ok(Some(value))
3576 }
3577 }
3578 Some((_, _, value)) => Ok(Some(value)),
3579 None => Ok(None),
3580 }
3581 }
3582
3583 /// Shared post-lookup resolution for `get_pinned` and `multi_get`:
3584 /// tombstone filter, range-tombstone suppression, merge operand resolution.
3585 /// Returns `None` if entry is tombstoned or suppressed.
3586 #[expect(
3587 clippy::too_many_arguments,
3588 reason = "each is a separate input of the one resolution"
3589 )]
3590 fn resolve_pinned_entry(
3591 super_version: &SuperVersion,
3592 key: &[u8],
3593 entry: InternalValue,
3594 seqno: SeqNo,
3595 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
3596 comparator: &dyn crate::comparator::UserComparator,
3597 wrap: impl FnOnce(UserValue) -> crate::PinnableSlice,
3598 blob_source: impl FnOnce() -> Option<crate::blob_tree::BlobSource>,
3599 ) -> crate::Result<Option<crate::PinnableSlice>> {
3600 use crate::PinnableSlice;
3601
3602 let Some(entry) = ignore_tombstone_value(entry) else {
3603 return Ok(None);
3604 };
3605 if Self::is_suppressed_by_range_tombstones(
3606 super_version,
3607 key,
3608 entry.key.seqno,
3609 seqno,
3610 comparator,
3611 ) {
3612 return Ok(None);
3613 }
3614 if entry.key.value_type == ValueType::MergeOperand
3615 && let Some(merge_op) = merge_operator
3616 {
3617 // A merged value is built anew, so it is Owned.
3618 return Self::resolve_point_merge(
3619 super_version,
3620 key,
3621 seqno,
3622 merge_op,
3623 blob_source().as_ref(),
3624 )
3625 .map(|opt| opt.map(PinnableSlice::owned));
3626 }
3627 Ok(Some(wrap(entry.value)))
3628 }
3629
3630 /// Like [`Tree::resolve_or_passthrough`], but returns a [`PinnableSlice`](crate::PinnableSlice)
3631 /// that may keep the decompressed block buffer alive.
3632 fn resolve_or_passthrough_pinned(
3633 super_version: &SuperVersion,
3634 key: &[u8],
3635 seqno: SeqNo,
3636 merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
3637 comparator: &dyn crate::comparator::UserComparator,
3638 blob_source: impl FnOnce() -> Option<crate::blob_tree::BlobSource>,
3639 ) -> crate::Result<Option<crate::PinnableSlice>> {
3640 use crate::PinnableSlice;
3641
3642 // Check memtables first — always Owned
3643 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
3644 return Self::resolve_pinned_entry(
3645 super_version,
3646 key,
3647 entry,
3648 seqno,
3649 merge_operator,
3650 comparator,
3651 PinnableSlice::owned,
3652 blob_source,
3653 );
3654 }
3655
3656 // Sealed memtables — always Owned
3657 if let Some(entry) =
3658 Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
3659 {
3660 return Self::resolve_pinned_entry(
3661 super_version,
3662 key,
3663 entry,
3664 seqno,
3665 merge_operator,
3666 comparator,
3667 PinnableSlice::owned,
3668 blob_source,
3669 );
3670 }
3671
3672 // Tables — Pinned (value shares decompressed block buffer)
3673 let key_hash = crate::hash::hash64(key);
3674
3675 if let Some((entry, block)) = Self::get_internal_entry_with_block_from_tables(
3676 &super_version.version,
3677 key,
3678 seqno,
3679 key_hash,
3680 comparator,
3681 )? {
3682 return Self::resolve_pinned_entry(
3683 super_version,
3684 key,
3685 entry,
3686 seqno,
3687 merge_operator,
3688 comparator,
3689 |value| PinnableSlice::pinned(block, value),
3690 blob_source,
3691 );
3692 }
3693
3694 Ok(None)
3695 }
3696
3697 /// Like [`Tree::get_internal_entry_from_tables`], but returns the block
3698 /// along with the entry for pinned zero-copy access.
3699 fn get_internal_entry_with_block_from_tables(
3700 version: &Version,
3701 key: &[u8],
3702 seqno: SeqNo,
3703 key_hash: u64,
3704 comparator: &dyn crate::comparator::UserComparator,
3705 ) -> crate::Result<Option<(InternalValue, crate::table::Block)>> {
3706 Self::find_in_tables::<TableEntryWithBlock>(version, key, seqno, key_hash, comparator)
3707 }
3708
3709 /// Where this tree's values kept in the value log are read from; only the
3710 /// index of a blob tree has any.
3711 pub(crate) fn blob_source(&self) -> Option<crate::blob_tree::BlobSource> {
3712 self.config
3713 .kv_separation_opts
3714 .is_some()
3715 .then(|| crate::blob_tree::BlobSource {
3716 tree_id: self.id,
3717 cache: Arc::clone(&self.config.cache),
3718 fill: self.runtime_config.load().blob_cache_fill,
3719 #[cfg(feature = "metrics")]
3720 metrics: Arc::clone(&self.0.metrics),
3721 })
3722 }
3723
3724 /// Resolves the merge of `key` for a point read whose newest visible
3725 /// version is an operand: reads the sources newest first and stops at the
3726 /// first base, so a table below it is never opened.
3727 ///
3728 /// `blob_source` reads a base kept in the value log, which only a blob
3729 /// tree's index holds.
3730 pub(crate) fn resolve_point_merge(
3731 super_version: &SuperVersion,
3732 key: &[u8],
3733 seqno: SeqNo,
3734 merge_operator: &Arc<dyn crate::merge_operator::MergeOperator>,
3735 blob_source: Option<&crate::blob_tree::BlobSource>,
3736 ) -> crate::Result<Option<UserValue>> {
3737 Ok(crate::range::resolve_point_merge(
3738 super_version,
3739 key,
3740 seqno,
3741 merge_operator,
3742 blob_source,
3743 )?
3744 .map(|entry| entry.value))
3745 }
3746
3747 #[doc(hidden)]
3748 pub fn create_internal_range<'a, K: AsRef<[u8]> + 'a, R: RangeBounds<K> + 'a>(
3749 version: SuperVersion,
3750 range: &'a R,
3751 seqno: SeqNo,
3752 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
3753 merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
3754 comparator: crate::comparator::SharedComparator,
3755 ) -> impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static {
3756 Self::create_internal_range_with_prefix_hash(
3757 version,
3758 range,
3759 seqno,
3760 ephemeral,
3761 merge_operator,
3762 comparator,
3763 None,
3764 None,
3765 )
3766 }
3767
3768 /// Like [`Tree::create_internal_range`], but with an optional prefix hash
3769 /// for prefix bloom filter skipping during prefix scans, and the
3770 /// `blob_source` a merge reads a base kept in the value log from.
3771 #[doc(hidden)]
3772 #[expect(
3773 clippy::too_many_arguments,
3774 reason = "each is a separate input of the iterator it builds"
3775 )]
3776 pub(crate) fn create_internal_range_with_prefix_hash<
3777 'a,
3778 K: AsRef<[u8]> + 'a,
3779 R: RangeBounds<K> + 'a,
3780 >(
3781 version: SuperVersion,
3782 range: &'a R,
3783 seqno: SeqNo,
3784 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
3785 merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
3786 comparator: crate::comparator::SharedComparator,
3787 prefix_hash: Option<u64>,
3788 blob_source: Option<crate::blob_tree::BlobSource>,
3789 ) -> impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static {
3790 use crate::range::{IterState, TreeIter};
3791 use core::ops::Bound::{self, Excluded, Included, Unbounded};
3792
3793 let lo: Bound<UserKey> = match range.start_bound() {
3794 Included(x) => Included(x.as_ref().into()),
3795 Excluded(x) => Excluded(x.as_ref().into()),
3796 Unbounded => Unbounded,
3797 };
3798
3799 let hi: Bound<UserKey> = match range.end_bound() {
3800 Included(x) => Included(x.as_ref().into()),
3801 Excluded(x) => Excluded(x.as_ref().into()),
3802 Unbounded => Unbounded,
3803 };
3804
3805 let bounds: (Bound<UserKey>, Bound<UserKey>) = (lo, hi);
3806
3807 let iter_state = IterState {
3808 version,
3809 ephemeral,
3810 merge_operator,
3811 blob_source,
3812 comparator,
3813 prefix_hash,
3814 #[cfg(feature = "metrics")]
3815 metrics: None,
3816 };
3817
3818 TreeIter::create_range(iter_state, bounds, seqno)
3819 }
3820
3821 pub(crate) fn get_internal_entry_from_version(
3822 super_version: &SuperVersion,
3823 key: &[u8],
3824 seqno: SeqNo,
3825 comparator: &dyn crate::comparator::UserComparator,
3826 ) -> crate::Result<Option<InternalValue>> {
3827 // Search order: active → sealed → SST (newest first). A point
3828 // tombstone in a newer source is authoritative — no older source
3829 // can contain a newer value, so returning None is correct.
3830 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
3831 let Some(entry) = ignore_tombstone_value(entry) else {
3832 return Ok(None);
3833 };
3834
3835 // Check if any range tombstone suppresses this entry
3836 if Self::is_suppressed_by_range_tombstones(
3837 super_version,
3838 key,
3839 entry.key.seqno,
3840 seqno,
3841 comparator,
3842 ) {
3843 return Ok(None);
3844 }
3845 return Ok(Some(entry));
3846 }
3847
3848 // Now look in sealed memtables
3849 if let Some(entry) =
3850 Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
3851 {
3852 let Some(entry) = ignore_tombstone_value(entry) else {
3853 return Ok(None);
3854 };
3855
3856 if Self::is_suppressed_by_range_tombstones(
3857 super_version,
3858 key,
3859 entry.key.seqno,
3860 seqno,
3861 comparator,
3862 ) {
3863 return Ok(None);
3864 }
3865 return Ok(Some(entry));
3866 }
3867
3868 // Now look in tables... this may involve disk I/O
3869 let entry =
3870 Self::get_internal_entry_from_tables(&super_version.version, key, seqno, comparator)?;
3871
3872 if let Some(entry) = entry {
3873 if Self::is_suppressed_by_range_tombstones(
3874 super_version,
3875 key,
3876 entry.key.seqno,
3877 seqno,
3878 comparator,
3879 ) {
3880 return Ok(None);
3881 }
3882 return Ok(Some(entry));
3883 }
3884
3885 Ok(None)
3886 }
3887
3888 /// Value-only mirror of [`Self::get_internal_entry_from_version`].
3889 ///
3890 /// Returns `(value_type, seqno, value)` for the newest visible entry without
3891 /// reconstructing the entry key. Same search order (active -> sealed -> SST,
3892 /// newest first), tombstone filtering, and range-tombstone suppression; only
3893 /// the SST path differs, using the value-only [`TableValue`] lookup that
3894 /// skips the delta-key fusion of the full `InternalValue` path. Used by the
3895 /// value-returning `get` path, which never reads the matched key.
3896 pub(crate) fn get_value(
3897 super_version: &SuperVersion,
3898 key: &[u8],
3899 seqno: SeqNo,
3900 comparator: &dyn crate::comparator::UserComparator,
3901 ) -> crate::Result<Option<(ValueType, SeqNo, crate::Slice)>> {
3902 if let Some(entry) = super_version.active_memtable.get(key, seqno) {
3903 let Some(entry) = ignore_tombstone_value(entry) else {
3904 return Ok(None);
3905 };
3906 if Self::is_suppressed_by_range_tombstones(
3907 super_version,
3908 key,
3909 entry.key.seqno,
3910 seqno,
3911 comparator,
3912 ) {
3913 return Ok(None);
3914 }
3915 return Ok(Some((entry.key.value_type, entry.key.seqno, entry.value)));
3916 }
3917
3918 if let Some(entry) =
3919 Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
3920 {
3921 let Some(entry) = ignore_tombstone_value(entry) else {
3922 return Ok(None);
3923 };
3924 if Self::is_suppressed_by_range_tombstones(
3925 super_version,
3926 key,
3927 entry.key.seqno,
3928 seqno,
3929 comparator,
3930 ) {
3931 return Ok(None);
3932 }
3933 return Ok(Some((entry.key.value_type, entry.key.seqno, entry.value)));
3934 }
3935
3936 let key_hash = crate::hash::hash64(key);
3937 let entry = Self::find_in_tables::<TableValue>(
3938 &super_version.version,
3939 key,
3940 seqno,
3941 key_hash,
3942 comparator,
3943 )?;
3944 if let Some((value_type, entry_seqno, value)) = entry {
3945 if Self::is_suppressed_by_range_tombstones(
3946 super_version,
3947 key,
3948 entry_seqno,
3949 seqno,
3950 comparator,
3951 ) {
3952 return Ok(None);
3953 }
3954 return Ok(Some((value_type, entry_seqno, value)));
3955 }
3956
3957 Ok(None)
3958 }
3959
3960 /// Checks if a key at `key_seqno` is suppressed by any range tombstone
3961 /// in the active memtable, sealed memtables, or SST tables, visible at `read_seqno`.
3962 pub(crate) fn is_suppressed_by_range_tombstones(
3963 super_version: &SuperVersion,
3964 key: &[u8],
3965 key_seqno: SeqNo,
3966 read_seqno: SeqNo,
3967 comparator: &dyn crate::comparator::UserComparator,
3968 ) -> bool {
3969 // Check active memtable range tombstones.
3970 // Future optimization: skip lock when memtable has no RTs (atomic count).
3971 if super_version
3972 .active_memtable
3973 .is_key_suppressed_by_range_tombstone(key, key_seqno, read_seqno)
3974 {
3975 return true;
3976 }
3977
3978 // Check sealed memtable range tombstones
3979 for mt in super_version.sealed_memtables.iter().rev() {
3980 if mt.is_key_suppressed_by_range_tombstone(key, key_seqno, read_seqno) {
3981 return true;
3982 }
3983 }
3984
3985 // Check SST table range tombstones.
3986 //
3987 // Per-table RT lists are sorted by start key (using comparator) on load,
3988 // so binary search narrows candidates to RTs with start <= key. A
3989 // table's key range does not bound its tombstones (a compaction cuts
3990 // them at the next output's first key without widening the range up
3991 // to it), so the stretch they cover rejects a table instead: from the
3992 // first one's start to the greatest end, two compares.
3993 for table in super_version
3994 .version
3995 .iter_levels()
3996 .flat_map(|lvl| lvl.iter())
3997 .filter(|run| run.holds_range_tombstones())
3998 .flat_map(|run| run.iter())
3999 {
4000 let rts = table.range_tombstones();
4001 let (Some(first), Some(end)) = (rts.first(), table.range_tombstones_end()) else {
4002 continue;
4003 };
4004 if comparator.compare(&first.start, key) == core::cmp::Ordering::Greater
4005 || comparator.compare(key, end) != core::cmp::Ordering::Less
4006 {
4007 continue;
4008 }
4009
4010 // Binary search: find the first RT whose start is > key (in comparator order).
4011 // All RTs before that index have start <= key and are candidates.
4012 let candidate_end = rts.partition_point(|rt| {
4013 comparator.compare(&rt.start, key) != core::cmp::Ordering::Greater
4014 });
4015
4016 for rt in rts.iter().take(candidate_end) {
4017 // start <= key holds by the search; the seqno checks are
4018 // cheaper than the key compare, so they go first.
4019 if key_seqno < rt.seqno
4020 && rt.visible_at(read_seqno)
4021 && comparator.compare(key, &rt.end) == core::cmp::Ordering::Less
4022 {
4023 return true;
4024 }
4025 }
4026 }
4027
4028 false
4029 }
4030
4031 /// De-duplicates equal query keys in a comparator-sorted `remaining` index
4032 /// list, returning the `(key_index, bloom_hash)` pairs for the batched
4033 /// on-disk resolver (which requires strictly-sorted-unique input) and a
4034 /// `(duplicate_index, representative_index)` map. Pair with
4035 /// [`Self::fan_out_duplicates`] after the batch resolves.
4036 ///
4037 /// Shared by [`Self::multi_get`] and the `BlobTree` multi-get so the two
4038 /// cannot silently diverge: forwarding duplicate miss keys into the
4039 /// strictly-sorted-unique resolver was exactly the regression class this
4040 /// guards against. `remaining` must already be sorted by `comparator`.
4041 #[expect(
4042 clippy::indexing_slicing,
4043 reason = "remaining/miss_keys carry batch-local key indices < keys.len()"
4044 )]
4045 pub(crate) fn dedup_sorted_miss_keys<K: AsRef<[u8]>>(
4046 remaining: &[usize],
4047 keys: &[K],
4048 comparator: &dyn crate::comparator::UserComparator,
4049 ) -> DedupedMissKeys {
4050 let mut miss_keys: Vec<(usize, u64)> = Vec::with_capacity(remaining.len());
4051 let mut duplicates: Vec<(usize, usize)> = Vec::new();
4052 for &idx in remaining {
4053 let key = keys[idx].as_ref();
4054 match miss_keys.last() {
4055 Some(&(rep_idx, _))
4056 if comparator.compare(keys[rep_idx].as_ref(), key)
4057 == core::cmp::Ordering::Equal =>
4058 {
4059 duplicates.push((idx, rep_idx));
4060 }
4061 _ => miss_keys.push((idx, crate::hash::hash64(key))),
4062 }
4063 }
4064 (miss_keys, duplicates)
4065 }
4066
4067 /// Fans each representative's answer out to its duplicate positions, so
4068 /// every input slot carries the same answer the per-key path would have
4069 /// produced. Counterpart to [`Self::dedup_sorted_miss_keys`]; call once the
4070 /// representatives' answers are in `answers`.
4071 #[expect(
4072 clippy::indexing_slicing,
4073 reason = "duplicate/representative indices are batch-local key indices < answers.len()"
4074 )]
4075 pub(crate) fn fan_out_duplicates<T: Clone>(
4076 duplicates: &[(usize, usize)],
4077 answers: &mut [Option<T>],
4078 ) {
4079 for &(dup_idx, rep_idx) in duplicates {
4080 let resolved = answers[rep_idx].clone();
4081 answers[dup_idx] = resolved;
4082 }
4083 }
4084
4085 /// Resolves the keys of `remaining` (sorted under `comparator`) against
4086 /// `level` key by key, run by run, and returns the versions found by key
4087 /// index and the keys left for the levels below, still sorted.
4088 ///
4089 /// On level 0, and on a deeper level of several runs, every run is
4090 /// checked and the highest seqno per key kept; a key found at the read's
4091 /// ceiling skips the later runs, which cannot beat it. On a level of one
4092 /// run, the table covering a key answers it for the whole level.
4093 ///
4094 /// # Errors
4095 ///
4096 /// The first read of a table that fails.
4097 #[expect(
4098 clippy::indexing_slicing,
4099 reason = "remaining's indices are valid key indices, and `found` and `at_ceiling` are as long as `keys`"
4100 )]
4101 fn resolve_level_serially<K: AsRef<[u8]>>(
4102 level_zero: bool,
4103 level: &crate::version::Level,
4104 mut remaining: Vec<(usize, u64)>,
4105 keys: &[K],
4106 seqno: SeqNo,
4107 comparator: &dyn crate::comparator::UserComparator,
4108 ) -> crate::Result<tables_read::SerialLevel> {
4109 // A deeper level a tiered layout leaves overlapping runs in is read as
4110 // L0 is: a key's covering table in one run may miss it while an older
4111 // run holds it.
4112 if level_zero || level.run_count() > 1 {
4113 // L0: must check ALL runs, keep highest seqno per key. Track keys
4114 // at the seqno ceiling (seqno + 1 == read_seqno): no other L0 run
4115 // can beat them, so skip them in subsequent runs. Both are dense
4116 // over 0..keys.len().
4117 let mut found: Vec<Option<InternalValue>> = vec![None; keys.len()];
4118 let mut at_ceiling = vec![false; keys.len()];
4119
4120 for run in level.iter() {
4121 // `at_ceiling` is read as this run's skip set (a key is visited
4122 // once per run, so the updates below only affect later runs)
4123 // and mutated from the returned outcomes: never both at once.
4124 let resolved =
4125 Self::resolve_run_batched(run, &remaining, keys, seqno, comparator, |idx| {
4126 at_ceiling[idx]
4127 })?;
4128 for (idx, _hash, item) in resolved.covered {
4129 let Some(item) = item else { continue };
4130 match &found[idx] {
4131 Some(current) if current.key.seqno >= item.key.seqno => {}
4132 _ => {
4133 if item.key.seqno.checked_add(1) == Some(seqno) {
4134 at_ceiling[idx] = true;
4135 }
4136 found[idx] = Some(item);
4137 }
4138 }
4139 }
4140 // Uncovered keys stay in `remaining`; the retain below prunes
4141 // the ones any run resolved.
4142 }
4143
4144 // Remove found keys (both values and tombstones)
4145 remaining.retain(|&(idx, _)| found[idx].is_none());
4146 let found = found
4147 .into_iter()
4148 .enumerate()
4149 .filter_map(|(idx, item)| Some((idx, item?)))
4150 .collect();
4151 return Ok((found, remaining));
4152 }
4153
4154 // L1+ runs have non-overlapping key ranges within a level. A covering
4155 // run resolves a key definitively: a hit is found, a covering miss
4156 // drops it to lower levels (`covered_miss`), and an uncovered key
4157 // tries the next run in this level (`not_covered`).
4158 let mut found: Vec<(usize, InternalValue)> = Vec::new();
4159 let mut covered_miss: Vec<(usize, u64)> = Vec::new();
4160
4161 for run in level.iter() {
4162 let resolved =
4163 Self::resolve_run_batched(run, &remaining, keys, seqno, comparator, |_| false)?;
4164 for (idx, hash, item) in resolved.covered {
4165 if let Some(item) = item {
4166 found.push((idx, item));
4167 } else {
4168 // Covering run found, key absent: no other run in this
4169 // level can have it. Keep for lower levels.
4170 covered_miss.push((idx, hash));
4171 }
4172 }
4173 remaining = resolved.not_covered;
4174 }
4175
4176 // Merge back: keys without a covering run + keys with a covering miss
4177 // both proceed to lower levels. Re-sort to preserve comparator order
4178 // for the next level's sequential scan.
4179 let needs_sort = !covered_miss.is_empty();
4180 remaining.extend(covered_miss);
4181 if needs_sort {
4182 remaining
4183 .sort_by(|&(a, _), &(b, _)| comparator.compare(keys[a].as_ref(), keys[b].as_ref()));
4184 }
4185 Ok((found, remaining))
4186 }
4187
4188 /// Resolves `remaining` (sorted ascending under `comparator`) against a
4189 /// single run with per-table batched gets instead of a per-key `table.get`:
4190 /// consecutive keys covered by the same table within the run share one
4191 /// [`Table::batch_get`], so co-located keys decode their data block once.
4192 /// Byte-identical to per-key resolution (the same point reads, the same
4193 /// values). `skip(idx)` omits a key (e.g. one already pinned at the L0 seqno
4194 /// ceiling, where no later run can beat it).
4195 ///
4196 /// Returns, per covered non-skipped key, `(idx, hash, resolved item)` in
4197 /// input order, plus the keys this run does not cover (also in input order)
4198 /// for the caller to pass to the next run or level.
4199 #[expect(
4200 clippy::indexing_slicing,
4201 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)"
4202 )]
4203 fn resolve_run_batched<K: AsRef<[u8]>>(
4204 run: &crate::version::Run<crate::Table>,
4205 remaining: &[(usize, u64)],
4206 keys: &[K],
4207 seqno: SeqNo,
4208 comparator: &dyn crate::comparator::UserComparator,
4209 skip: impl Fn(usize) -> bool,
4210 ) -> crate::Result<RunResolve> {
4211 let mut covered: Vec<CoveredKey> = Vec::new();
4212 let mut not_covered: Vec<(usize, u64)> = Vec::new();
4213
4214 // One pair of buffers reused across the run's tables, cleared per
4215 // table. Each is consumed before the next table fills it, so a fresh
4216 // Vec per table would only re-allocate the capacity the previous one
4217 // just released — once per table on a batched read path.
4218 let mut batch: Vec<(&[u8], u64)> = Vec::new();
4219 let mut batch_keys: Vec<(usize, u64)> = Vec::new();
4220
4221 let mut i = 0;
4222 while i < remaining.len() {
4223 let (idx, hash) = remaining[i];
4224 if skip(idx) {
4225 i += 1;
4226 continue;
4227 }
4228 let key = keys[idx].as_ref();
4229 let Some(table) = run.get_for_key_cmp(key, comparator) else {
4230 not_covered.push((idx, hash));
4231 i += 1;
4232 continue;
4233 };
4234
4235 // Gather the contiguous, non-skipped keys covered by THIS table. The
4236 // input is sorted and a run's tables partition the key space, so the
4237 // keys for one table form a contiguous slice; one `batch_get` drains
4238 // them with a single block decode for co-located keys.
4239 let table_id = table.id();
4240 batch.clear();
4241 batch_keys.clear();
4242 while i < remaining.len() {
4243 let (jdx, jhash) = remaining[i];
4244 if skip(jdx) {
4245 i += 1;
4246 continue;
4247 }
4248 let jkey = keys[jdx].as_ref();
4249 match run.get_for_key_cmp(jkey, comparator) {
4250 Some(t) if t.id() == table_id => {
4251 batch.push((jkey, jhash));
4252 batch_keys.push((jdx, jhash));
4253 i += 1;
4254 }
4255 _ => break,
4256 }
4257 }
4258
4259 // `drain`, not `into_iter`: the buffer is reused by the next table,
4260 // so its capacity has to survive the walk. The lint assumes the
4261 // vector is dead afterwards, which is exactly what this is not.
4262 #[expect(
4263 clippy::iter_with_drain,
4264 reason = "buffer is reused across tables; into_iter would consume it"
4265 )]
4266 for ((kidx, khash), item) in batch_keys.drain(..).zip(table.batch_get(&batch, seqno)?) {
4267 covered.push((kidx, khash, item));
4268 }
4269 }
4270
4271 Ok(RunResolve {
4272 covered,
4273 not_covered,
4274 })
4275 }
4276
4277 /// The most of these `tables` whose files a level read holds open at
4278 /// once: what the smallest descriptor cache among them keeps, unbounded
4279 /// when their descriptors are pinned and open anyway.
4280 fn open_cap<'t>(tables: impl Iterator<Item = &'t Table>) -> usize {
4281 tables
4282 .filter_map(Table::descriptor_capacity)
4283 .min()
4284 .map_or(usize::MAX, |cap| {
4285 usize::try_from(cap).unwrap_or(usize::MAX).max(1)
4286 })
4287 }
4288
4289 /// Reads the keys of `ctx` on the calling thread, through the memtables
4290 /// and the levels of its version, and turns each key's newest version
4291 /// into its value as its value source says: the multi-get of a standard
4292 /// tree and of a blob tree alike.
4293 ///
4294 /// # Errors
4295 ///
4296 /// The first failure of a level resolved key by key, or of the lowest
4297 /// key whose merge or blob read failed.
4298 pub(crate) fn read_many<K: AsRef<[u8]>>(
4299 ctx: &read_job::ReadCtx<K>,
4300 ) -> crate::Result<Vec<Option<UserValue>>> {
4301 // Each key of the batch is a point lookup of the read mix.
4302 ctx.super_version
4303 .version
4304 .read_mix()
4305 .note_points(u64::try_from(ctx.keys.len()).unwrap_or(u64::MAX));
4306 let mut read = multi_get_read::MultiGetRead::new(ctx);
4307 Self::drive(&mut read, ctx)
4308 }
4309
4310 /// Drives a read of `ctx` to its end on the calling thread, and returns
4311 /// its answer. A block read goes to the read queue of the backend its
4312 /// table was opened through as it is asked for, and the queues hold the
4313 /// reads of a pass until the driver waits on them, so they go out
4314 /// together; jobs are run after the pass that asked for them, and the read
4315 /// moves on again before any wait. A read is handed back the moment it is
4316 /// in, while others may still be in flight: no table waits on the slowest
4317 /// file of the level.
4318 fn drive<'a, K: AsRef<[u8]>, M: read_job::ReadMachine<'a>>(
4319 level: &mut M,
4320 ctx: &read_job::ReadCtx<K>,
4321 ) -> M::Output {
4322 let mut sink = DriveSink {
4323 queues: Vec::new(),
4324 #[cfg(feature = "std")]
4325 wake: Arc::new(LevelWake {
4326 generation: core::sync::atomic::AtomicU64::new(0),
4327 sleeping: core::sync::atomic::AtomicBool::new(false),
4328 thread: std::thread::current(),
4329 }),
4330 jobs: Vec::new(),
4331 cache: None,
4332 };
4333 // The jobs being run; those their results ask for collect in the
4334 // sink meanwhile.
4335 let mut running: Vec<read_job::Job> = Vec::new();
4336
4337 loop {
4338 if let Some(answer) = level.pump(&mut sink) {
4339 return answer;
4340 }
4341 if !sink.jobs.is_empty() {
4342 core::mem::swap(&mut running, &mut sink.jobs);
4343 #[expect(
4344 clippy::iter_with_drain,
4345 reason = "the two job lists trade places on every pass and keep their allocations; into_iter would consume one"
4346 )]
4347 for job in running.drain(..) {
4348 level.job_done(job.run(ctx), &mut sink);
4349 }
4350 continue;
4351 }
4352
4353 // A block is handed to its read the moment it is back.
4354 let mut on_done = |done: crate::fs::ReadDone| level.read_done(done);
4355 let cache = sink.cache;
4356 let queues = &mut sink.queues;
4357 #[cfg(feature = "std")]
4358 let wake = &sink.wake;
4359 // What has finished on any backend, without waiting. The wake's
4360 // generation is taken first, so a read back after this look is
4361 // seen by the sleep below.
4362 #[cfg(feature = "std")]
4363 let seen = wake.generation.load(core::sync::atomic::Ordering::SeqCst);
4364 let mut handed = 0usize;
4365 for q in queues.iter_mut() {
4366 q.queue.wait(0, &mut |done| {
4367 handed += 1;
4368 on_done(done);
4369 });
4370 }
4371 if handed > 0 {
4372 continue;
4373 }
4374 // Nothing is back. One backend still reading is waited on itself.
4375 // A level whose tables sit on several (after a route change, until
4376 // compaction moves them) sleeps until any of them has a read back,
4377 // so a fast backend's tables move on while a slow one is still
4378 // reading. A queue that cannot wake the level, or holds reads only
4379 // a wait carries out, is waited on itself.
4380 let must_wait = |q: &LevelQueue<'_>| !q.wakes || q.queue.held() > 0;
4381 let mut reading = queues.iter_mut().filter(|q| q.queue.outstanding() > 0);
4382 let Some(first) = reading.next() else {
4383 // Nothing in flight: the level moves on from what is back.
4384 continue;
4385 };
4386 let second = reading.next();
4387 if second.is_none() || must_wait(first) {
4388 Self::timed_wait(first.queue.as_mut(), cache, &mut on_done);
4389 continue;
4390 }
4391 if let Some(q) = second.into_iter().chain(reading).find(|q| must_wait(q)) {
4392 Self::timed_wait(q.queue.as_mut(), cache, &mut on_done);
4393 continue;
4394 }
4395 // Every queue still reading wakes the level, which only a queue
4396 // given the wake under `std` does.
4397 #[cfg(feature = "std")]
4398 {
4399 wake.sleeping
4400 .store(true, core::sync::atomic::Ordering::SeqCst);
4401 while wake.generation.load(core::sync::atomic::Ordering::SeqCst) == seen {
4402 std::thread::park();
4403 }
4404 wake.sleeping
4405 .store(false, core::sync::atomic::Ordering::SeqCst);
4406 }
4407 }
4408 }
4409
4410 /// Waits on `queue` for at least one read, handing each to `on_done`,
4411 /// and folds a sample of such waits into `cache`'s load-time estimate as
4412 /// the wait's length shared among the reads it handed back: what each
4413 /// read cost the storage, whether the queue reads them one after another
4414 /// or overlaps them in flight.
4415 fn timed_wait(
4416 queue: &mut dyn crate::fs::ReadQueue,
4417 cache: Option<&crate::cache::Cache>,
4418 on_done: &mut dyn FnMut(crate::fs::ReadDone),
4419 ) {
4420 let timer = crate::cache::Cache::time_load();
4421 let mut reads = 0usize;
4422 queue.wait(1, &mut |done| {
4423 reads += 1;
4424 on_done(done);
4425 });
4426 if let Some(cache) = cache {
4427 cache.record_batch_wait(timer, reads);
4428 }
4429 }
4430
4431 /// Resolves the current version's level `level_idx` from its staged plan
4432 /// for `keys` on the calling thread, with the default comparator and
4433 /// metadata budget: `true` when it answered the level (results updated,
4434 /// found keys dropped from `still_remaining`), `false` when it hands the
4435 /// level to the serial resolve.
4436 #[cfg(test)]
4437 fn resolve_level_staged<K: AsRef<[u8]>>(
4438 &self,
4439 level_idx: usize,
4440 still_remaining: &mut Vec<(usize, u64)>,
4441 keys: Vec<K>,
4442 results: &mut [Option<InternalValue>],
4443 ) -> crate::Result<bool> {
4444 /// One level's resolve, answering into the caller's `results`.
4445 struct OneLevel<'a, 'r, K> {
4446 level: level_resolve::LevelResolve<'a, K>,
4447 results: &'r mut [Option<InternalValue>],
4448 }
4449
4450 impl<'a, K: AsRef<[u8]>> read_job::ReadMachine<'a> for OneLevel<'a, '_, K> {
4451 type Output = bool;
4452
4453 fn pump<S: read_job::ReadSink<'a>>(&mut self, out: &mut S) -> Option<bool> {
4454 match self.level.pump(self.results, out) {
4455 level_resolve::LevelStep::Pending => None,
4456 level_resolve::LevelStep::Resolved => Some(true),
4457 level_resolve::LevelStep::Serial => Some(false),
4458 }
4459 }
4460
4461 fn job_done<S: read_job::ReadSink<'a>>(
4462 &mut self,
4463 done: read_job::JobDone,
4464 out: &mut S,
4465 ) {
4466 self.level.job_done(done, out);
4467 }
4468
4469 fn read_done(&mut self, done: crate::fs::ReadDone) {
4470 self.level.read_done(done);
4471 }
4472 }
4473
4474 let ctx = read_job::ReadCtx {
4475 super_version: self.snapshot_for_read(SeqNo::MAX)?,
4476 keys,
4477 seqno: SeqNo::MAX,
4478 comparator: crate::comparator::default_comparator(),
4479 merge_operator: None,
4480 merge_base: None,
4481 values: read_job::Values::Inline,
4482 metadata_budget: crate::config::DEFAULT_MULTI_GET_METADATA_BUDGET,
4483 };
4484 let Some(level) = ctx.version().level(level_idx) else {
4485 return Ok(true);
4486 };
4487 let mut one = OneLevel {
4488 level: level_resolve::LevelResolve::new(
4489 level,
4490 level_idx,
4491 core::mem::take(still_remaining),
4492 &ctx.keys,
4493 ctx.comparator.as_ref(),
4494 ctx.seqno,
4495 ctx.metadata_budget,
4496 ),
4497 results,
4498 };
4499 let resolved = Self::drive(&mut one, &ctx);
4500 *still_remaining = one.level.into_remaining();
4501 Ok(resolved)
4502 }
4503
4504 /// Counts, once per (table, key), the keys a table's filter let through
4505 /// that no read of that table found: a filter query and, where the read
4506 /// saw every version, a false positive, as a point read counts them.
4507 #[expect(
4508 clippy::indexing_slicing,
4509 reason = "task indices come from enumerating `tasks`"
4510 )]
4511 fn count_chunked_false_positives(
4512 tasks: &[BlockTask<'_>],
4513 mut found: Vec<(crate::TableId, usize)>,
4514 ) {
4515 found.sort_unstable();
4516 found.dedup();
4517 let mut missed: Vec<(crate::TableId, usize, usize)> = tasks
4518 .iter()
4519 .enumerate()
4520 .filter(|(_, task)| task.table.counts_filter_misses())
4521 .flat_map(|(at, task)| {
4522 task.keys
4523 .iter()
4524 .map(move |&(key, _)| (task.table.id(), key, at))
4525 })
4526 .filter(|&(table, key, _)| found.binary_search(&(table, key)).is_err())
4527 .collect();
4528 missed.sort_unstable_by_key(|&(table, key, _)| (table, key));
4529 missed.dedup_by_key(|&mut (table, key, _)| (table, key));
4530 for (_, _, at) in missed {
4531 let task = &tasks[at];
4532 task.table.count_filter_miss(task.table_seqno);
4533 }
4534 }
4535
4536 /// Decodes the block of task `index` from `bytes`, keeping it in the cache
4537 /// when it fits in `keep_room` and handing it to the reads waiting on
4538 /// `lead`, and point-reads its keys, adding each hit to `hits` as
4539 /// `(task index, key index, entry)`.
4540 fn resolve_block_task<K: AsRef<[u8]>>(
4541 task: &BlockTask<'_>,
4542 index: usize,
4543 bytes: &[u8],
4544 keep_room: &mut u64,
4545 lead: Option<crate::cache::BlockLead>,
4546 keys: &[K],
4547 hits: &mut Vec<(usize, usize, InternalValue)>,
4548 ) -> crate::Result<()> {
4549 let block = task
4550 .table
4551 .decode_data_block_keeping(bytes, &task.handle, keep_room, lead)?;
4552 Self::read_task_keys(task, index, &block, keys, hits)
4553 }
4554
4555 /// Point-reads the keys of task `index` in its data `block`, adding each
4556 /// hit to `hits` as `(task index, key index, entry)`.
4557 #[expect(
4558 clippy::indexing_slicing,
4559 reason = "the task's key indices are valid (caller's keys aligned)"
4560 )]
4561 fn read_task_keys<K: AsRef<[u8]>>(
4562 task: &BlockTask<'_>,
4563 index: usize,
4564 block: &crate::table::DataBlock,
4565 keys: &[K],
4566 hits: &mut Vec<(usize, usize, InternalValue)>,
4567 ) -> crate::Result<()> {
4568 for &(kidx, key_hash) in &task.keys {
4569 if let Some(item) = task.table.point_read_translated(
4570 block,
4571 keys[kidx].as_ref(),
4572 key_hash,
4573 task.table_seqno,
4574 )? {
4575 hits.push((index, kidx, item));
4576 }
4577 }
4578 Ok(())
4579 }
4580
4581 /// Keeps the higher-seqno of an existing result and a new candidate (the L0
4582 /// newest-version-wins merge; correct for L1+ too, where each key has one
4583 /// candidate).
4584 #[expect(
4585 clippy::indexing_slicing,
4586 reason = "idx is a valid key index aligned with results"
4587 )]
4588 fn keep_highest(results: &mut [Option<InternalValue>], idx: usize, item: InternalValue) {
4589 match &results[idx] {
4590 Some(current) if current.key.seqno >= item.key.seqno => {}
4591 _ => results[idx] = Some(item),
4592 }
4593 }
4594
4595 fn get_internal_entry_from_tables(
4596 version: &Version,
4597 key: &[u8],
4598 seqno: SeqNo,
4599 comparator: &dyn crate::comparator::UserComparator,
4600 ) -> crate::Result<Option<InternalValue>> {
4601 let key_hash = crate::hash::hash64(key);
4602 Self::find_in_tables::<TableEntry>(version, key, seqno, key_hash, comparator)
4603 }
4604
4605 /// Generic level-walk for point reads, monomorphized over the lookup result type.
4606 ///
4607 /// A level of several runs (always L0, and a deeper level a tiered
4608 /// layout leaves overlapping runs in): check ALL runs, keep highest seqno.
4609 /// A level of one run: at most one table covers the key.
4610 /// Once a level yields a match, lower levels cannot have newer data.
4611 fn find_in_tables<T: TablePointLookup>(
4612 version: &Version,
4613 key: &[u8],
4614 seqno: SeqNo,
4615 key_hash: u64,
4616 comparator: &dyn crate::comparator::UserComparator,
4617 ) -> crate::Result<Option<T>> {
4618 for (level_idx, level) in version.iter_levels().enumerate() {
4619 if level_idx == 0 || level.run_count() > 1 {
4620 let mut best: Option<T> = None;
4621
4622 for run in level.iter() {
4623 if let Some(table) = run.get_for_key_cmp(key, comparator)
4624 && let Some(item) = T::lookup(table, key, seqno, key_hash)?
4625 {
4626 match &best {
4627 Some(current) if current.entry_seqno() >= item.entry_seqno() => {}
4628 _ => {
4629 // Short-circuit: point reads use exclusive upper bound,
4630 // so the highest visible seqno is read_seqno - 1.
4631 // If matched, no other L0 run can have a higher one.
4632 if item.entry_seqno().checked_add(1) == Some(seqno) {
4633 return Ok(item.filter_tombstone());
4634 }
4635 best = Some(item);
4636 }
4637 }
4638 }
4639 }
4640
4641 if let Some(entry) = best {
4642 return Ok(entry.filter_tombstone());
4643 }
4644 } else if let Some(table) = level
4645 .iter()
4646 .next()
4647 .and_then(|run| run.get_for_key_cmp(key, comparator))
4648 && let Some(item) = T::lookup(table, key, seqno, key_hash)?
4649 {
4650 // One run: the table covering the key answers for the level.
4651 return Ok(item.filter_tombstone());
4652 }
4653 }
4654
4655 Ok(None)
4656 }
4657
4658 pub(crate) fn get_internal_entry_from_sealed_memtables(
4659 super_version: &SuperVersion,
4660 key: &[u8],
4661 seqno: SeqNo,
4662 ) -> Option<InternalValue> {
4663 for mt in super_version.sealed_memtables.iter().rev() {
4664 if let Some(entry) = mt.get(key, seqno) {
4665 return Some(entry);
4666 }
4667 }
4668
4669 None
4670 }
4671
4672 /// Resolves the super-version serving snapshot `seqno`, as an owned clone
4673 /// a caller can keep for as long as it reads (an iterator, a scan).
4674 ///
4675 /// Every snapshot above the retention floor is served by the current
4676 /// version (see
4677 /// [`SuperVersions::get_version_for_snapshot`](crate::version::SuperVersions::get_version_for_snapshot)),
4678 /// so under `std` it is cloned from the lock-free mirror rather than out of
4679 /// the locked history. The clone is the reader's hold: it keeps the
4680 /// version's tables on disk until the reader drops it, however many
4681 /// installs happen in the meantime.
4682 ///
4683 /// # Errors
4684 ///
4685 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4686 /// when `0 < seqno <= floor`.
4687 #[cfg(feature = "std")]
4688 pub(crate) fn get_version_for_snapshot(&self, seqno: SeqNo) -> crate::Result<SuperVersion> {
4689 let current = self.latest_super_version.load();
4690 current.check_serves(seqno)?;
4691 Ok(SuperVersion::clone(¤t))
4692 }
4693
4694 /// Resolves the super-version serving snapshot `seqno`; see the `std`
4695 /// build's twin. Without the mirror it clones out of the locked history.
4696 ///
4697 /// # Errors
4698 ///
4699 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4700 /// when `0 < seqno <= floor`.
4701 #[cfg(not(feature = "std"))]
4702 pub(crate) fn get_version_for_snapshot(&self, seqno: SeqNo) -> crate::Result<SuperVersion> {
4703 self.version_history.read().get_version_for_snapshot(seqno)
4704 }
4705
4706 /// Unflushed memtable bytes the memtable backpressure axis compares: the
4707 /// total of the shared write-buffer budget when the tree joined one, else
4708 /// the tree's own active and sealed memtables.
4709 #[cfg(feature = "std")]
4710 pub(crate) fn unflushed_memtable_bytes(&self) -> u64 {
4711 match &self.config.write_buffer_budget {
4712 Some(budget) => budget.unflushed_bytes(),
4713 None => self.latest_super_version.load().unflushed_bytes(),
4714 }
4715 }
4716
4717 /// The tree's own unflushed memtable bytes; see the `std` build's twin.
4718 #[cfg(not(feature = "std"))]
4719 pub(crate) fn unflushed_memtable_bytes(&self) -> u64 {
4720 self.version_history
4721 .read()
4722 .latest_version_ref()
4723 .unflushed_bytes()
4724 }
4725
4726 /// The snapshot for one point read, without a clone.
4727 ///
4728 /// Every snapshot above the retention floor is served by the current
4729 /// version, so under `std` a point read never takes the history `RwLock`:
4730 /// it loads the mirrored current [`SuperVersion`], checks the floor it
4731 /// carries, and reads under the load guard. Recent inserts stay visible
4732 /// because they mutate the shared `active_memtable` behind a stable Arc;
4733 /// the current version only changes on rotation, flush and compaction,
4734 /// which refresh the mirror under the write lock.
4735 ///
4736 /// What a caller gets, and may assume: the memtables and tables of ONE
4737 /// version, the current one. Nothing here hides a compaction from a read
4738 /// the floor admits: a snapshot below an install is answered from the
4739 /// install's output, which holds every key version that snapshot reads.
4740 ///
4741 /// Point reads only: a guard held across a long scan would delay the
4742 /// mirror's writers, so iterators keep their own clones (see
4743 /// [`get_version_for_snapshot`](Self::get_version_for_snapshot)). no-std has
4744 /// no mirror (`arc-swap` is std-only) and clones out of the locked history.
4745 ///
4746 /// # Errors
4747 ///
4748 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4749 /// when `0 < seqno <= floor`.
4750 #[inline]
4751 pub(crate) fn snapshot_for_read(
4752 &self,
4753 seqno: SeqNo,
4754 ) -> crate::Result<crate::version::SnapshotRef> {
4755 #[cfg(feature = "std")]
4756 {
4757 let current = self.latest_super_version.load();
4758 current.check_serves(seqno)?;
4759 Ok(crate::version::SnapshotRef::Latest(current))
4760 }
4761 #[cfg(not(feature = "std"))]
4762 {
4763 self.version_history
4764 .read()
4765 .get_version_for_snapshot(seqno)
4766 .map(crate::version::SnapshotRef::Owned)
4767 }
4768 }
4769
4770 /// Opens an LSM-tree in the given directory.
4771 ///
4772 /// Will recover previous state if the folder was previously
4773 /// occupied by an LSM-tree, including the previous configuration.
4774 /// If not, a new tree will be initialized with the given config.
4775 ///
4776 /// After recovering a previous state, use `Tree::set_active_memtable`
4777 /// to fill the memtable with data from a write-ahead log for full durability.
4778 ///
4779 /// # Errors
4780 ///
4781 /// Returns error, if an IO error occurred.
4782 ///
4783 /// `lock` says how the open comes by the directory lock: a caller that
4784 /// wrote the tree's files under it passes it in, so no other opener gets
4785 /// in between.
4786 ///
4787 /// `names` says who syncs the folders that gain an entry when the open
4788 /// makes the tree's folder.
4789 ///
4790 /// Returns the tree, and when this open made it rather than recovering
4791 /// one, what takes it back.
4792 pub(crate) fn open(
4793 config: Config,
4794 #[cfg(feature = "std")] lock: crate::config::DirectoryLock,
4795 names: NewFolderNames,
4796 ) -> crate::Result<(Self, Option<Creation>)> {
4797 log::debug!("Opening LSM-tree at {}", config.path.display());
4798
4799 // Resolve the per-tree compaction compression pool once, at open: if the
4800 // caller supplied no shared pool but asked for >1 thread, build the
4801 // default rayon-backed pool now so every compaction reuses it (building
4802 // a pool per compaction would spawn threads on each run). A caller-
4803 // supplied pool is left untouched. Shadowed under `parallel` only, so
4804 // non-parallel builds don't carry an unused `mut`.
4805 // `mut` under `zstd_any` too: installing the dictionaries ASSIGNS the
4806 // config a registry of its own rather than storing through the one it
4807 // arrived with, which is what keeps two trees opened from clones of one
4808 // config from sharing a set.
4809 #[cfg_attr(
4810 not(any(feature = "parallel", zstd_any)),
4811 expect(unused_mut, reason = "assigned only by the parallel / zstd builds")
4812 )]
4813 let mut config = config;
4814
4815 #[cfg(feature = "parallel")]
4816 if config.compaction_pool.is_none() && config.compaction_threads > 1 {
4817 config.compaction_pool = Some(Arc::new(
4818 crate::table::writer::RayonSpawner::with_threads(config.compaction_threads)?,
4819 ));
4820 }
4821
4822 // Gate on the `page_ecc` cargo feature: caller asked for ECC
4823 // but the build does not link the Reed-Solomon codec. We have
4824 // no way to verify or recover RS parity without the codec, so
4825 // refuse to open rather than silently downgrade integrity.
4826 // Two surfaces to check:
4827 // - `Config::page_ecc(true)` → SST data-block ECC
4828 // - `Config::with_runtime_config(RuntimeConfig { page_ecc: true, .. })`
4829 // → manifest-Block ECC (consumed by manifest_blocks::writer)
4830 // Both silently no-op without the feature; refusing here is
4831 // the only place callers see a typed error.
4832 if (config.page_ecc || config.initial_runtime_config.page_ecc)
4833 && !cfg!(feature = "page_ecc")
4834 {
4835 return Err(crate::Error::PageEccUnsupported);
4836 }
4837
4838 // Acquire the cross-process directory lock BEFORE any manifest access
4839 // (the installed-snapshot probe + `has_existing_version_state` check below, and
4840 // the recover / create paths). Acquiring it here makes `open()`
4841 // exclusive end-to-end: a concurrent opener fails fast with
4842 // `Error::Locked` instead of racing through the probe and observing a
4843 // peer's half-created directory (which would surface as the InvalidData
4844 // "half-written checkpoint" path rather than `Locked`). The `LOCK` file
4845 // needs its directory to exist, so create the root directory first
4846 // (idempotent; `create_new` re-creates the `tables/` subtree). The lock
4847 // is threaded into the constructor so it lives for the tree's lifetime.
4848 #[cfg(feature = "std")]
4849 let restoring = matches!(lock, crate::config::DirectoryLock::Held(_));
4850 // The folders that gain an entry when the open makes the tree folder:
4851 // a tree created here syncs each of them, though by then they exist.
4852 let made_by_open = new_entry_holders(&*config.fs, &config.path)?;
4853 let open_holders: Vec<(crate::path::PathBuf, Arc<dyn Fs>)> = made_by_open
4854 .iter()
4855 .map(|holder| (holder.clone(), Arc::clone(&config.fs)))
4856 .collect();
4857 // What the tree folder holds before this open, when it exists: a tree
4858 // the open makes there and then takes back takes back only what it
4859 // added.
4860 let found = if made_by_open.is_empty() {
4861 Some(folder_entries(&*config.fs, &config.path)?)
4862 } else {
4863 None
4864 };
4865 #[cfg(feature = "std")]
4866 let directory_lock = match lock {
4867 crate::config::DirectoryLock::Held(held) => held,
4868 crate::config::DirectoryLock::Acquire => {
4869 config.fs.create_dir_all(&config.path)?;
4870 match crate::config::acquire_directory_lock(
4871 &*config.fs,
4872 &config.path,
4873 config.directory_lock,
4874 ) {
4875 Ok(held) => held,
4876 Err(e) => {
4877 // Without the lock the folder may be another opener's
4878 // as much as this one's: nothing is removed, and the
4879 // names this open made are made durable instead, so
4880 // a retry that finds them needs no sync for them.
4881 sync_made_names(&open_holders);
4882 return Err(e);
4883 }
4884 }
4885 }
4886 };
4887
4888 // From here to the creation, a failure takes back the folders this
4889 // open made, as a failed creation does: a retry finds them existing
4890 // and would give the folders holding their names no sync.
4891 let prelude = (|| -> crate::Result<bool> {
4892 // A restore that published the manifest but never made the restored
4893 // memtable contents durable left a tree that lacks them: serving it
4894 // would answer from a partial snapshot. Only the restore that finishes
4895 // the job opens it.
4896 #[cfg(feature = "std")]
4897 if !restoring
4898 && config
4899 .fs
4900 .exists(&config.path.join(crate::file::RESTORE_MARKER_FILE))?
4901 {
4902 return Err(crate::Error::from(crate::io::Error::new(
4903 crate::io::ErrorKind::InvalidData,
4904 format!(
4905 "{} holds an interrupted restore; run restore_pinned again to finish it",
4906 config.path.display(),
4907 ),
4908 )));
4909 }
4910
4911 // Check for old version
4912 if config.fs.exists(&config.path.join("version"))? {
4913 log::error!(
4914 "refusing to open: this directory has a `version` marker file, which only the \
4915 retired V1 layout wrote. V6 is the only on-disk format THIS engine decodes, and \
4916 it ships no conversion tooling for V1. If the directory is a V1 database the data is \
4917 not lost: open it with the engine that wrote it, or convert it there. If the \
4918 file is unrelated to this store, move it aside and retry"
4919 );
4920 // The marker's contents are deliberately not read: no legacy
4921 // decode path exists to act on them, so parsing it could only
4922 // change the wording of this error. That makes the presence of
4923 // the file evidence, not proof, hence "has a marker" rather than
4924 // "is a V1 database", and the second escape hatch for a directory
4925 // that merely happens to carry the name.
4926 //
4927 // Scoped to this engine on purpose. The operator reads this line
4928 // while deciding what to do with the directory, and "no conversion
4929 // path" full stop would read as "unrecoverable" — which is wrong,
4930 // and the opposite of what the repair gate documents (an
4931 // unsupported version needs offline conversion or a matching
4932 // binary; see `is_repairable_open_error`).
4933 //
4934 // Literal discriminant: V1 is a retired format and FormatVersion
4935 // carries no legacy variants (single-format contract).
4936 return Err(crate::Error::InvalidVersion(1));
4937 }
4938
4939 // Recover when the tree holds an installed snapshot, create one
4940 // otherwise. The installed snapshot is verified here, before the
4941 // dictionaries below are read or their registrations swept: its
4942 // footer under the configured provider, its format, and that it is
4943 // the version its name says.
4944 let installed =
4945 crate::version::recovery::installed_snapshot(&config.path, &*config.fs)?;
4946 if let Some(id) = installed {
4947 crate::version::recovery::verify_snapshot(
4948 &config.path,
4949 id,
4950 &*config.fs,
4951 config.encryption.clone(),
4952 )?;
4953 }
4954 let recovering = installed.is_some();
4955
4956 // Load the tree's own dictionaries before anything opens a table: a
4957 // table resolves the dictionary id it recorded against this set, so an
4958 // empty one fails every dictionary-compressed table in the tree. Before
4959 // the recover / create split, because a freshly created tree writes
4960 // dictionary-compressed tables too.
4961 //
4962 // AFTER the probe above, which verified the manifest under the
4963 // configured provider. A dictionary file carries no header saying it is
4964 // sealed, so without the key an encrypted one reads as bytes that fail
4965 // their hash, which is how a damaged one reads, and a damaged one is
4966 // what `open_or_repair` repairs. The probe fails a wrong or missing key
4967 // first, as the configuration error it is.
4968 //
4969 // A dictionary supplied through the config joins them, which is what
4970 // keeps `Config::dict` working: on the first open it is the only one
4971 // there, and it is registered at the end of this function so later
4972 // opens need no config at all.
4973 //
4974 // Under the directory lock, since it reads the tree's own folder and
4975 // SWEEPS unpublished `.tmp` registrations: doing that before the lock
4976 // lets an opener that is about to fail with `Locked` delete a live
4977 // owner's in-flight registration between its write and its rename.
4978 #[cfg(zstd_any)]
4979 config.install_own_zstd_dictionaries()?;
4980
4981 // Only now can the compression policies be checked: they name
4982 // dictionaries by id, and the tree may hold one without the caller
4983 // supplying the bytes again. Validating before the load would refuse
4984 // exactly the reopen the tree owning its dictionaries exists to allow.
4985 #[cfg(zstd_any)]
4986 config.initial_runtime_config.check_dictionaries(
4987 config.kv_separation_opts.is_some(),
4988 &config.current_zstd_dictionaries(),
4989 )?;
4990
4991 // No installed snapshot MUST coincide with a directory whose
4992 // tables and blob folders hold nothing; otherwise we are looking
4993 // at a half-written checkpoint (or other interrupted sealing),
4994 // and calling `create_new` would overwrite the partial state with
4995 // an empty tree, turning a recoverable failure into data loss.
4996 // What a creation that never completed leaves (its empty folders,
4997 // a temporary snapshot never renamed into place, the empty log
4998 // made before that rename) holds no data and is created over. A
4999 // snapshot under its own name is installed, with or without its
5000 // log, and is recovered instead.
5001 if !recovering && has_existing_version_state(&config)? {
5002 // Return Error::Io(InvalidData, ...) rather than
5003 // Error::Unrecoverable so callers that don't read
5004 // logs still get a programmatic surface with the
5005 // path and remediation hint embedded. `log::error!`
5006 // stays for human ops who DO watch logs and want
5007 // the full context at the moment of failure (the
5008 // structured error is what propagates up the call
5009 // chain; the log line records the diagnosis next
5010 // to the timestamp).
5011 let msg = format!(
5012 "Tree::open: refusing to recover {} — it holds no installed manifest \
5013 snapshot (a v<N>) but its tables/ or blobs/ folder holds files, \
5014 or an edits-N holds edits. This is the on-disk signature of a \
5015 half-written \
5016 checkpoint or interrupted sealing. Remove the partial directory and \
5017 retry the checkpoint, or restore the snapshot from a backup before \
5018 reopening.",
5019 config.path.display(),
5020 );
5021 log::error!("{msg}");
5022 return Err(crate::Error::from(crate::io::Error::new(
5023 crate::io::ErrorKind::InvalidData,
5024 msg,
5025 )));
5026 }
5027 Ok(recovering)
5028 })();
5029 let recovering = match prelude {
5030 Ok(recovering) => recovering,
5031 Err(e) => {
5032 if !made_by_open.is_empty() {
5033 // Another opener may have filled the folder between the
5034 // probe that found it missing and this open's lock: only
5035 // what is empty is taken back. The lock file goes while
5036 // the lock is held, so no opener is left holding the lock
5037 // of a file another one can make anew; where an open
5038 // file cannot be removed it stays, and so does the folder.
5039 #[cfg(feature = "std")]
5040 {
5041 let lock_file = config.path.join(crate::config::DIRECTORY_LOCK_FILE);
5042 if let Err(e) = config.fs.remove_file(&lock_file)
5043 && e.kind() != crate::io::ErrorKind::NotFound
5044 {
5045 log::warn!(
5046 "Tree::open: could not remove {} after a failed open ({e})",
5047 lock_file.display()
5048 );
5049 }
5050 drop(directory_lock);
5051 }
5052 let made = made_folders(&config.path, &made_by_open)
5053 .into_iter()
5054 .map(|folder| (folder, Arc::clone(&config.fs)))
5055 .collect();
5056 undo_creation(None, made, &open_holders);
5057 }
5058 return Err(e);
5059 }
5060 };
5061
5062 let creation = (!recovering).then(|| Creation {
5063 path: config.path.clone(),
5064 fs: Arc::clone(&config.fs),
5065 holders: made_by_open.clone(),
5066 found,
5067 names,
5068 sync_mode: config.sync_mode,
5069 });
5070 let tree = if recovering {
5071 Self::recover(
5072 config,
5073 #[cfg(feature = "std")]
5074 directory_lock,
5075 )
5076 } else {
5077 Self::create_new(
5078 config,
5079 &made_by_open,
5080 names,
5081 #[cfg(feature = "std")]
5082 directory_lock,
5083 )
5084 }?;
5085
5086 // A dictionary supplied through the config becomes the tree's, so the
5087 // caller supplies it once rather than on every open forever. Idempotent
5088 // by id, so re-opening with the same one writes nothing.
5089 //
5090 // BOTH write slots, not just the table one: a KV-separated tree
5091 // compresses its blob files against a dictionary of their own, and a
5092 // blob file whose dictionary the tree never stored cannot be read back
5093 // from a checkpoint or after the caller stops supplying it — the same
5094 // failure the table side had, one file class over.
5095 //
5096 // After the recover / create above, because registering installs a
5097 // version edit and there is no history to install into before that.
5098 #[cfg(zstd_any)]
5099 let registered = [
5100 tree.config.zstd_dictionary.clone(),
5101 tree.config
5102 .kv_separation_opts
5103 .as_ref()
5104 .and_then(|o| o.zstd_dictionary.clone()),
5105 ]
5106 .into_iter()
5107 .flatten()
5108 .try_for_each(|dict| tree.register_zstd_dictionary(dict));
5109 // A tree this open made, whose open then fails, is taken back: the
5110 // caller is told it was not opened, and a retry makes it anew.
5111 #[cfg(zstd_any)]
5112 if let Err(e) = registered {
5113 if let Some(creation) = creation {
5114 creation.take_back(tree);
5115 }
5116 return Err(e);
5117 }
5118
5119 Ok((tree, creation))
5120 }
5121
5122 /// Returns `true` if there are some tables that are being compacted.
5123 #[doc(hidden)]
5124 #[must_use]
5125 pub fn is_compacting(&self) -> bool {
5126 !self.compaction_state.lock().hidden_set().is_empty()
5127 }
5128
5129 /// Computed storage admission predicate backing
5130 /// [`AbstractTree::write_admission`].
5131 ///
5132 /// Cheap: reads in-memory size accounting only (no syscall). Returns
5133 /// `Ok(())` unless admission control is enabled AND a budget is set AND the
5134 /// live footprint plus reserved headroom exceeds it.
5135 /// Best-effort minimum free space across every filesystem this tree writes
5136 /// to: the primary data path AND each per-level route (`Config::level_routes`
5137 /// can place cold-level SSTs on separate volumes). The admission gate must
5138 /// reflect the tightest volume, since a full routed disk fails compaction /
5139 /// flush targeting it even while the primary still has room.
5140 ///
5141 /// A backend that cannot report free space (or an I/O hiccup) yields
5142 /// `u64::MAX` = "no disk pressure", so a probe failure never falsely drives
5143 /// the tree read-only.
5144 fn probe_disk_free(&self) -> u64 {
5145 self.0.config.min_available_space()
5146 }
5147
5148 /// Disk-aware capacity figures for [`AbstractTree::storage_stats`], given the
5149 /// live footprint `used`: `(capacity, available, compaction_possible)`.
5150 ///
5151 /// `capacity` is the tighter of the configured quota and the physical disk
5152 /// headroom (`free + used`) — the same effective limit
5153 /// [`Self::compute_write_admission`] gates against — reported regardless of
5154 /// whether the admission gate is enabled (introspection is always available).
5155 /// `None` capacity/available means unbounded (no quota AND the backend
5156 /// cannot report free space). `compaction_possible` is `true` when unbounded
5157 /// or when at least [`MIN_RESERVED_HEADROOM`] of working room remains.
5158 pub(crate) fn admission_capacity(&self, used: u64) -> (Option<u64>, Option<u64>, bool) {
5159 let quota = self
5160 .0
5161 .runtime_config
5162 .load()
5163 .storage_limit_bytes
5164 .unwrap_or(u64::MAX);
5165 let free = self.probe_disk_free();
5166 // `free == u64::MAX` is the "backend can't report free space" sentinel:
5167 // adding `used` would overflow, so treat capacity as quota-only (the
5168 // explicit branch avoids the overflow without masking it with saturation).
5169 // Otherwise `free + used` ≤ ~2× disk capacity and cannot overflow u64.
5170 let capacity = if free == u64::MAX {
5171 quota
5172 } else {
5173 quota.min(free + used)
5174 };
5175 if capacity == u64::MAX {
5176 return (None, None, true);
5177 }
5178 // `available = max(0, capacity - used)`: an operator quota set below the
5179 // live footprint makes `capacity < used`, and available space cannot be
5180 // negative. The clamp-to-zero IS the intended semantics here.
5181 let available = capacity.saturating_sub(used);
5182 (
5183 Some(capacity),
5184 Some(available),
5185 available >= MIN_RESERVED_HEADROOM,
5186 )
5187 }
5188
5189 /// The logical partition-quota headroom for the two-layer space model:
5190 /// `max(0, storage_limit_bytes - used)`, or `u64::MAX` when no quota is set.
5191 ///
5192 /// The quota half of [`crate::compaction::worker::merge_would_fit`]; the
5193 /// free space of each volume is the other. An operator quota set below the
5194 /// live footprint leaves zero headroom — the clamp-to-zero is the intended
5195 /// min-semantics, not masking.
5196 pub(crate) fn quota_headroom(&self, used: u64) -> u64 {
5197 self.0
5198 .runtime_config
5199 .load()
5200 .storage_limit_bytes
5201 .map_or(u64::MAX, |limit| limit.saturating_sub(used))
5202 }
5203
5204 /// Whether the opt-in storage admission gate is active (a near-full disk or
5205 /// configured quota can drive the tree read-only and gate compaction space).
5206 /// Capacity introspection figures are reported regardless; this only governs
5207 /// whether the gate actually enforces.
5208 pub(crate) fn storage_admission_enabled(&self) -> bool {
5209 self.0.runtime_config.load().storage_admission_check
5210 }
5211
5212 #[expect(
5213 clippy::significant_drop_tightening,
5214 reason = "the admission cache lock intentionally spans the recompute \
5215 (stat + disk-free probe) so concurrent admission checks \
5216 coalesce on a single probe rather than each issuing a syscall"
5217 )]
5218 fn compute_write_admission(&self) -> crate::Result<()> {
5219 let rc = self.0.runtime_config.load();
5220 if !rc.storage_admission_check {
5221 return Ok(());
5222 }
5223
5224 // Take ONE coherent snapshot of the latest super-version and derive
5225 // BOTH the on-disk footprint and the pending-memtable bytes from it.
5226 // Reading them from two separate `latest_version()` loads would be a
5227 // TOCTOU bug: a flush installing a new version between the two reads
5228 // could pair an old (larger) disk usage with new (smaller) pending
5229 // bytes — or vice versa — and open the gate incorrectly.
5230 let super_version = self.version_history.read().latest_version();
5231 let vid = super_version.version.id();
5232
5233 // True physical footprint, including blob files — the SAME basis
5234 // `storage_stats()` reports, so the gate and the reported usage agree.
5235 // NOT `disk_space()` (metadata Level::size, which omits blob files and
5236 // undercounts the physical file by the meta block / footer).
5237 //
5238 // Cached so gated writes don't re-stat every live file or re-probe disk
5239 // on every call. `used_bytes` only changes when a new version is
5240 // installed (flush / compaction), so it is recomputed on a version
5241 // change. `disk_free` can change under us (another process writing the
5242 // same filesystem), so it is ALSO re-probed once its sample is older
5243 // than `ADMISSION_DISK_FREE_TTL` — bounding staleness without a syscall
5244 // per write. `update_runtime_config` resets the entry for an immediate
5245 // re-probe. The values live behind one mutex as a coherent unit (see
5246 // `TreeInner::admission_used_cache`).
5247 //
5248 // The TTL fast-path is std-only: under `no_std` there is no monotonic
5249 // clock (`crate::time::Instant::elapsed` is a zero stub), so an
5250 // elapsed-time window cannot bound staleness — a same-version sample
5251 // would otherwise look fresh forever and a filling disk would never be
5252 // re-probed. Under `no_std` the fast-path is skipped, so `disk_free` is
5253 // re-probed on every gated write (the `used` footprint stays cached by
5254 // version either way), keeping admission safe without a monotonic clock.
5255 let now = crate::time::Instant::now();
5256 let (used, disk_free) = {
5257 let mut cache = self.0.admission_used_cache.lock();
5258 match *cache {
5259 // Fresh: same version AND disk sample within the TTL. std-only —
5260 // `cfg!(feature = "std")` is `false` under `no_std`, so the guard
5261 // short-circuits there and the next arm re-probes every call.
5262 Some((cvid, used, free, at))
5263 if cvid == vid
5264 && cfg!(feature = "std")
5265 && at.elapsed() < ADMISSION_DISK_FREE_TTL =>
5266 {
5267 (used, free)
5268 }
5269 // Same version, stale disk sample: keep `used`, re-probe disk.
5270 Some((cvid, used, _, _)) if cvid == vid => {
5271 let free = self.probe_disk_free();
5272 *cache = Some((vid, used, free, now));
5273 (used, free)
5274 }
5275 // New version (or unset): recompute footprint and re-probe disk.
5276 _ => {
5277 let used = crate::storage_stats::compute_used_bytes(&super_version.version)?;
5278 let free = self.probe_disk_free();
5279 *cache = Some((vid, used, free, now));
5280 (used, free)
5281 }
5282 }
5283 };
5284
5285 // Effective limit is the tighter of the configured quota and the
5286 // physical disk headroom (free + what we already occupy): the disk can
5287 // fill from other processes even below a generous quota, and a tree with
5288 // no quota at all must still stop before ENOSPC. `None` quota = unbounded
5289 // by configuration; disk-free then alone bounds it.
5290 //
5291 // `disk_free` is the MINIMUM free across every volume the tree writes to
5292 // (`probe_disk_free` mins the primary path and all `level_routes`). The
5293 // `+ used` here is NOT an accounting of one volume's usage against
5294 // another's free space — it cancels out of the disk branch of the gate:
5295 // passing requires `used + reserved <= disk_free + used`, i.e.
5296 // `reserved <= disk_free`. So a passing gate guarantees the TIGHTEST
5297 // volume alone has at least `reserved` free — a conservative per-volume
5298 // headroom, never the sum of an empty routed volume's slack plus an
5299 // unrelated full volume's occupancy. A route that drops below `reserved`
5300 // free drives the whole tree read-only, exactly so a later flush /
5301 // compaction targeting that route cannot hit ENOSPC.
5302 let quota = rc.storage_limit_bytes.unwrap_or(u64::MAX);
5303 // `disk_free == u64::MAX` is the "backend can't report" sentinel; adding
5304 // `used` would overflow, so treat the limit as quota-only (explicit
5305 // branch, no saturation masking). Otherwise `disk_free + used` ≤ ~2× disk
5306 // capacity and cannot overflow u64.
5307 let limit = if disk_free == u64::MAX {
5308 quota
5309 } else {
5310 quota.min(disk_free + used)
5311 };
5312 // Both sources unbounded → nothing to gate.
5313 if limit == u64::MAX {
5314 return Ok(());
5315 }
5316
5317 // Reserved headroom keeps the soft budget from becoming a hard wall:
5318 // enough to flush every pending memtable (plus a margin for the
5319 // index/filter/footer overhead a flush adds) so a queued flush always
5320 // fits at the limit, with a floor for compaction working space.
5321 // Internal flush / compaction are never gated, so this band is the
5322 // engine's room to reclaim.
5323 //
5324 // Count ALL pending memtable bytes in this snapshot — the active one AND
5325 // any sealed (rotated) memtables awaiting flush — not just the active
5326 // one: after a rotation the active memtable is empty but the sealed
5327 // memtable's queued flush will still consume disk, so it must be
5328 // reserved for. Memtable sizes are bounded by RAM, so the sum (and the
5329 // +1/8 overhead margin below) cannot overflow u64 → plain arithmetic.
5330 let pending_memtable_bytes: u64 = super_version.active_memtable.size()
5331 + super_version
5332 .sealed_memtables
5333 .iter()
5334 .map(|m| m.size())
5335 .sum::<u64>();
5336
5337 let reserved =
5338 (pending_memtable_bytes + pending_memtable_bytes / 8).max(MIN_RESERVED_HEADROOM);
5339 // `used` (disk) + `reserved` (RAM-bounded) cannot realistically overflow,
5340 // but keep the comparison fail-closed with checked arithmetic: any
5341 // overflow means "definitely over budget", so deny.
5342 match used.checked_add(reserved) {
5343 Some(total) if total <= limit => Ok(()),
5344 _ => Err(crate::Error::StorageFull { used, limit }),
5345 }
5346 }
5347
5348 fn inner_compact(
5349 &self,
5350 strategy: Arc<dyn CompactionStrategy>,
5351 gc_watermark: SeqNo,
5352 ) -> crate::Result<crate::compaction::CompactionResult> {
5353 use crate::compaction::worker::{Options, do_compaction};
5354
5355 let mut opts = Options::from_tree(self, strategy);
5356 opts.gc_watermark = gc_watermark;
5357
5358 let result = do_compaction(&opts)?;
5359
5360 log::debug!("Compaction run over");
5361
5362 Ok(result)
5363 }
5364
5365 /// Iterator over the whole tree at snapshot `seqno`.
5366 ///
5367 /// # Errors
5368 ///
5369 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
5370 /// when `seqno` is at or below the retention floor; the error is
5371 /// raised here, before any I/O, rather than as an iterator item.
5372 #[doc(hidden)]
5373 pub fn create_iter(
5374 &self,
5375 seqno: SeqNo,
5376 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5377 ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
5378 self.create_range::<UserKey, _>(&.., seqno, ephemeral)
5379 }
5380
5381 /// Iterator over `range` at snapshot `seqno`.
5382 ///
5383 /// # Errors
5384 ///
5385 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
5386 /// when `seqno` is at or below the retention floor; the error is
5387 /// raised here, before any I/O, rather than as an iterator item.
5388 ///
5389 /// The version is resolved ONCE here and moved into the iterator, so the
5390 /// whole traversal reads one file set: a compaction installing mid-scan
5391 /// neither adds its output nor removes the inputs this iterator holds, and
5392 /// those inputs stay on disk until the iterator drops.
5393 #[doc(hidden)]
5394 pub fn create_range<'a, K: AsRef<[u8]> + 'a, R: RangeBounds<K> + 'a>(
5395 &self,
5396 range: &'a R,
5397 seqno: SeqNo,
5398 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5399 ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
5400 let super_version = self.get_version_for_snapshot(seqno)?;
5401
5402 Ok(Self::create_internal_range_with_prefix_hash(
5403 super_version,
5404 range,
5405 seqno,
5406 ephemeral,
5407 self.config.merge_operator.clone(),
5408 self.config.comparator.clone(),
5409 None,
5410 self.blob_source(),
5411 )
5412 .map(|item| match item {
5413 Ok(kv) => Ok((kv.key.user_key, kv.value)),
5414 Err(e) => Err(e),
5415 }))
5416 }
5417
5418 /// Build a [`SeekableTreeIter`](crate::range::SeekableTreeIter) over
5419 /// `[lo, hi)`. Source collection (Phase 1) runs once; repositions reuse it.
5420 ///
5421 /// Because the sources are collected once and reused, every reposition
5422 /// resolves against the file set of the version taken here, however long
5423 /// the iterator lives. That is the same one-version rule the plain range
5424 /// iterator states, and it binds for longer.
5425 ///
5426 /// # Errors
5427 ///
5428 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
5429 /// when `seqno` is at or below the retention floor.
5430 #[doc(hidden)]
5431 pub fn create_seekable_range_bounds(
5432 &self,
5433 lo: Bound<UserKey>,
5434 hi: Bound<UserKey>,
5435 seqno: SeqNo,
5436 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5437 ) -> crate::Result<crate::range::SeekableTreeIter> {
5438 self.create_seekable_range_bounds_with(lo, hi, seqno, ephemeral, self.blob_source())
5439 }
5440
5441 /// [`Self::create_seekable_range_bounds`], reading values kept in the
5442 /// value log through `blob_source`, the one its caller took as the read
5443 /// began.
5444 pub(crate) fn create_seekable_range_bounds_with(
5445 &self,
5446 lo: Bound<UserKey>,
5447 hi: Bound<UserKey>,
5448 seqno: SeqNo,
5449 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5450 blob_source: Option<crate::blob_tree::BlobSource>,
5451 ) -> crate::Result<crate::range::SeekableTreeIter> {
5452 use crate::range::{IterState, SeekableTreeIter};
5453
5454 let super_version = self.get_version_for_snapshot(seqno)?;
5455
5456 let iter_state = IterState {
5457 version: super_version,
5458 ephemeral,
5459 merge_operator: self.config.merge_operator.clone(),
5460 blob_source,
5461 comparator: self.config.comparator.clone(),
5462 prefix_hash: None,
5463 #[cfg(feature = "metrics")]
5464 metrics: Some(self.0.metrics.clone()),
5465 };
5466
5467 Ok(SeekableTreeIter::create(iter_state, lo, hi, seqno))
5468 }
5469
5470 /// Iterator over the keys starting with `prefix` at snapshot `seqno`.
5471 ///
5472 /// # Errors
5473 ///
5474 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
5475 /// when `seqno` is at or below the retention floor; the error is
5476 /// raised here, before any I/O, rather than as an iterator item.
5477 ///
5478 /// One version for the whole traversal, as for the range iterator. The
5479 /// prefix filter narrows which of ITS tables are consulted; it never
5480 /// reaches a table outside the version resolved here.
5481 #[doc(hidden)]
5482 pub fn create_prefix<'a, K: AsRef<[u8]> + 'a>(
5483 &self,
5484 prefix: K,
5485 seqno: SeqNo,
5486 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5487 ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
5488 Ok(self
5489 .create_internal_prefix(
5490 prefix.as_ref(),
5491 seqno,
5492 ephemeral,
5493 self.config.merge_operator.clone(),
5494 self.blob_source(),
5495 )?
5496 .map(|item| match item {
5497 Ok(kv) => Ok((kv.key.user_key, kv.value)),
5498 Err(e) => Err(e),
5499 }))
5500 }
5501
5502 /// Versions visible at `seqno` under `prefix`, a merge chain resolved by
5503 /// `merge_operator` (reading a separated base from `blob_source`) or, with
5504 /// none, yielded as its newest operand.
5505 fn create_internal_prefix(
5506 &self,
5507 prefix: &[u8],
5508 seqno: SeqNo,
5509 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5510 merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
5511 blob_source: Option<crate::blob_tree::BlobSource>,
5512 ) -> crate::Result<
5513 impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static + use<>,
5514 > {
5515 use crate::prefix::compute_prefix_hash;
5516 use crate::range::{IterState, TreeIter, prefix_to_range};
5517
5518 let prefix_hash = compute_prefix_hash(self.config.prefix_extractor.as_ref(), prefix);
5519
5520 let range = prefix_to_range(prefix);
5521
5522 let super_version = self.get_version_for_snapshot(seqno)?;
5523
5524 let iter_state = IterState {
5525 version: super_version,
5526 ephemeral,
5527 merge_operator,
5528 blob_source,
5529 comparator: self.config.comparator.clone(),
5530 prefix_hash,
5531 #[cfg(feature = "metrics")]
5532 metrics: Some(self.0.metrics.clone()),
5533 };
5534
5535 Ok(TreeIter::create_range(iter_state, range, seqno))
5536 }
5537
5538 /// The keys visible at `seqno`, each as its newest version with a merge
5539 /// chain left unmerged: what the key-only reads walk.
5540 fn create_key_only_iter(
5541 &self,
5542 seqno: SeqNo,
5543 ephemeral: Option<(Arc<Memtable>, SeqNo)>,
5544 ) -> crate::Result<
5545 impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static + use<>,
5546 > {
5547 let super_version = self.get_version_for_snapshot(seqno)?;
5548 Ok(Self::create_internal_range_with_prefix_hash::<UserKey, _>(
5549 super_version,
5550 &..,
5551 seqno,
5552 ephemeral,
5553 None,
5554 self.config.comparator.clone(),
5555 None,
5556 None,
5557 ))
5558 }
5559
5560 /// Seals the active memtable under the held version lock, returning it,
5561 /// or `None` when it is empty.
5562 pub(crate) fn rotate_memtable_locked(
5563 &self,
5564 history: &mut crate::version::SuperVersions,
5565 ) -> Option<Arc<Memtable>> {
5566 let super_version = history.latest_version_ref();
5567
5568 if super_version.active_memtable.is_empty() {
5569 return None;
5570 }
5571
5572 let yanked_memtable = Arc::clone(&super_version.active_memtable);
5573
5574 let mut copy = super_version.clone();
5575 copy.active_memtable = Arc::new(Memtable::new(
5576 self.memtable_id_counter.next(),
5577 self.config.comparator.clone(),
5578 ));
5579 copy.sealed_memtables =
5580 Arc::new(super_version.sealed_memtables.add(yanked_memtable.clone()));
5581
5582 // Rotate does not modify the memtable so it cannot break snapshots
5583 copy.seqno = super_version.seqno;
5584
5585 history.replace_latest_version(copy);
5586
5587 log::trace!(
5588 "rotate: added memtable id={} to sealed memtables",
5589 yanked_memtable.id,
5590 );
5591
5592 Some(yanked_memtable)
5593 }
5594
5595 /// Adds an item to the active memtable.
5596 ///
5597 /// Returns the added item's size and new size of the memtable.
5598 #[doc(hidden)]
5599 #[must_use]
5600 pub fn append_entry(&self, value: InternalValue) -> (u64, u64) {
5601 #[cfg(feature = "metrics")]
5602 self.accept(&value);
5603 let kv_digest = self.insert_digest(&value);
5604
5605 // The insert holds its thread's shard of the insert gate, which a
5606 // version replacing the active memtable closes before it publishes:
5607 // `value` and its digest land in the active memtable the gate hands
5608 // out, and no rotation seals it mid-insert. Unlike the history read
5609 // lock, the shard is a line no other writer thread touches.
5610 #[cfg(feature = "std")]
5611 {
5612 self.insert_gate
5613 .enter()
5614 .memtable()
5615 .insert_with_kv_digest(value, kv_digest)
5616 }
5617 // no-std: the history read lock, with the same guarantee.
5618 #[cfg(not(feature = "std"))]
5619 {
5620 self.version_history
5621 .read()
5622 .latest_version_ref()
5623 .active_memtable
5624 .insert_with_kv_digest(value, kv_digest)
5625 }
5626 }
5627
5628 /// Adds a cell row whose references are `refs` to the active memtable,
5629 /// once each reference is shown to name a frame the current version still
5630 /// holds.
5631 ///
5632 /// The check and the insert run under the version-history read lock, the
5633 /// lock a version install takes for writing: an install that removes or
5634 /// punches a blob file sees this row's registration in the memtable, or
5635 /// this check sees the file gone.
5636 ///
5637 /// # Errors
5638 ///
5639 /// [`crate::Error::BlobRef`] when a reference names a blob file the current
5640 /// version no longer holds, or a frame before that file's live start: a
5641 /// relocation has moved it, and the key's latest version names the copy.
5642 /// Also when its object was released after the version it was read from
5643 /// (each reference comes with that version's id).
5644 pub(crate) fn append_cell_row(
5645 &self,
5646 value: InternalValue,
5647 refs: &[(crate::blob_tree::handle::BlobIndirection, u64)],
5648 ) -> crate::Result<(u64, u64)> {
5649 let kv_digest = self.insert_digest(&value);
5650 let history = self.version_history.read();
5651 let latest = history.latest_version_ref();
5652 for (reference, source) in refs {
5653 let held = latest
5654 .version
5655 .blob_files
5656 .get(reference.vhandle.blob_file_id)
5657 .is_some_and(|file| reference.vhandle.offset >= file.live_data_start());
5658 if !held {
5659 return Err(crate::Error::BlobRef(
5660 "stale reference: the object has moved, read the key again",
5661 ));
5662 }
5663 // An object let go of since the read: writing it back would make
5664 // this row hold what the accounting already charged as garbage.
5665 if history
5666 .released()
5667 .released_since(*source, &reference.vhandle)
5668 {
5669 return Err(crate::Error::BlobRef(
5670 "stale reference: the object was released, read the key again",
5671 ));
5672 }
5673 }
5674 let inserted = latest
5675 .active_memtable
5676 .insert_with_kv_digest(value, kv_digest);
5677 // Held through the insert: the check above is only as good as the
5678 // version it read staying the latest until the row is in.
5679 drop(history);
5680 Ok(inserted)
5681 }
5682
5683 /// Charges `value`, taken in from the caller, to the payload the tree's
5684 /// writes are measured against: a deletion its key, anything else its key
5685 /// and value.
5686 #[cfg(feature = "metrics")]
5687 fn accept(&self, value: &InternalValue) {
5688 let key = value.key.user_key.len();
5689 if value.key.value_type.is_tombstone() {
5690 self.0.metrics.accepted_deletion(key);
5691 } else {
5692 self.0.metrics.accepted_value(key + value.value.len());
5693 }
5694 }
5695
5696 /// The insert-time per-KV digest of `value`, when the runtime config asks
5697 /// for one.
5698 fn insert_digest(
5699 &self,
5700 value: &InternalValue,
5701 ) -> Option<(u32, crate::runtime_config::ChecksumAlgorithm)> {
5702 // Per-KV residence digest (KvChecksumComputePoint::AtInsert): compute
5703 // the entry's 4-byte logical-content digest now, so a RAM bit-flip
5704 // while it sits in the memtable is caught at flush. The digest covers
5705 // the OWNED `value` and is independent of which active memtable
5706 // receives it, so computing it before taking the version-history guard
5707 // is correct (a concurrent rotation just routes the same value+digest
5708 // into the new active memtable) AND keeps the hash out of the read-lock
5709 // critical section. The gate is one relaxed byte mirrored from the
5710 // runtime config (`TreeInner::kv_digest_at_insert`); under the default
5711 // `AtBlockCompile` (or `Off`) it is `0` and no digest is computed.
5712 let gate = self
5713 .0
5714 .kv_digest_at_insert
5715 .load(core::sync::atomic::Ordering::Relaxed);
5716 inner::kv_digest_algo_from_gate(gate).and_then(|algo| {
5717 crate::table::block::kv_checksum::kv_digest(value, algo).map(|d| {
5718 #[expect(
5719 clippy::cast_possible_truncation,
5720 reason = "AtInsert is config-validated to a 4-byte algorithm; the digest fits u32"
5721 )]
5722 let lo = d as u32;
5723 (lo, algo)
5724 })
5725 })
5726 }
5727
5728 /// Adds multiple items to the active memtable in bulk.
5729 ///
5730 /// Acquires the version-history lock once and delegates to
5731 /// [`Memtable::insert_batch`] for batch size accounting.
5732 ///
5733 /// Returns the total bytes added and new size of the memtable.
5734 #[doc(hidden)]
5735 #[must_use]
5736 pub(crate) fn append_batch(&self, items: Vec<InternalValue>) -> (u64, u64) {
5737 // Per-KV residence digest under AtInsert (see `append_entry`): pass the
5738 // algorithm so the bulk path fixes each entry's digest at insert. The
5739 // default path passes `None` and is unchanged.
5740 let kv_algo = inner::kv_digest_algo_from_gate(
5741 self.0
5742 .kv_digest_at_insert
5743 .load(core::sync::atomic::Ordering::Relaxed),
5744 );
5745 #[cfg(feature = "metrics")]
5746 for item in &items {
5747 self.accept(item);
5748 }
5749
5750 // Hold the read guard for the entire insert to prevent rotate_memtable()
5751 // from sealing this memtable mid-batch (which could cause data loss if
5752 // a concurrent flush persists only a prefix of the batch).
5753 self.version_history
5754 .read()
5755 .latest_version_ref()
5756 .active_memtable
5757 .insert_batch_with_kv_algo(items, kv_algo)
5758 }
5759
5760 /// Recovers previous state, by loading the level manifest, tables and blob files.
5761 ///
5762 /// # Errors
5763 ///
5764 /// Returns error, if an IO error occurred.
5765 #[expect(
5766 clippy::too_many_lines,
5767 reason = "Tree::recover threads the whole open sequence (snapshot validation, \
5768 Manifest decode, encryption + runtime plumbing, version recovery, \
5769 TreeInner assembly) — splitting it would create helper functions whose \
5770 only caller is this one site"
5771 )]
5772 fn recover(
5773 mut config: Config,
5774 // The cross-process directory lock acquired by `Tree::open` before the
5775 // manifest probe; held for the tree's lifetime via
5776 // `TreeInner::directory_lock`.
5777 #[cfg(feature = "std")] directory_lock: Option<Box<dyn crate::fs::FsFile>>,
5778 ) -> crate::Result<Self> {
5779 use crate::stop_signal::StopSignal;
5780 use inner::get_next_tree_id;
5781
5782 log::info!("Recovering LSM-tree at {}", config.path.display());
5783
5784 // Validate manifest metadata (format version, comparator name)
5785 // BEFORE recover_levels, so a rejected open is side-effect free
5786 // — recover_levels loads tables and cleans up orphans.
5787 // Tree type is checked after recovery (needs the Version object).
5788 // NOTE: the version file is read twice (here for metadata, then inside
5789 // recover_levels for table/blob data). This is intentional — metadata
5790 // validation must complete before any disk-mutating recovery work.
5791 // Version id of the installed on-disk snapshot. This is the base the
5792 // edit log replays on top of; the live version id can be higher (it
5793 // has no `v{id}` file of its own). Threaded into the version history
5794 // so the next persist appends to / rotates the right snapshot's log.
5795 let snapshot_id = crate::version::recovery::installed_snapshot(&config.path, &*config.fs)?
5796 .ok_or(crate::Error::ManifestFooterInvalid(
5797 "the tree holds no installed manifest snapshot",
5798 ))?;
5799 {
5800 let version_id = snapshot_id;
5801 let manifest_path = config.path.join(format!("v{version_id}"));
5802 // Open the manifest with a default runtime snapshot: ECC
5803 // awareness is captured per-Block via the header
5804 // (`ECC_PARITY` flag), so the reader does not need to know
5805 // which ECC mode the writer used and nothing on this path
5806 // reads the runtime.
5807 let mut archive_reader = crate::manifest_blocks::reader::ManifestArchiveReader::open(
5808 &manifest_path,
5809 &*config.fs,
5810 alloc::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
5811 config.encryption.clone(),
5812 )?;
5813 let manifest = Manifest::decode_from(&mut archive_reader)?;
5814
5815 // V6 is the only variant `FormatVersion` can decode to (the
5816 // engine reads exactly one on-disk format, no legacy paths), so
5817 // anything else already failed above: on its framing if the
5818 // manifest is not shaped like the current one, on the version
5819 // field if it is. This match stays as the explicit gate the
5820 // format contract documents — and as the compile-time hook that
5821 // forces a review of the open path when a new variant is added.
5822 match manifest.version {
5823 FormatVersion::V6 => {}
5824 }
5825
5826 let supplied_name = config.comparator.name();
5827 if manifest.comparator_name != supplied_name {
5828 log::warn!(
5829 "Comparator mismatch: tree was created with {:?} but opened with {:?}",
5830 manifest.comparator_name,
5831 supplied_name,
5832 );
5833 return Err(crate::Error::ComparatorMismatch {
5834 stored: manifest.comparator_name,
5835 supplied: supplied_name,
5836 });
5837 }
5838
5839 // IMPORTANT: Restore persisted config
5840 config.level_count = manifest.level_count;
5841 }
5842
5843 let tree_id = get_next_tree_id();
5844
5845 #[cfg(feature = "metrics")]
5846 let metrics = Arc::new(Metrics::default());
5847
5848 let (version, pooled) = Self::recover_levels(
5849 &config.path,
5850 tree_id,
5851 &config,
5852 #[cfg(feature = "metrics")]
5853 &metrics,
5854 )?;
5855
5856 {
5857 let requested_tree_type = match config.kv_separation_opts {
5858 Some(_) => crate::TreeType::Blob,
5859 None => crate::TreeType::Standard,
5860 };
5861
5862 if version.tree_type() != requested_tree_type {
5863 log::error!(
5864 "Tried to open a {requested_tree_type:?}Tree, but the existing tree is of type {:?}Tree. This indicates a misconfiguration or corruption.",
5865 version.tree_type(),
5866 );
5867 // A dedicated error, NOT `Unrecoverable`: the auto-repair path
5868 // answers `Unrecoverable` with a manifest rebuild, and a
5869 // rebuild under the mismatched type commits the wrong tree
5870 // shape (a Standard rebuild of a blob tree strands its blob
5871 // files for the orphan sweep). A configuration error must
5872 // propagate to the caller instead.
5873 return Err(crate::Error::TreeTypeMismatch {
5874 requested: requested_tree_type,
5875 actual: version.tree_type(),
5876 });
5877 }
5878 }
5879
5880 // Table ids and L0 recency keys share one counter, and a flushed or
5881 // ingested table's recency is an id taken that no table carries: the
5882 // next id has to be above both, or a flush after the reopen would lay
5883 // out behind one it is newer than. The pooled files hold ids of their
5884 // own, which no later table may take.
5885 let highest_table_id = version
5886 .iter_tables()
5887 .map(|table| table.id().max(table.l0_recency()))
5888 .chain(pooled.iter().flat_map(|(_, _, ids)| ids.iter().copied()))
5889 .max()
5890 .unwrap_or_default();
5891 let table_id_counter = SequenceNumberCounter::new(highest_table_id + 1);
5892 let life = crate::close::Life::new();
5893 let table_files = Arc::new(crate::table::file_pool::TableFilePools::for_tree(
5894 table_id_counter.clone(),
5895 config.table_file_pool,
5896 config.sync_mode,
5897 config.table_file_refill,
5898 Arc::clone(&life),
5899 ));
5900 for (folder, folder_fs, ids) in pooled {
5901 table_files.adopt(&folder, &folder_fs, ids);
5902 }
5903
5904 let comparator = config.comparator.clone();
5905
5906 let deletion_pause = crate::deletion_pause::DeletionPause::for_tree(Arc::clone(&life));
5907 #[cfg(feature = "std")]
5908 let background_deleter =
5909 Arc::new(crate::BackgroundDeleter::for_tree(None, Arc::clone(&life)));
5910 let heal_hints =
5911 crate::heal_hints::HealHints::new_shared(config.initial_runtime_config.auto_heal);
5912
5913 // Clone the seed snapshot BEFORE moving config into the Arc
5914 // below — the runtime handle initializer needs it after the
5915 // move.
5916 let initial_runtime = config.initial_runtime_config.clone();
5917 let sync_mode = config.sync_mode;
5918 // Same reason: built before the move.
5919 let compaction_rate_limiter = config.tree_compaction_rate_limiter();
5920 // A strategy reads the compaction budget from the config: given the
5921 // live limiter, it sees a rate changed after open.
5922 config.live_compaction_rate_limiter = Some(Arc::clone(&compaction_rate_limiter));
5923 let compaction_capacity = config.tree_compaction_capacity();
5924 let write_rate_limiter = config.tree_write_rate_limiter();
5925 // A compaction throttled by the limiter sleeps until its deadline;
5926 // dropping the tree has to wake it.
5927 let stop_signal = StopSignal::default();
5928 #[cfg(feature = "std")]
5929 stop_signal.wake_on_stop(&compaction_rate_limiter);
5930 let filter_budget = Arc::<crate::filter_budget::FilterBudget>::default();
5931 // The open installed the recovered state as its own snapshot.
5932 let snapshot_id = version.id();
5933 let mut super_versions = SuperVersions::new(
5934 version,
5935 &comparator,
5936 sync_mode,
5937 snapshot_id,
5938 config.manifest_log_rotate_bytes,
5939 );
5940 if config.filter_advisor.is_some() {
5941 super_versions = super_versions.with_filter_budget(Arc::clone(&filter_budget));
5942 }
5943 #[cfg(feature = "std")]
5944 let latest_super_version = super_versions.latest_handle();
5945 #[cfg(feature = "std")]
5946 let insert_gate = super_versions.insert_gate_handle();
5947 #[cfg(feature = "std")]
5948 if let Some(budget) = &config.write_buffer_budget {
5949 budget.register(Arc::downgrade(&latest_super_version));
5950 }
5951 let inner = TreeInner {
5952 id: tree_id,
5953 life,
5954 closing: core::sync::atomic::AtomicBool::new(false),
5955 memtable_id_counter: SequenceNumberCounter::new(1),
5956 table_id_counter,
5957 table_files,
5958 blob_file_id_counter: SequenceNumberCounter::default(),
5959 version_history: Arc::new(RwLock::new(super_versions)),
5960 #[cfg(feature = "std")]
5961 latest_super_version,
5962 #[cfg(feature = "std")]
5963 insert_gate,
5964 stop_signal,
5965 config: Arc::new(config),
5966 major_compaction_lock: RwLock::default(),
5967 flush_lock: Mutex::default(),
5968 #[cfg(feature = "std")]
5969 directory_lock: directory_lock.map(Arc::from),
5970 compaction_state: Arc::new(Mutex::new(CompactionState::default())),
5971 deletion_pause: Arc::clone(&deletion_pause),
5972 #[cfg(feature = "std")]
5973 background_deleter: Arc::clone(&background_deleter),
5974 heal_hints: Arc::clone(&heal_hints),
5975 filter_budget,
5976 compaction_rate_limiter,
5977 compaction_capacity,
5978 write_rate_limiter,
5979 kv_digest_at_insert: portable_atomic::AtomicU8::new(inner::kv_digest_at_insert_gate(
5980 &initial_runtime,
5981 )),
5982 runtime_config: Arc::new(crate::runtime_config::handle::RuntimeConfigHandle::new(
5983 initial_runtime,
5984 )),
5985 admission_used_cache: Mutex::new(None),
5986 #[cfg(zstd_any)]
5987 retired_write_snapshots: Mutex::new(Vec::new()),
5988
5989 #[cfg(feature = "metrics")]
5990 metrics,
5991
5992 #[cfg(test)]
5993 test_hooks: inner::TestHooks::default(),
5994 };
5995
5996 // Install the pause on every recovered table / blob file so their
5997 // Drop impls consult it when a checkpoint is in flight. Snapshot
5998 // the Arc handles into owned collections so the read lock is
5999 // released before iterating (avoids `significant_drop_tightening`).
6000 // Snapshot the version under the read lock, then drop the lock before
6001 // collecting so the version_history lock isn't held across the clones.
6002 let version = inner.version_history.read().latest_version().version;
6003 let recovered_tables: Vec<Table> = version.iter_tables().cloned().collect();
6004 let recovered_blobs: Vec<BlobFile> = version.blob_files.iter().cloned().collect();
6005
6006 let sinks = crate::table::TableSinks {
6007 deletion_pause: &deletion_pause,
6008 heal_hints: &heal_hints,
6009 read_budget: inner.config.columnar_read_budget,
6010 #[cfg(feature = "std")]
6011 background_deleter: &background_deleter,
6012 track_filter_probes: inner.config.filter_advisor.is_some(),
6013 };
6014 for table in &recovered_tables {
6015 table.bind_to_tree(&sinks);
6016 }
6017 for blob_file in &recovered_blobs {
6018 blob_file.bind_to_tree(&sinks);
6019 }
6020
6021 // Re-arm the tight-space reclaims a previous session could not finish.
6022 // A reclaim deferred because a checkpoint still hard-linked the file
6023 // lived only in that session's queue, and the unrestricted view that
6024 // could re-arm it is gone once the process restarts, so the consumed
6025 // prefix would stay allocated for the table's lifetime. Nothing is
6026 // persisted for this: the intent is DERIVABLE, and the extent is
6027 // exactly the prefix the committed bound cuts away. A prefix already
6028 // punched re-punches as a no-op, so a completed reclaim costs one
6029 // call, and only on a restricted table.
6030 #[cfg(feature = "std")]
6031 {
6032 for table in &recovered_tables {
6033 let Some(bound) = table.restrict_lower_bound() else {
6034 continue;
6035 };
6036 // Reclaiming frees space; it never decides what the tree
6037 // serves. A table that opened cleanly must not be denied to
6038 // readers because its punch offset cannot be re-derived, so a
6039 // failure here is reported and skipped. An ENVIRONMENTAL fault
6040 // still propagates: it says nothing about this table, and a
6041 // retry can re-read it.
6042 let offset = match table.punch_offset_for(bound) {
6043 Ok(offset) => offset,
6044 Err(e) if e.is_environmental() => return Err(e),
6045 Err(e) => {
6046 log::warn!(
6047 "table {} carries a committed restriction whose punch offset \
6048 could not be re-derived ({e}); its consumed prefix stays \
6049 allocated until the next tight-space compaction",
6050 table.id(),
6051 );
6052 continue;
6053 }
6054 };
6055 if offset > 0 {
6056 deletion_pause.retain_reclaim(
6057 Arc::clone(&table.fs),
6058 (*table.path).clone(),
6059 alloc::vec![(0, offset)],
6060 );
6061 }
6062 }
6063 // Blob files carry the same deferred intent, in their committed
6064 // frontier rather than a restriction bound.
6065 for blob in &recovered_blobs {
6066 let extent = match blob.committed_reclaimable_prefix() {
6067 Ok(Some(extent)) => extent,
6068 Ok(None) => continue,
6069 Err(e) if e.is_environmental() => return Err(e),
6070 Err(e) => {
6071 log::warn!(
6072 "blob file {:?} carries a committed frontier whose reclaimable \
6073 extent could not be re-derived ({e}); its consumed prefix stays \
6074 allocated until the next relocation",
6075 blob.id(),
6076 );
6077 continue;
6078 }
6079 };
6080 deletion_pause.retain_reclaim(
6081 Arc::clone(&blob.0.fs),
6082 blob.0.path.clone(),
6083 alloc::vec![extent],
6084 );
6085 }
6086 deletion_pause.retry_pending_reclaims();
6087 }
6088
6089 Ok(Self(Arc::new(inner)))
6090 }
6091
6092 /// Creates a new LSM-tree in a directory.
6093 fn create_new(
6094 config: Config,
6095 // The folders that gained an entry when `Tree::open` made the tree
6096 // folder, to lock it.
6097 made_by_open: &[crate::path::PathBuf],
6098 // Whether this creation syncs those folders, or its caller does.
6099 names: NewFolderNames,
6100 // The cross-process directory lock acquired by `Tree::open`, held for
6101 // the tree's lifetime.
6102 #[cfg(feature = "std")] directory_lock: Option<Box<dyn crate::fs::FsFile>>,
6103 ) -> crate::Result<Self> {
6104 use crate::file::{BLOBS_FOLDER, fsync_directory};
6105 use crate::fs::SyncMode;
6106 use crate::path::PathBuf;
6107
6108 /// Adds `folder` on `fs` to `folders` unless it is there already.
6109 fn once(folders: &mut Vec<(PathBuf, Arc<dyn Fs>)>, folder: &Path, fs: &Arc<dyn Fs>) {
6110 if !folders
6111 .iter()
6112 .any(|(at, on)| at == folder && Arc::ptr_eq(on, fs))
6113 {
6114 folders.push((folder.to_path_buf(), Arc::clone(fs)));
6115 }
6116 }
6117
6118 let path = config.path.clone();
6119 log::trace!("Creating LSM-tree at {}", path.display());
6120
6121 let sync_mode = config.sync_mode;
6122
6123 // Every folder that gains a new entry is synced once, before the first
6124 // snapshot is written, so a sync that fails fails the creation before
6125 // there is a tree for a retry to open instead. The tree folder on the
6126 // tree's own backend is the one exception: the directory sync that
6127 // makes the snapshot's name durable makes every name made in it
6128 // durable too. A route on another backend holds its own names, even
6129 // one whose path lies inside the tree folder. The folders above the
6130 // tree folder are left to the caller under `NewFolderNames::Deferred`,
6131 // which syncs them once for a batch of trees after all of them.
6132 let mut holders: Vec<(PathBuf, Arc<dyn Fs>)> = Vec::new();
6133 // Every folder a chain made, on its backend: what a failed creation
6134 // removes, so a retry makes the chain anew and syncs it, rather than
6135 // finding folders whose names were never made durable.
6136 let mut made: Vec<(PathBuf, Arc<dyn Fs>)> = Vec::new();
6137 // The open made the tree folder already, to lock it, and every folder
6138 // above it that did not exist: their names are new all the same.
6139 for folder in made_folders(&path, made_by_open) {
6140 once(&mut made, &folder, &config.fs);
6141 }
6142 if names == NewFolderNames::Synced {
6143 for holder in made_by_open {
6144 once(&mut holders, holder, &config.fs);
6145 }
6146 }
6147
6148 let built = (|| -> crate::Result<()> {
6149 // A tree folder that a restore made and locked before this open:
6150 // probed under the cleanup, so a probe that fails takes back the
6151 // folders found missing before it rather than leave them unsynced.
6152 let tree_holders = new_entry_holders(&*config.fs, &path)?;
6153 for folder in made_folders(&path, &tree_holders) {
6154 once(&mut made, &folder, &config.fs);
6155 }
6156 for holder in &tree_holders {
6157 once(&mut holders, holder, &config.fs);
6158 }
6159 (*config.fs).create_dir_all(&path)?;
6160 for (tables_folder, folder_fs) in config.all_tables_folders() {
6161 let covered = Arc::ptr_eq(&folder_fs, &config.fs);
6162 let folder_holders = new_entry_holders(&*folder_fs, &tables_folder)?;
6163 for folder in made_folders(&tables_folder, &folder_holders) {
6164 once(&mut made, &folder, &folder_fs);
6165 }
6166 for holder in folder_holders {
6167 if !(covered && holder == path) {
6168 once(&mut holders, &holder, &folder_fs);
6169 }
6170 }
6171 folder_fs.create_dir_all(&tables_folder)?;
6172 }
6173 if config.kv_separation_opts.is_some() {
6174 let blobs = path.join(BLOBS_FOLDER);
6175 if !(*config.fs).exists(&blobs)? {
6176 once(&mut made, &blobs, &config.fs);
6177 }
6178 (*config.fs).create_dir_all(&blobs)?;
6179 }
6180 // A barrier orders syncs on its own device only: the last sync of
6181 // the creation on each other device than the tree's is a full one,
6182 // and the ones before it there are ordered ahead of it.
6183 let tree_volume = config.fs.volume_id(&path);
6184 let volumes: Vec<Option<u64>> = holders
6185 .iter()
6186 .map(|(folder, folder_fs)| folder_fs.volume_id(folder))
6187 .collect();
6188 for (at, (folder, folder_fs)) in holders.iter().enumerate() {
6189 let volume = volumes.get(at).copied().flatten();
6190 let foreign = volume.is_none() || volume != tree_volume;
6191 let last_there =
6192 volume.is_none() || !volumes.iter().skip(at + 1).any(|later| *later == volume);
6193 let mode = if sync_mode == SyncMode::Barrier && foreign && last_there {
6194 SyncMode::Full
6195 } else {
6196 sync_mode
6197 };
6198 fsync_directory(folder, &**folder_fs, mode)?;
6199 }
6200 Ok(())
6201 })();
6202
6203 let config_fs = Arc::clone(&config.fs);
6204 let created = built.and_then(|()| {
6205 TreeInner::create_new(
6206 config,
6207 #[cfg(feature = "std")]
6208 directory_lock,
6209 )
6210 });
6211 match created {
6212 Ok(inner) => Ok(Self(Arc::new(inner))),
6213 Err(e) => {
6214 undo_creation(Some((&path, &config_fs)), made, &holders);
6215 Err(e)
6216 }
6217 }
6218 }
6219
6220 /// Recovers the level manifest, loading all tables from disk.
6221 ///
6222 /// When [`level_routes`](Config::level_routes) is configured, all
6223 /// configured table folders are scanned so tables on different storage
6224 /// tiers are discovered correctly.
6225 #[expect(
6226 clippy::too_many_lines,
6227 reason = "recovery logic is inherently complex"
6228 )]
6229 fn recover_levels<P: AsRef<Path>>(
6230 tree_path: P,
6231 tree_id: TreeId,
6232 config: &Config,
6233 #[cfg(feature = "metrics")] metrics: &Arc<Metrics>,
6234 ) -> crate::Result<(Version, PooledFiles)> {
6235 use crate::{TableId, file::fsync_directory};
6236
6237 let tree_path = tree_path.as_ref();
6238
6239 let recovery = recover(
6240 tree_path,
6241 &*config.fs,
6242 config.manifest_recovery_mode,
6243 config.encryption.clone(),
6244 )?;
6245
6246 // The installed on-disk snapshot: the generation orphan cleanup
6247 // must preserve. Intermediate versions live only in the edit log, so the
6248 // latest version id (`version.id()`) has no `v{id}` file of its own.
6249 let snapshot_id = recovery.snapshot_id;
6250 let mut sweep = OpenSweep::default();
6251
6252 let mut table_map = {
6253 let mut result: crate::HashMap<
6254 TableId,
6255 (
6256 u8, /* Level index */
6257 Checksum,
6258 SeqNo,
6259 TableId, /* L0 recency */
6260 ),
6261 > = crate::HashMap::default();
6262
6263 for (level_idx, table_ids) in recovery.table_ids.iter().enumerate() {
6264 for run in table_ids {
6265 for table in run {
6266 #[expect(
6267 clippy::expect_used,
6268 reason = "there are always less than 256 levels"
6269 )]
6270 result.insert(
6271 table.id,
6272 (
6273 level_idx
6274 .try_into()
6275 .expect("there are less than 256 levels"),
6276 table.checksum,
6277 table.global_seqno,
6278 table.recency,
6279 ),
6280 );
6281 }
6282 }
6283 }
6284
6285 result
6286 };
6287
6288 let cnt = table_map.len();
6289
6290 // Immutable snapshot of every table id the manifest knows. `table_map`
6291 // is drained as tables are recovered below, so it cannot answer "is this
6292 // id live?" order-independently; this set can. Used to sweep an orphaned
6293 // `.heal-attest` sidecar whose SST was retired.
6294 let manifest_ids: crate::HashSet<TableId> = table_map.keys().copied().collect();
6295
6296 log::debug!("Recovering {cnt} tables from {}", tree_path.display());
6297
6298 let progress_mod = match cnt {
6299 _ if cnt <= 20 => 1,
6300 _ if cnt <= 100 => 10,
6301 _ => 100,
6302 };
6303
6304 let mut tables = vec![];
6305 // Track recovered table IDs so duplicate sightings (via symlinks,
6306 // junctions, or case-insensitive aliases of the same directory) are
6307 // skipped rather than orphan-deleted.
6308 let mut recovered_table_ids: crate::HashSet<TableId> = crate::HashSet::default();
6309 let mut orphaned_tables: Vec<(crate::path::PathBuf, Arc<dyn crate::fs::Fs>)> = vec![];
6310 // Copies the digest PROVED stale. Left on disk they are worse than
6311 // clutter: once the winning route is removed or temporarily unmounted,
6312 // such a file is the only sighting of its id, arbitration is skipped
6313 // for want of a duplicate, and the stale generation is served instead
6314 // of the missing route being reported. They are swept only after a
6315 // winner for that id is established, so a scan that never finds one
6316 // keeps every copy it has.
6317 let mut rejected_copies: Vec<(TableId, crate::path::PathBuf, Arc<dyn crate::fs::Fs>)> =
6318 Vec::new();
6319 // Where an ambiguous id was accepted from, so a LATER sighting of it can
6320 // be judged against the winner rather than merely skipped.
6321 let mut accepted_copies: crate::HashMap<
6322 TableId,
6323 (crate::path::PathBuf, Arc<dyn crate::fs::Fs>),
6324 > = crate::HashMap::default();
6325 // First recovery failure per manifest id, kept until a later routed
6326 // folder yields a copy that opens. Whatever is left once every folder
6327 // is scanned is a table the manifest names and no copy delivers, so
6328 // its error is the open's.
6329 let mut unrecovered_sightings: crate::HashMap<TableId, crate::Error> =
6330 crate::HashMap::default();
6331 // Repair replacements whose authority could not be settled against the
6332 // damaged original beside them. Held until every folder is scanned: a
6333 // later routed copy that opens against the manifest settles it (the temp
6334 // is then provably NOT what the manifest names, so it is swept there and
6335 // then), and only an id no copy delivers surfaces its ambiguity.
6336 // A LIST, not a per-id map: routing can put a temp for the same id in
6337 // more than one folder, and each one is a file that has to be settled.
6338 let mut deferred_temps: Vec<(
6339 TableId,
6340 crate::path::PathBuf,
6341 Arc<dyn crate::fs::Fs>,
6342 crate::Error,
6343 )> = Vec::new();
6344
6345 // Scan all configured table folders (primary + level routes).
6346 let all_folders = config.all_tables_folders();
6347
6348 // One listing per folder, taken up front. The recovery loop has to know
6349 // whether an id is sighted in MORE THAN ONE routed folder BEFORE it
6350 // accepts the first sighting, and answering that later would mean
6351 // listing every folder a second time.
6352 let mut folder_scans: Vec<(
6353 &crate::path::PathBuf,
6354 &Arc<dyn crate::fs::Fs>,
6355 Vec<crate::fs::FsDirEntry>,
6356 )> = Vec::with_capacity(all_folders.len());
6357 // Ids present in more than one folder. Only these are digest-arbitrated
6358 // at open: a post-commit sweep that failed leaves an INTACT stale twin,
6359 // and `Table::recover` parses structure without re-deriving the
6360 // manifest's digest, so it opens the stale generation just as happily.
6361 // Ids sighted once cannot be ambiguous, and hashing them would turn
6362 // every open into a full read of the tree.
6363 let mut folder_sightings: crate::HashMap<TableId, usize> = crate::HashMap::default();
6364 for (table_base_folder, folder_fs) in &all_folders {
6365 if !folder_fs.exists(table_base_folder)? {
6366 folder_fs.create_dir_all(table_base_folder)?;
6367 fsync_directory(table_base_folder, &**folder_fs, config.sync_mode)?;
6368 if let Some(parent) = table_base_folder.parent() {
6369 fsync_directory(parent, &**folder_fs, config.sync_mode)?;
6370 if let Some(grandparent) = parent.parent() {
6371 fsync_directory(grandparent, &**folder_fs, config.sync_mode)?;
6372 }
6373 }
6374 }
6375
6376 // Pending repair swaps resolve FIRST: a committed swap's `{id}` file
6377 // is the superseded source until the rename lands, so adopting it
6378 // before the temp entry is processed would hand this session a
6379 // handle onto bytes the finished swap then replaces on disk.
6380 let mut dirents = folder_fs.read_dir(table_base_folder)?;
6381 dirents.sort_by_key(|e| {
6382 !matches!(
6383 crate::file::TableDirEntry::classify(&e.file_name),
6384 crate::file::TableDirEntry::RepairTmp(_)
6385 )
6386 });
6387 for dirent in &dirents {
6388 if let crate::file::TableDirEntry::Table(id) =
6389 crate::file::TableDirEntry::classify(&dirent.file_name)
6390 {
6391 *folder_sightings.entry(id).or_insert(0) += 1;
6392 }
6393 }
6394 folder_scans.push((table_base_folder, folder_fs, dirents));
6395 }
6396 let ambiguous_ids: crate::HashSet<TableId> = folder_sightings
6397 .into_iter()
6398 .filter(|(_, count)| *count > 1)
6399 .map(|(id, _)| id)
6400 .collect();
6401
6402 // The empty table files no version names, per folder: the pool each
6403 // folder kept, adopted by the tree.
6404 let mut pooled: PooledFiles = Vec::new();
6405 let mut pooled_ids: Vec<TableId> = Vec::new();
6406 for (table_base_folder, folder_fs, dirents) in folder_scans {
6407 for dirent in dirents {
6408 let crate::fs::FsDirEntry {
6409 path: table_file_path,
6410 file_name,
6411 is_dir,
6412 } = dirent;
6413
6414 let table_file_name = &file_name;
6415 if is_dir {
6416 log::warn!(
6417 "Skipping unexpected directory in tables folder: {}",
6418 table_file_path.display()
6419 );
6420 continue;
6421 }
6422
6423 // One grammar decides what each name IS (`TableDirEntry`, shared
6424 // with the repair scan so the two can never disagree on
6425 // ownership); this match is the open's POLICY for each kind.
6426 let table_id = match crate::file::TableDirEntry::classify(table_file_name) {
6427 // An in-place heal's detach copy, renamed over the live path
6428 // on success: a survivor is a crash leftover no manifest ever
6429 // references. Sweep it.
6430 crate::file::TableDirEntry::HealTmp(_) => {
6431 log::warn!(
6432 "Removing abandoned heal copy: {}",
6433 table_file_path.display()
6434 );
6435 sweep.remove(folder_fs, &table_file_path);
6436 continue;
6437 }
6438 // A crashed attestation publish: either the live sidecar it
6439 // would have replaced still bridges the crash window, or the
6440 // heal is re-run. Disposable.
6441 crate::file::TableDirEntry::HealAttestTmp(_) => {
6442 log::warn!(
6443 "Removing abandoned heal-attest temp: {}",
6444 table_file_path.display()
6445 );
6446 sweep.remove(folder_fs, &table_file_path);
6447 continue;
6448 }
6449 // A LIVE table's pending attestation is preserved (the next
6450 // scrub reconciles a crashed digest refresh through it). One
6451 // whose SST left the manifest is unreconcilable forever:
6452 // sweep the orphan rather than re-process it on every open.
6453 crate::file::TableDirEntry::HealAttest(attest_id) => {
6454 if !manifest_ids.contains(&attest_id) {
6455 log::warn!(
6456 "Removing orphaned heal attestation (its table is gone): {}",
6457 table_file_path.display()
6458 );
6459 sweep.remove(folder_fs, &table_file_path);
6460 }
6461 continue;
6462 }
6463 // A crashed bound publish. Disposable.
6464 crate::file::TableDirEntry::RestrictBoundTmp(_) => {
6465 log::warn!(
6466 "Removing abandoned restrict-bound temp: {}",
6467 table_file_path.display()
6468 );
6469 sweep.remove(folder_fs, &table_file_path);
6470 continue;
6471 }
6472 // A LIVE table's restriction bound is preserved (manifest
6473 // repair reads it); an ORPHAN one is swept so a reused id
6474 // cannot later pick up a stale restriction.
6475 crate::file::TableDirEntry::RestrictBound(bound_id) => {
6476 if !manifest_ids.contains(&bound_id) {
6477 log::warn!(
6478 "Removing orphaned restrict-bound sidecar (its table is gone): {}",
6479 table_file_path.display()
6480 );
6481 sweep.remove(folder_fs, &table_file_path);
6482 }
6483 continue;
6484 }
6485 // A repair's replacement still at its temp name. The manifest
6486 // is the authority on what it is — but it names the id in
6487 // BOTH crash cases (before the commit its entry still
6488 // describes the SOURCE beside the temp), so the entry's
6489 // checksum decides: only a committed repair recorded the
6490 // temp's digest, and it died before the swap — this file is
6491 // what the manifest describes, so the swap is finished.
6492 // Any other temp is an abandoned build (possibly truncated
6493 // mid-write), and swapping it in would destroy the source
6494 // the manifest names: it is garbage. An open resolves this
6495 // exactly as a re-run of the repair would, from the durable
6496 // manifest alone.
6497 crate::file::TableDirEntry::RepairTmp(tmp_id) => {
6498 #[cfg(feature = "std")]
6499 {
6500 let published = match table_map.get(&tmp_id) {
6501 Some(&(_, manifest_checksum, _, _)) => {
6502 match crate::repair::repair_tmp_is_published(
6503 config,
6504 folder_fs,
6505 &table_file_path,
6506 tmp_id,
6507 manifest_checksum,
6508 recovery.restrictions.get(&tmp_id),
6509 ) {
6510 Ok(published) => published,
6511 // A refused mount / missing key says
6512 // nothing about which copy the manifest
6513 // names: surface it.
6514 Err(e) if e.is_environmental() => return Err(e),
6515 // Neither this temp nor the damaged
6516 // original beside it can prove it is the
6517 // manifest's copy. A LATER routed folder
6518 // may still hold that copy, and deciding
6519 // here would end the open on a leftover a
6520 // failed post-commit sweep left behind.
6521 // Defer: leave the temp untouched, keep
6522 // the ambiguity, and let the end of the
6523 // scan decide (see `deferred_temps`).
6524 Err(e) => {
6525 log::warn!(
6526 "repair replacement {} is ambiguous ({e}); \
6527 deferring until every routed folder is scanned",
6528 table_file_path.display(),
6529 );
6530 deferred_temps.push((
6531 tmp_id,
6532 table_file_path,
6533 Arc::clone(folder_fs),
6534 e,
6535 ));
6536 continue;
6537 }
6538 }
6539 }
6540 None => false,
6541 };
6542 if published {
6543 log::warn!(
6544 "Finishing a repair's pending swap of table {tmp_id}: {}",
6545 table_file_path.display()
6546 );
6547 crate::repair::commit_repair_tmp(
6548 folder_fs.as_ref(),
6549 &table_file_path,
6550 &table_base_folder.join(tmp_id.to_string()),
6551 config.sync_mode,
6552 recovery.restrictions.contains_key(&tmp_id),
6553 )?;
6554 } else {
6555 log::warn!(
6556 "Removing abandoned repair replacement: {}",
6557 table_file_path.display()
6558 );
6559 // A RESTRICTED salvage also wrote
6560 // `{temp}.restrict-bound`; that companion must
6561 // go with the temp — its name classifies as
6562 // Foreign and would fail this very open.
6563 let companion =
6564 crate::restrict_bound::sidecar_path(&table_file_path);
6565 if folder_fs.exists(&companion)? {
6566 sweep.remove(folder_fs, &companion);
6567 }
6568 sweep.remove(folder_fs, &table_file_path);
6569 }
6570 }
6571 // Without the repair module nothing here can verify or
6572 // finish a swap the manifest may describe, and sweeping
6573 // the temp could destroy the only copy of what the
6574 // manifest names.
6575 #[cfg(not(feature = "std"))]
6576 {
6577 if manifest_ids.contains(&tmp_id) {
6578 log::error!(
6579 "Table {tmp_id} exists only as an unpublished repair \
6580 replacement; run a repair to finish it: {}",
6581 table_file_path.display()
6582 );
6583 return Err(crate::Error::Unrecoverable);
6584 }
6585 log::warn!(
6586 "Removing abandoned repair replacement: {}",
6587 table_file_path.display()
6588 );
6589 // Same companion rule as the std arm above (the
6590 // `restrict_bound` helper is std-gated, so the
6591 // name is spelled out).
6592 let companion = table_base_folder.join(alloc::format!(
6593 "{tmp_id}{}.restrict-bound",
6594 crate::file::REPAIR_TMP_SUFFIX
6595 ));
6596 if folder_fs.exists(&companion)? {
6597 sweep.remove(folder_fs, &companion);
6598 }
6599 sweep.remove(folder_fs, &table_file_path);
6600 }
6601 continue;
6602 }
6603 // The companion's fate followed its temp when that entry
6604 // resolved (first, by sort order): a finished swap renamed
6605 // it into place, an abandoned build's sweep removed it. A
6606 // survivor here is an orphan (its temp is gone) — remove
6607 // it rather than reject the whole open over it.
6608 crate::file::TableDirEntry::RepairTmpCompanion(_) => {
6609 if folder_fs.exists(&table_file_path)? {
6610 log::warn!(
6611 "Removing orphaned repair-replacement sidecar: {}",
6612 table_file_path.display()
6613 );
6614 sweep.remove(folder_fs, &table_file_path);
6615 }
6616 continue;
6617 }
6618 // Not a shape the engine names, so not engine state: passed
6619 // over untouched. Refusing the store here would let any
6620 // stray file (an operator's note, a backup, a desktop
6621 // environment's directory metadata) make it unopenable,
6622 // which is why scanners used to carry a list of foreign
6623 // names to tolerate. The grammar answers it instead.
6624 crate::file::TableDirEntry::Foreign => {
6625 log::debug!(
6626 "Ignoring {table_file_name:?} in the tables folder: not an engine file"
6627 );
6628 continue;
6629 }
6630 crate::file::TableDirEntry::Table(id) => id,
6631 };
6632
6633 // Remove from map to prevent duplicate recovery if the same
6634 // table file exists in multiple scanned folders.
6635 if let Some(entry) = table_map.remove(&table_id) {
6636 let (level_idx, checksum, global_seqno, recency) = entry;
6637 let pin_filter = config.filter_block_pinning_policy.get(level_idx.into());
6638 let pin_index = config.index_block_pinning_policy.get(level_idx.into());
6639
6640 let table = {
6641 let mut params = crate::table::RecoverParams::new(
6642 table_file_path.clone(),
6643 checksum,
6644 table_id,
6645 folder_fs.clone(),
6646 config.comparator.clone(),
6647 config.cache.clone(),
6648 );
6649 params.global_seqno = global_seqno;
6650 params.recency = Some(recency);
6651 params.tree_id = tree_id;
6652 params.descriptor_table.clone_from(&config.descriptor_table);
6653 params.pin_filter = pin_filter;
6654 params.pin_index = pin_index;
6655 params.encryption.clone_from(&config.encryption);
6656 params.prefix_extractor.clone_from(&config.prefix_extractor);
6657 #[cfg(zstd_any)]
6658 {
6659 params.zstd_dictionaries = config.current_zstd_dictionaries();
6660 }
6661 #[cfg(feature = "metrics")]
6662 {
6663 params.metrics = metrics.clone();
6664 }
6665 match Table::recover(params) {
6666 Ok(table) => table,
6667 Err(e) => {
6668 // A routed tree can hold this id in more than
6669 // one folder, and repair's post-commit sweep of
6670 // the copy it displaced can fail after the
6671 // manifest is already durable. Ending the search
6672 // on the first sighting would then shut the tree
6673 // on a table that is present and intact one
6674 // folder over. Put the id back, remember the
6675 // failure, and keep scanning; if no folder
6676 // yields the manifest's copy, this error is what
6677 // the open reports.
6678 log::warn!(
6679 "table {table_id} at {} did not recover ({e}); \
6680 looking for another routed copy",
6681 table_file_path.display(),
6682 );
6683 table_map.insert(table_id, entry);
6684 unrecovered_sightings.entry(table_id).or_insert(e);
6685 continue;
6686 }
6687 }
6688 };
6689
6690 // Opening proves the file is STRUCTURALLY sound, never that
6691 // it is the generation the manifest committed: recover parses
6692 // metadata and loads data blocks lazily. Where two folders
6693 // hold this id, take the digest as the arbiter, exactly as
6694 // repair does, so a stale twin an interrupted sweep left
6695 // behind cannot win on scan order.
6696 if ambiguous_ids.contains(&table_id) {
6697 // The candidate is UNRESTRICTED here: the manifest's
6698 // restrictions are attached when the version is built,
6699 // which is after this scan. Digesting it whole would
6700 // compare against a live-suffix digest and reject every
6701 // copy of a restricted id, so the bound is supplied
6702 // explicitly.
6703 let live =
6704 match table.suffix_checksum_for(recovery.restrictions.get(&table_id)) {
6705 Ok(live) => live,
6706 // A fault in the ENVIRONMENT says nothing about
6707 // these bytes, so it propagates and a retry can
6708 // re-read them.
6709 Err(e) if e.is_environmental() => return Err(e),
6710 // Anything else is damage to THIS copy: metadata
6711 // can parse while a data extent is unreadable, and
6712 // ending the scan on it would let a damaged
6713 // displaced copy permanently block the intact one a
6714 // folder over. Same treatment as a failed recover.
6715 Err(e) => {
6716 log::warn!(
6717 "table {table_id} at {} could not be digested ({e}); \
6718 looking for another routed copy",
6719 table_file_path.display(),
6720 );
6721 table_map.insert(table_id, entry);
6722 unrecovered_sightings.entry(table_id).or_insert(e);
6723 continue;
6724 }
6725 };
6726 if live != checksum {
6727 log::warn!(
6728 "table {table_id} at {} is not the generation the manifest \
6729 committed; looking for another routed copy",
6730 table_file_path.display(),
6731 );
6732 table_map.insert(table_id, entry);
6733 unrecovered_sightings.entry(table_id).or_insert(
6734 crate::Error::ChecksumMismatch {
6735 got: live,
6736 expected: checksum,
6737 },
6738 );
6739 rejected_copies.push((
6740 table_id,
6741 table_file_path,
6742 Arc::clone(folder_fs),
6743 ));
6744 continue;
6745 }
6746 accepted_copies
6747 .insert(table_id, (table_file_path.clone(), Arc::clone(folder_fs)));
6748 }
6749
6750 tables.push(table);
6751 recovered_table_ids.insert(table_id);
6752 unrecovered_sightings.remove(&table_id);
6753
6754 if tables.len() % progress_mod == 0 {
6755 log::debug!("Recovered {}/{cnt} tables", tables.len());
6756 }
6757 } else if recovered_table_ids.contains(&table_id) {
6758 // Duplicate sighting of an already-recovered manifest table
6759 // (e.g., via symlink or case-insensitive alias). Never an
6760 // orphan: that would delete the live SST. But an id the
6761 // digest arbitrated has a KNOWN answer, so a later sighting
6762 // is judged rather than merely skipped, or a stale twin
6763 // scanned after the winner would outlive the open.
6764 let stale = match accepted_copies.get(&table_id) {
6765 Some((winner, winner_fs)) if *winner != table_file_path => {
6766 differs_byte_for_byte(
6767 &**winner_fs,
6768 winner,
6769 folder_fs.as_ref(),
6770 &table_file_path,
6771 )?
6772 }
6773 _ => false,
6774 };
6775 if stale {
6776 rejected_copies.push((
6777 table_id,
6778 table_file_path.clone(),
6779 Arc::clone(folder_fs),
6780 ));
6781 }
6782 log::warn!(
6783 "Skipping duplicate sighting of manifest table {table_id} in {}",
6784 table_file_path.display(),
6785 );
6786 } else if config.table_file_pool > 0
6787 // The counter starts above every pooled id, so one at its
6788 // top would leave it no id to hand out.
6789 && table_id < crate::seqno::MAX_SEQNO
6790 && folder_fs
6791 .metadata(&table_file_path)
6792 .is_ok_and(|meta| meta.is_file && meta.len == 0)
6793 // A table is written into a pooled file where it lies: a
6794 // symlink would take it to a file the tree never made.
6795 && folder_fs
6796 .read_link(&table_file_path)
6797 .is_ok_and(|target| target.is_none())
6798 {
6799 // No version names it and no table is empty: a pooled
6800 // file, used again rather than made anew. Anything else
6801 // unnamed, or whose kind cannot be read, is an orphan like
6802 // any other.
6803 pooled_ids.push(table_id);
6804 } else {
6805 orphaned_tables.push((table_file_path, folder_fs.clone()));
6806 }
6807 }
6808 if !pooled_ids.is_empty() {
6809 pooled.push((
6810 table_base_folder.clone(),
6811 Arc::clone(folder_fs),
6812 core::mem::take(&mut pooled_ids),
6813 ));
6814 }
6815 }
6816
6817 // Every folder is scanned, so an id that never recovered has no copy
6818 // left to try. Report the failure that stopped it — a missing-table
6819 // diagnostic would hide the reason the file on disk could not be read.
6820 if let Some((_, e)) = unrecovered_sightings
6821 .into_iter()
6822 .find(|(id, _)| table_map.contains_key(id))
6823 {
6824 return Err(e);
6825 }
6826
6827 // A deferred replacement whose id RECOVERED from some folder is settled:
6828 // that copy matched the manifest, so this temp is provably NOT what the
6829 // manifest names — an abandoned build, removed here exactly as the
6830 // in-scan branch removes one. One whose id never arrived is still
6831 // ambiguous: the temp may be the only copy of what the manifest
6832 // describes, so the ambiguity is what the open reports.
6833 #[cfg(feature = "std")]
6834 for (tmp_id, temp_path, temp_fs, ambiguity) in deferred_temps {
6835 if !recovered_table_ids.contains(&tmp_id) {
6836 return Err(ambiguity);
6837 }
6838 log::warn!(
6839 "Removing abandoned repair replacement for table {tmp_id} (the copy this open \
6840 recovered is what the manifest names): {}",
6841 temp_path.display(),
6842 );
6843 // The restricted-salvage companion classifies as Foreign and would
6844 // fail the next open, so it goes with the temp.
6845 let companion = crate::restrict_bound::sidecar_path(&temp_path);
6846 if temp_fs.exists(&companion)? {
6847 sweep.remove(&temp_fs, &companion);
6848 }
6849 sweep.remove(&temp_fs, &temp_path);
6850 }
6851
6852 if tables.len() < cnt {
6853 // Route configuration is NOT persisted. This is a best-effort
6854 // heuristic: it checks each missing table's level against the
6855 // current routes, but cannot detect same-level path changes
6856 // (e.g., L0 routed to /hot_old → /hot_new). Persisting route
6857 // provenance per-table in the manifest would enable exact
6858 // detection but requires a format change.
6859 //
6860 // - Level IS covered by a current route → its directory was scanned
6861 // and the file was not found → data corruption / deletion.
6862 // - Level is NOT covered → falls back to primary (always scanned).
6863 // If the table isn't there, it was likely in a route that has
6864 // since been removed from the config.
6865 //
6866 // Return RouteMismatch only when ALL missing tables are on levels
6867 // not covered by any current route. If ANY missing table is on a
6868 // covered level, at least one SST was genuinely lost.
6869 if let Some(routes) = &config.level_routes {
6870 let all_missing_uncovered = table_map
6871 .values()
6872 .all(|(level, ..)| !routes.iter().any(|r| r.levels.contains(level)));
6873
6874 if all_missing_uncovered {
6875 let found = tables.len();
6876 let missing_ids: Vec<_> = table_map.keys().collect();
6877
6878 log::error!(
6879 "Route mismatch: expected {cnt} tables but found {found} — \
6880 level_routes do not cover all previously used levels. \
6881 Missing table IDs: {missing_ids:?}",
6882 );
6883 return Err(crate::Error::RouteMismatch {
6884 expected: cnt,
6885 found,
6886 });
6887 }
6888 }
6889
6890 log::error!(
6891 "Recovered less tables than expected: {:?}",
6892 table_map.keys(),
6893 );
6894 return Err(crate::Error::Unrecoverable);
6895 }
6896
6897 log::debug!("Successfully recovered {} tables", tables.len());
6898
6899 // Pair each blob file with its live-data frontier: a file whose consumed
6900 // prefix was reclaimed in place records a checksum over the suffix only,
6901 // so the recovered view must carry the offset that digest starts at.
6902 let blob_ids_with_frontier: Vec<(crate::vlog::BlobFileId, crate::Checksum, u64)> = recovery
6903 .blob_file_ids
6904 .iter()
6905 .map(|&(id, checksum)| {
6906 (
6907 id,
6908 checksum,
6909 recovery.blob_restrictions.get(&id).copied().unwrap_or(0),
6910 )
6911 })
6912 .collect();
6913 let (blob_files, orphaned_blob_files) = crate::vlog::recover_blob_files(
6914 &tree_path.join(crate::file::BLOBS_FOLDER),
6915 &blob_ids_with_frontier,
6916 tree_id,
6917 config.descriptor_table.as_ref(),
6918 &config.fs,
6919 #[cfg(zstd_any)]
6920 &config.current_zstd_dictionaries(),
6921 )?;
6922
6923 let version =
6924 Version::from_recovery(recovery, &tables, &blob_files, config.comparator.as_ref())?;
6925
6926 // Registered ids the tree no longer holds are dropped. One a recovered
6927 // file names cannot be among them, since that file would have refused
6928 // to open; the rest owe a reader nothing once the file is gone, while
6929 // every checkpoint copies what the version registers and would fail on
6930 // the missing file.
6931 #[cfg(zstd_any)]
6932 let version = {
6933 let held = config.current_zstd_dictionaries();
6934 if version.dicts().iter().all(|id| held.get(*id).is_some()) {
6935 version
6936 } else {
6937 let dicts = version
6938 .dicts()
6939 .iter()
6940 .copied()
6941 .filter(|id| held.get(*id).is_some())
6942 .collect();
6943 version.with_dicts(dicts)
6944 }
6945 };
6946
6947 // The state this open recovered becomes durable before the tree is
6948 // served: a caller takes the persisted seqno of the tree it opened as
6949 // the point up to which its own log may be trimmed, and what was read
6950 // may be an edit or a snapshot name whose sync failed, which a power
6951 // loss can still take back. Syncing the files read is no proof (a
6952 // sync after a failed one may report success for pages the kernel
6953 // already dropped), so the recovered state is written as a fresh
6954 // snapshot under the next id, installed as a rotation installs one:
6955 // its bytes synced, renamed into place, the folder synced. A repair
6956 // report the tree still owes rides in the version, so the snapshot
6957 // carries it on.
6958 //
6959 // A tree whose id reached the top half of the range goes back to id
6960 // 0 instead. Recovery takes the snapshot with the highest id, so no
6961 // other may be left above the new one: every manifest file but the
6962 // snapshot read goes, durably, before the install, and that snapshot
6963 // goes, durably, right after it. A power loss in between leaves the
6964 // snapshot read as the highest, which a reopen installs again.
6965 let renumbered = version.is_renumbered_on_open();
6966 if renumbered {
6967 log::warn!(
6968 "version {} is in the top half of the id range; reinstalling the tree as \
6969 version 0",
6970 version.id()
6971 );
6972 Self::remove_manifests_but(tree_path, snapshot_id, &config.fs, config.sync_mode)?;
6973 }
6974 let version = version.reinstalled();
6975 crate::version::persist_version(
6976 tree_path,
6977 &version,
6978 config.comparator.name(),
6979 &*config.fs,
6980 Arc::new(config.initial_runtime_config.clone()),
6981 config.encryption.clone(),
6982 config.sync_mode,
6983 )?;
6984 if renumbered {
6985 Self::remove_manifests_but(tree_path, version.id(), &config.fs, config.sync_mode)?;
6986 }
6987
6988 // Republish any restriction sidecar that never landed. The sidecar is
6989 // written AFTER its slice commits, so a failure (or a crash) in that
6990 // window leaves a committed restriction with no `.restrict-bound` file.
6991 // The manifest still describes it, so a normal open is unaffected — but
6992 // a later manifest-loss repair would find an unrestricted input beside
6993 // the slice output and publish BOTH histories, applying every merge
6994 // operand of the consumed prefix twice (operands are deliberately never
6995 // deduplicated across sources). The manifest is the authority for the
6996 // bound, so the missing sidecar is derived from it here, once the
6997 // install above made that manifest durable: derived from a manifest
6998 // still in an unsynced log, it would outlive that manifest when a
6999 // power loss takes it back, and drop an input prefix whose
7000 // replacement went with it.
7001 #[cfg(feature = "std")]
7002 Self::republish_missing_restriction_sidecars(&version, config)?;
7003
7004 // Everything of the generation read goes, now that it is superseded:
7005 // its snapshot and its log.
7006 Self::cleanup_orphaned_version(tree_path, version.id(), &config.fs, &mut sweep)?;
7007
7008 // A copy the digest rejected goes only once its id has a winner: with
7009 // one it is provably superseded, without one it may be all that is
7010 // left. Its `.restrict-bound` companion goes with it, or the next scan
7011 // classifies that name as foreign and fails the open.
7012 for (table_id, path, copy_fs) in rejected_copies {
7013 if !recovered_table_ids.contains(&table_id) {
7014 continue;
7015 }
7016 log::warn!(
7017 "Removing superseded copy of table {table_id}: {}",
7018 path.display()
7019 );
7020 #[cfg(feature = "std")]
7021 {
7022 let companion = crate::restrict_bound::sidecar_path(&path);
7023 if copy_fs.exists(&companion)? {
7024 sweep.remove(©_fs, &companion);
7025 }
7026 }
7027 sweep.remove(©_fs, &path);
7028 }
7029
7030 for (table_path, orphan_fs) in orphaned_tables {
7031 log::debug!("Deleting orphaned table {}", table_path.display());
7032 sweep.remove(&orphan_fs, &table_path);
7033 }
7034
7035 for blob_file_path in orphaned_blob_files {
7036 log::debug!("Deleting orphaned blob file {}", blob_file_path.display());
7037 sweep.remove(&config.fs, &blob_file_path);
7038 }
7039
7040 sweep.finish()?;
7041 Ok((version, pooled))
7042 }
7043
7044 /// Writes the `.restrict-bound` sidecar of every restricted table in
7045 /// `version` that has none, so the bound the manifest holds is recoverable
7046 /// without it. An existing sidecar is REREAD rather than assumed good: its
7047 /// mere presence proves nothing, and a repair that later trusts a corrupt
7048 /// one derives a conservative bound (dropping up to one live block), while
7049 /// a valid-but-STALE one — the shape a second slice leaves when its own
7050 /// write fails — restricts LESS than reality and resurrects consumed rows.
7051 /// A sidecar that already records this table and this bound is left alone,
7052 /// so a healthy tree does not churn the file on every open. Failures
7053 /// propagate — an unrecoverable restriction is exactly what this closes, so
7054 /// silently skipping it would keep the window open.
7055 ///
7056 /// The sidecar is a filesystem artifact of the tight-space reclaim path, so
7057 /// this is a no-op without `std` — a build without it never writes one.
7058 #[cfg(feature = "std")]
7059 fn republish_missing_restriction_sidecars(
7060 version: &Version,
7061 config: &Config,
7062 ) -> crate::Result<()> {
7063 for table in version.iter_tables() {
7064 let Some(bound) = table.restrict_lower_bound() else {
7065 continue;
7066 };
7067 let recorded =
7068 crate::restrict_bound::read(&*table.fs, &table.path, config.encryption.as_deref())?;
7069 let state = match &recorded {
7070 crate::restrict_bound::SidecarRead::Present(id, recorded_bound)
7071 if *id == table.metadata.id && recorded_bound.as_slice() == bound.as_ref() =>
7072 {
7073 continue;
7074 }
7075 crate::restrict_bound::SidecarRead::Present(..) => {
7076 "disagrees with the manifest (stale bound or another table)"
7077 }
7078 crate::restrict_bound::SidecarRead::Corrupt => "unreadable",
7079 crate::restrict_bound::SidecarRead::Missing => "absent",
7080 };
7081 log::warn!(
7082 "table {} carries a committed restriction whose sidecar is {state}; \
7083 republishing it from the manifest",
7084 table.id(),
7085 );
7086 table.write_restrict_sidecar(bound, config.sync_mode)?;
7087 }
7088 Ok(())
7089 }
7090
7091 /// Removes stale manifest files left by older generations: every `v{id}`
7092 /// snapshot except the live one (`v{snapshot_id}`) and every `edits-{id}`
7093 /// log except the live snapshot's (`edits-{snapshot_id}`). A crashed
7094 /// rotation can leak an old snapshot or its log; this sweeps them on open.
7095 /// The live snapshot and log are the installed generation.
7096 ///
7097 /// # Behavior change vs pre-Fs-trait code
7098 ///
7099 /// The previous implementation used `std::fs::read_dir` + `to_string_lossy()`,
7100 /// which silently skipped non-UTF-8 filenames. `Fs::read_dir` returns
7101 /// `InvalidData` for such entries instead (see [`FsDirEntry`](crate::fs::FsDirEntry) docs), so this
7102 /// function now fails fast on non-UTF-8 names. This is intentional: version
7103 /// files are always `v{u64}` — any non-UTF-8 entry indicates filesystem
7104 /// corruption and should surface as an error rather than be silently ignored.
7105 fn cleanup_orphaned_version(
7106 path: &Path,
7107 snapshot_id: crate::version::VersionId,
7108 fs: &Arc<dyn crate::fs::Fs>,
7109 sweep: &mut OpenSweep,
7110 ) -> crate::Result<()> {
7111 use crate::version::recovery::{ManifestFile, manifest_file_of};
7112 for dirent in fs.read_dir(path)? {
7113 if dirent.is_dir {
7114 continue;
7115 }
7116 let orphan = match manifest_file_of(&dirent.file_name) {
7117 Some(ManifestFile::Snapshot(id) | ManifestFile::Log(id)) => id != snapshot_id,
7118 Some(ManifestFile::Temporary(_)) => true,
7119 None => false,
7120 };
7121 if orphan {
7122 log::trace!("Cleanup orphaned manifest file {}", dirent.file_name);
7123 sweep.remove(fs, &dirent.path);
7124 }
7125 }
7126
7127 Ok(())
7128 }
7129
7130 /// Removes every manifest file [`Self::cleanup_orphaned_version`] finds
7131 /// stale beside snapshot `keep`, at once rather than after the install,
7132 /// and makes the removals durable.
7133 ///
7134 /// The snapshots go first, durably, and only then their logs: a snapshot
7135 /// that outlives its log would be recovered without the edits in it.
7136 fn remove_manifests_but(
7137 path: &Path,
7138 keep: crate::version::VersionId,
7139 fs: &Arc<dyn crate::fs::Fs>,
7140 sync_mode: crate::fs::SyncMode,
7141 ) -> crate::Result<()> {
7142 use crate::version::recovery::{ManifestFile, manifest_file_of};
7143 let mut snapshots = OpenSweep::default();
7144 for dirent in fs.read_dir(path)? {
7145 let other = match manifest_file_of(&dirent.file_name) {
7146 Some(ManifestFile::Snapshot(id)) => id != keep,
7147 Some(ManifestFile::Temporary(_)) => true,
7148 Some(ManifestFile::Log(_)) | None => false,
7149 };
7150 if !dirent.is_dir && other {
7151 snapshots.remove(fs, &dirent.path);
7152 }
7153 }
7154 snapshots.finish()?;
7155 crate::file::fsync_directory(path, &**fs, sync_mode)?;
7156 let mut rest = OpenSweep::default();
7157 Self::cleanup_orphaned_version(path, keep, fs, &mut rest)?;
7158 rest.finish()?;
7159 crate::file::fsync_directory(path, &**fs, sync_mode)?;
7160 Ok(())
7161 }
7162}
7163
7164/// Who syncs the folders that gain an entry when an open makes a tree's
7165/// folder.
7166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7167pub(crate) enum NewFolderNames {
7168 /// The open, before the tree's first snapshot.
7169 Synced,
7170 /// The open's caller, once for a batch of trees after every one of them
7171 /// is made.
7172 Deferred,
7173}
7174
7175/// A tree an open made, and what takes it back: its folder on its backend,
7176/// the folders that gained an entry making that folder, what the folder held
7177/// before the open when it existed, and who syncs those folders.
7178pub(crate) struct Creation {
7179 path: crate::path::PathBuf,
7180 fs: Arc<dyn Fs>,
7181 /// Nearest first; none when the tree folder existed already.
7182 holders: Vec<crate::path::PathBuf>,
7183 /// The names in the tree folder before the open; `None` when the open
7184 /// made the folder.
7185 found: Option<Vec<alloc::string::String>>,
7186 names: NewFolderNames,
7187 sync_mode: crate::fs::SyncMode,
7188}
7189
7190impl Creation {
7191 /// Takes back `tree`, which this open made: what it put in its folder goes
7192 /// while its lock is still held, then the folders the open made, once
7193 /// empty, and the folders that held their names are synced where the open
7194 /// synced them, so the tree does not come back after a power loss.
7195 pub(crate) fn take_back(self, tree: Tree) {
7196 #[cfg(feature = "std")]
7197 let lock = tree.0.directory_lock.clone();
7198 drop(tree);
7199 self.empty();
7200 #[cfg(feature = "std")]
7201 drop(lock);
7202 undo_creation(None, self.made_folders(), &[]);
7203 if self.names == NewFolderNames::Synced {
7204 sync_made_names(&self.holders_on_backend());
7205 }
7206 }
7207
7208 /// Removes what the tree put in its folder.
7209 fn empty(&self) {
7210 empty_tree_folder(&self.path, &self.fs, self.found.as_deref());
7211 }
7212
7213 /// The folders the open made, on the tree's backend.
7214 fn made_folders(&self) -> Vec<(crate::path::PathBuf, Arc<dyn Fs>)> {
7215 made_folders(&self.path, &self.holders)
7216 .into_iter()
7217 .map(|folder| (folder, Arc::clone(&self.fs)))
7218 .collect()
7219 }
7220
7221 /// The folders that gained an entry for the tree folder, on its backend.
7222 fn holders_on_backend(&self) -> Vec<(crate::path::PathBuf, Arc<dyn Fs>)> {
7223 self.holders
7224 .iter()
7225 .map(|holder| (holder.clone(), Arc::clone(&self.fs)))
7226 .collect()
7227 }
7228}
7229
7230/// The names in the folder `path`.
7231fn folder_entries(fs: &dyn Fs, path: &Path) -> crate::Result<Vec<alloc::string::String>> {
7232 Ok(fs
7233 .read_dir(path)?
7234 .into_iter()
7235 .map(|entry| entry.file_name)
7236 .collect())
7237}
7238
7239/// A tree a batch made, and while the batch runs, the tree's directory lock.
7240struct BatchMade {
7241 creation: Creation,
7242 #[cfg(feature = "std")]
7243 lock: Option<Arc<dyn crate::fs::FsFile>>,
7244}
7245
7246/// Opens a tree for each config, making the missing ones, and syncs each
7247/// folder that gained a new tree's name once, after all of them: see
7248/// [`Config::open_many`].
7249pub(crate) fn open_batch(
7250 configs: impl IntoIterator<Item = Config>,
7251) -> crate::Result<Vec<crate::AnyTree>> {
7252 let mut trees = Vec::new();
7253 let mut made: Vec<BatchMade> = Vec::new();
7254 for config in configs {
7255 let fs = Arc::clone(&config.fs);
7256 let path = config.path.clone();
7257 let sync_mode = config.sync_mode;
7258 let holders = match new_entry_holders(&*fs, &path) {
7259 Ok(holders) => holders,
7260 Err(e) => {
7261 undo_batch(trees, made, None);
7262 return Err(e);
7263 }
7264 };
7265 match config.open_with(
7266 #[cfg(feature = "std")]
7267 crate::config::DirectoryLock::Acquire,
7268 NewFolderNames::Deferred,
7269 ) {
7270 Ok((tree, creation)) => {
7271 // A tree made in a folder that existed already is the batch's
7272 // as much as one whose folder it made: it is taken back too,
7273 // though the folder stays.
7274 if let Some(creation) = creation {
7275 made.push(BatchMade {
7276 #[cfg(feature = "std")]
7277 lock: batch_tree(&tree).0.directory_lock.clone(),
7278 creation,
7279 });
7280 }
7281 trees.push(tree);
7282 }
7283 Err(e) => {
7284 // The failed open took back what it made; the folders that
7285 // gained an entry for it are synced with the batch's.
7286 let failed = Creation {
7287 path,
7288 fs,
7289 holders,
7290 found: None,
7291 names: NewFolderNames::Deferred,
7292 sync_mode,
7293 };
7294 undo_batch(trees, made, Some(&failed));
7295 return Err(e);
7296 }
7297 }
7298 }
7299 let holders = batch_holders(made.iter().map(|tree| &tree.creation));
7300 for (folder, fs, mode) in holders {
7301 if let Err(e) = crate::file::fsync_directory(&folder, &*fs, mode) {
7302 undo_batch(trees, made, None);
7303 return Err(e.into());
7304 }
7305 }
7306 Ok(trees)
7307}
7308
7309/// The tree behind `tree`, whose folder holds the manifest and the lock.
7310#[cfg(feature = "std")]
7311fn batch_tree(tree: &crate::AnyTree) -> &Tree {
7312 match tree {
7313 crate::AnyTree::Standard(tree) => tree,
7314 crate::AnyTree::Blob(tree) => &tree.index,
7315 }
7316}
7317
7318/// Whether `a` and `b` name one namespace of paths: one handle, or two
7319/// handles of one backend, as configs made one by one each hold their own
7320/// handle of the real file system.
7321fn same_backend(a: &Arc<dyn Fs>, b: &Arc<dyn Fs>) -> bool {
7322 Arc::ptr_eq(a, b) || a.backend_id().is_some_and(|id| b.backend_id() == Some(id))
7323}
7324
7325/// The folders that hold the names of the trees `made`, each once, on its
7326/// backend, with the sync that makes them durable: each tree's own mode where
7327/// they agree and a full sync where they do not, and a full one under barrier
7328/// syncs for a folder on another device than a tree it holds, whose own syncs
7329/// a barrier there does not order.
7330fn batch_holders<'a>(
7331 made: impl Iterator<Item = &'a Creation>,
7332) -> Vec<(crate::path::PathBuf, Arc<dyn Fs>, crate::fs::SyncMode)> {
7333 use crate::fs::SyncMode;
7334 let mut holders: Vec<(crate::path::PathBuf, Arc<dyn Fs>, SyncMode)> = Vec::new();
7335 for tree in made {
7336 let tree_volume = tree.fs.volume_id(&tree.path);
7337 for folder in &tree.holders {
7338 let volume = tree.fs.volume_id(folder);
7339 let mode = if tree.sync_mode == SyncMode::Barrier
7340 && (volume.is_none() || volume != tree_volume)
7341 {
7342 SyncMode::Full
7343 } else {
7344 tree.sync_mode
7345 };
7346 match holders
7347 .iter_mut()
7348 .find(|(at, on, _)| at == folder && same_backend(on, &tree.fs))
7349 {
7350 Some((_, _, held)) if *held != mode => *held = SyncMode::Full,
7351 Some(_) => {}
7352 None => holders.push((folder.clone(), Arc::clone(&tree.fs), mode)),
7353 }
7354 }
7355 }
7356 holders
7357}
7358
7359/// Takes back a failed batch: closes the trees it opened, removes what each
7360/// tree it made put in its folder while still holding that tree's lock, then
7361/// the folders it made once they are empty, and makes the removals durable by
7362/// syncing every folder that held a name the batch made, `failed`'s included,
7363/// so no tree of the batch comes back after a power loss.
7364fn undo_batch(trees: Vec<crate::AnyTree>, made: Vec<BatchMade>, failed: Option<&Creation>) {
7365 let holders: Vec<(crate::path::PathBuf, Arc<dyn Fs>)> =
7366 batch_holders(made.iter().map(|tree| &tree.creation).chain(failed))
7367 .into_iter()
7368 .map(|(folder, fs, _)| (folder, fs))
7369 .collect();
7370 // The trees close their files; each made one's lock stays held by `made`.
7371 drop(trees);
7372 // Another opener taking a lock now would have its tree taken back from
7373 // under it: what each tree holds goes while its lock is held.
7374 let mut folders = Vec::new();
7375 for tree in &made {
7376 tree.creation.empty();
7377 folders.extend(tree.creation.made_folders());
7378 }
7379 // Then the locks go, and only folders left empty with them, deepest
7380 // first: one the batch made for several of its trees goes after all of
7381 // them, and one a lock no longer keeps another opener out of may hold
7382 // that opener's tree by now.
7383 #[cfg(feature = "std")]
7384 for tree in made {
7385 drop(tree.lock);
7386 }
7387 undo_creation(None, folders, &[]);
7388 sync_made_names(&holders);
7389}
7390
7391/// Removes what a tree made in the tree folder `path`, the lock file last:
7392/// everything in a folder the open made (`found` is `None`), and in one that
7393/// existed, holding `found`, only what was not there before. A tables or
7394/// blob folder that was there was empty, as a creation requires, so what it
7395/// holds now is the tree's. The lock file goes while the lock is still held,
7396/// so no opener is left holding the lock of a file another one can make
7397/// anew. What cannot be removed stays, and keeps the folder. A folder that
7398/// existed is synced, so its emptying is durable and no snapshot of the tree
7399/// comes back after a power loss.
7400fn empty_tree_folder(path: &Path, fs: &Arc<dyn Fs>, found: Option<&[alloc::string::String]>) {
7401 let entries = match fs.read_dir(path) {
7402 Ok(entries) => entries,
7403 Err(e) => {
7404 log::warn!(
7405 "could not list {} to take back the tree made there ({e})",
7406 path.display()
7407 );
7408 return;
7409 }
7410 };
7411 let (lock, rest): (Vec<_>, Vec<_>) = entries
7412 .into_iter()
7413 .partition(|entry| entry.file_name == crate::config::DIRECTORY_LOCK_FILE);
7414 let remove = |at: &Path, is_dir: bool| {
7415 let removed = if is_dir {
7416 fs.remove_dir_all(at)
7417 } else {
7418 fs.remove_file(at)
7419 };
7420 match removed {
7421 Ok(()) => {}
7422 Err(gone) if gone.kind() == crate::io::ErrorKind::NotFound => {}
7423 Err(kept) => log::warn!(
7424 "could not remove {} taking back the tree made there ({kept})",
7425 at.display()
7426 ),
7427 }
7428 };
7429 for entry in rest.into_iter().chain(lock) {
7430 let was_there = found.is_some_and(|found| found.contains(&entry.file_name));
7431 if !was_there {
7432 remove(&entry.path, entry.is_dir);
7433 } else if entry.is_dir
7434 && (entry.file_name == crate::file::TABLES_FOLDER
7435 || entry.file_name == crate::file::BLOBS_FOLDER)
7436 {
7437 match fs.read_dir(&entry.path) {
7438 Ok(inner) => {
7439 for inner in inner {
7440 remove(&inner.path, inner.is_dir);
7441 }
7442 // Files a power loss brought back here would read as a
7443 // half-written tree to the next open.
7444 if let Err(e) =
7445 crate::file::fsync_directory(&entry.path, &**fs, crate::fs::SyncMode::Full)
7446 {
7447 log::warn!(
7448 "could not sync {} after taking back the tree made there ({e})",
7449 entry.path.display()
7450 );
7451 }
7452 }
7453 Err(e) => log::warn!(
7454 "could not list {} to take back the tree made there ({e})",
7455 entry.path.display()
7456 ),
7457 }
7458 }
7459 }
7460 if found.is_some()
7461 && let Err(e) = crate::file::fsync_directory(path, &**fs, crate::fs::SyncMode::Full)
7462 {
7463 log::warn!(
7464 "could not sync {} after taking back the tree made there ({e}); a power loss \
7465 may bring the tree back",
7466 path.display()
7467 );
7468 }
7469}
7470
7471/// Takes back what a failed creation made: the folders in `made`, so a retry
7472/// makes them anew and syncs the folders `holders` that gained their names;
7473/// or, where one cannot be removed, those syncs now, since a retry finds it
7474/// existing and syncs nothing for it. `tree` names the tree folder (on its
7475/// backend) when everything in it is the creation's own.
7476fn undo_creation(
7477 tree: Option<(&Path, &Arc<dyn Fs>)>,
7478 mut made: Vec<(crate::path::PathBuf, Arc<dyn Fs>)>,
7479 holders: &[(crate::path::PathBuf, Arc<dyn Fs>)],
7480) {
7481 // Deepest first, so a folder is empty when its turn comes. The tree folder
7482 // goes with what the creation put in it; any other folder is shared with
7483 // whatever else may have been put there since it was found missing, which
7484 // no lock kept out: it is removed only while empty.
7485 made.sort_by_key(|(folder, _)| core::cmp::Reverse(folder.components().count()));
7486 let mut left = false;
7487 for (folder, folder_fs) in &made {
7488 let own =
7489 tree.is_some_and(|(tree, tree_fs)| folder == tree && Arc::ptr_eq(folder_fs, tree_fs));
7490 let removed = if own {
7491 folder_fs.remove_dir_all(folder)
7492 } else {
7493 folder_fs.remove_dir(folder)
7494 };
7495 match removed {
7496 Ok(()) => {}
7497 Err(gone) if gone.kind() == crate::io::ErrorKind::NotFound => {}
7498 Err(kept) => {
7499 log::warn!(
7500 "Tree::open: could not remove {} after a failed creation ({kept}); \
7501 syncing the folders it made instead",
7502 folder.display()
7503 );
7504 left = true;
7505 }
7506 }
7507 }
7508 if left {
7509 sync_made_names(holders);
7510 }
7511}
7512
7513/// Makes durable the names a failed creation leaves behind, by syncing the
7514/// folders `holders` that gained them, each with a device flush: a retry
7515/// finds those folders existing and syncs nothing for them.
7516fn sync_made_names(holders: &[(crate::path::PathBuf, Arc<dyn Fs>)]) {
7517 use crate::file::fsync_directory;
7518 use crate::fs::SyncMode;
7519 for (folder, folder_fs) in holders {
7520 if let Err(sync) = fsync_directory(folder, &**folder_fs, SyncMode::Full) {
7521 log::warn!(
7522 "Tree::open: could not sync {} after a failed creation ({sync}); a power \
7523 loss may take back the folder it made",
7524 folder.display()
7525 );
7526 }
7527 }
7528}
7529
7530/// Every folder made on the way to `folder`, deepest first, given the folders
7531/// that gained an entry making it ([`new_entry_holders`], nearest first):
7532/// `folder` and each one above it up to, not including, the farthest of them.
7533/// Empty when nothing was made.
7534fn made_folders(folder: &Path, holders: &[crate::path::PathBuf]) -> Vec<crate::path::PathBuf> {
7535 let Some(farthest) = holders.last() else {
7536 return Vec::new();
7537 };
7538 let mut made = Vec::new();
7539 let mut at = folder;
7540 while at != farthest.as_path() {
7541 made.push(at.to_path_buf());
7542 match at.parent() {
7543 Some(parent) => at = parent,
7544 None => break,
7545 }
7546 }
7547 made
7548}
7549
7550/// The empty table files an open found that no version names, per tables
7551/// folder and its backend.
7552type PooledFiles = Vec<(
7553 crate::path::PathBuf,
7554 Arc<dyn crate::fs::Fs>,
7555 Vec<crate::TableId>,
7556)>;
7557
7558/// What an open finds no version names, removed only once the state it judged
7559/// that by is installed as a fresh snapshot.
7560///
7561/// What an open reads may be what a failed sync left: an edit in the log that
7562/// was never synced, or a snapshot name whose install or withdrawal was never
7563/// made durable. Syncing those files again proves nothing (a sync after a
7564/// failed one may report success for pages the kernel already dropped), so the
7565/// open writes the state it recovered as a new snapshot instead, and a removal
7566/// judged by that state runs after it is durable.
7567#[derive(Default)]
7568struct OpenSweep {
7569 pending: Vec<(Arc<dyn Fs>, crate::path::PathBuf)>,
7570}
7571
7572impl OpenSweep {
7573 /// Queues `path` on `fs` for removal.
7574 fn remove(&mut self, fs: &Arc<dyn Fs>, path: &Path) {
7575 self.pending.push((Arc::clone(fs), path.to_path_buf()));
7576 }
7577
7578 /// Removes every queued file, a file already gone included, and fails
7579 /// with the first removal that failed. Each removal is tried: one file
7580 /// that cannot go must not keep the others, the generation an open
7581 /// superseded among them, from going.
7582 fn finish(self) -> crate::Result<()> {
7583 let mut first = None;
7584 for (fs, path) in self.pending {
7585 match fs.remove_file(&path) {
7586 Ok(()) => {}
7587 Err(e) if e.kind() == crate::io::ErrorKind::NotFound => {}
7588 Err(e) => {
7589 first.get_or_insert(e);
7590 }
7591 }
7592 }
7593 first.map_or(Ok(()), |e| Err(e.into()))
7594 }
7595}
7596
7597/// The folders that gain a new entry when `folder` is made on `fs`: each one
7598/// above it up to the first that exists already, which holds the first new
7599/// one. A folder that exists makes nothing new.
7600fn new_entry_holders(fs: &dyn Fs, folder: &Path) -> crate::Result<Vec<crate::path::PathBuf>> {
7601 let mut holders = Vec::new();
7602 if fs.exists(folder)? {
7603 return Ok(holders);
7604 }
7605 let mut made = folder;
7606 while let Some(holder) = made.parent() {
7607 holders.push(holder.to_path_buf());
7608 if fs.exists(holder)? {
7609 break;
7610 }
7611 made = holder;
7612 }
7613 Ok(holders)
7614}
7615
7616/// Returns `true` if any tables folder of the tree (the primary one and every
7617/// [`level_routes`](crate::Config::level_routes) one) or its `blobs/` folder
7618/// holds a file, or the tree folder holds an edit log holding an edit.
7619///
7620/// Used by [`Tree::open`], for a directory with no installed snapshot, to
7621/// distinguish a fresh directory or a creation that never completed (empty
7622/// folders, a snapshot never renamed into place: nothing of value, safe to
7623/// `create_new` over) from a lost manifest, a half-written checkpoint or
7624/// another interrupted sealing (files already placed: must error rather than
7625/// silently overwrite, and leave the files to orphan cleanup).
7626///
7627/// A missing folder is treated as "no state": `create_new` is what creates
7628/// the directories in the first place, so callers may invoke `Tree::open`
7629/// against a path that does not exist yet.
7630fn has_existing_version_state(config: &Config) -> crate::Result<bool> {
7631 let blobs = (
7632 config.path.join(crate::file::BLOBS_FOLDER),
7633 Arc::clone(&config.fs),
7634 );
7635 // Files only: a folder there holds no table itself. A level route inside
7636 // the primary tables folder is one, empty after a creation cut short or
7637 // when made in advance, and is listed here as a tables folder of its own.
7638 for (folder, fs) in config.all_tables_folders().into_iter().chain([blobs]) {
7639 match fs.read_dir(&folder) {
7640 Ok(entries) if entries.iter().any(|entry| !entry.is_dir) => return Ok(true),
7641 Ok(_) => {}
7642 Err(e) if e.kind() == crate::io::ErrorKind::NotFound => {}
7643 Err(e) => return Err(e.into()),
7644 }
7645 }
7646 // A tree's history (a clear, the retention floor it raised) outlives its
7647 // tables, and a lost snapshot leaves its log behind: an edit in any log,
7648 // or the log of any generation past the first, which only a rotation
7649 // makes, is a tree that lived. A creation cut short leaves only the
7650 // first generation's empty log.
7651 let entries = match config.fs.read_dir(&config.path) {
7652 Ok(entries) => entries,
7653 Err(e) if e.kind() == crate::io::ErrorKind::NotFound => return Ok(false),
7654 Err(e) => return Err(e.into()),
7655 };
7656 // A name is judged by its shape, digits after the prefix, not by whether
7657 // the digits parse: a snapshot whose id is past the counter's range is
7658 // no installed one, yet no creation wrote it either, and it is refused
7659 // rather than created over.
7660 let digits = |rest: Option<&str>| {
7661 rest.is_some_and(|id| !id.is_empty() && id.bytes().all(|b| b.is_ascii_digit()))
7662 };
7663 for entry in &entries {
7664 let name = entry.file_name.as_str();
7665 if digits(name.strip_prefix('v')) {
7666 return Ok(true);
7667 }
7668 if digits(name.strip_prefix("edits-"))
7669 && (name != "edits-0" || config.fs.metadata(&entry.path)?.len > 0)
7670 {
7671 return Ok(true);
7672 }
7673 }
7674 Ok(false)
7675}
7676
7677/// Whether two sightings of one table id PROVABLY hold different bytes.
7678///
7679/// Used to judge a duplicate scanned after its id was already accepted: the
7680/// winner matched the manifest, so a copy whose bytes differ from it cannot
7681/// also be the committed generation and is safe to sweep. Identical bytes mean
7682/// an alias or an exact copy, which is kept.
7683///
7684/// Only a completed comparison is proof. A read that fails on the BYTES proves
7685/// nothing about which generation this is, so it answers `false` and the file
7686/// stays; an ENVIRONMENTAL fault propagates, since a retry can re-read it.
7687///
7688/// # Errors
7689///
7690/// Propagates an environmental read failure of either file.
7691fn differs_byte_for_byte(
7692 winner_fs: &dyn crate::fs::Fs,
7693 winner: &Path,
7694 candidate_fs: &dyn crate::fs::Fs,
7695 candidate: &Path,
7696) -> crate::Result<bool> {
7697 let digest = |fs: &dyn crate::fs::Fs, path: &Path| -> crate::Result<Option<u128>> {
7698 match crate::file::checksum_from_with_overrides(fs, path, 0, &[]) {
7699 Ok(d) => Ok(Some(d)),
7700 Err(e) if e.is_environmental() => Err(e),
7701 Err(_) => Ok(None),
7702 }
7703 };
7704 let (Some(a), Some(b)) = (digest(winner_fs, winner)?, digest(candidate_fs, candidate)?) else {
7705 return Ok(false);
7706 };
7707 Ok(a != b)
7708}
7709
7710/// Raises a query's lower bound to a table's tight-space restriction, if any.
7711///
7712/// Keys below `restriction` are the punched-out prefix served by the
7713/// replacement table, so a range estimate must not charge them to the
7714/// restricted view. Returns `lo` unchanged when the table is unrestricted or
7715/// the restriction is at or below `lo`.
7716fn effective_lower_bound<'a>(
7717 lo: core::ops::Bound<&'a [u8]>,
7718 restriction: Option<&'a [u8]>,
7719 cmp: &dyn crate::comparator::UserComparator,
7720) -> core::ops::Bound<&'a [u8]> {
7721 use core::cmp::Ordering;
7722 use core::ops::Bound;
7723 match (lo, restriction) {
7724 (Bound::Unbounded, Some(rb)) => Bound::Included(rb),
7725 (Bound::Included(k) | Bound::Excluded(k), Some(rb))
7726 if cmp.compare(rb, k) == Ordering::Greater =>
7727 {
7728 Bound::Included(rb)
7729 }
7730 _ => lo,
7731 }
7732}
7733
7734#[cfg(test)]
7735mod cardinality_tests;
7736
7737#[cfg(test)]
7738#[expect(clippy::expect_used, reason = "test code")]
7739mod scan_since_freeze_tests;
7740
7741#[cfg(test)]
7742#[expect(clippy::expect_used, clippy::panic, reason = "test code")]
7743mod scan_since_scope_tests;
7744
7745#[cfg(all(test, feature = "metrics"))]
7746mod cache_stats_tests;
7747
7748#[cfg(all(test, feature = "std"))]
7749#[expect(clippy::expect_used, reason = "test code")]
7750mod restricted_reclaim_tests;
7751
7752#[cfg(all(test, feature = "std", zstd_any))]
7753mod dict_collect_tests;
7754
7755#[cfg(all(test, feature = "std", zstd_any))]
7756#[expect(clippy::expect_used, reason = "test code")]
7757mod live_compression_tests;
7758
7759#[cfg(test)]
7760#[expect(clippy::expect_used, reason = "test code")]
7761mod partition_size_tests;
7762
7763#[cfg(all(test, feature = "std"))]
7764mod chunk_order_tests;
7765
7766#[cfg(all(test, feature = "std"))]
7767mod chunk_file_tests;
7768
7769#[cfg(all(test, feature = "std"))]
7770#[expect(clippy::expect_used, reason = "test code")]
7771mod merge_read_tests;
7772
7773#[cfg(all(test, feature = "std"))]
7774mod probe_stats_tests;
7775
7776#[cfg(all(test, feature = "std"))]
7777mod filter_advisor_tests;
7778
7779#[cfg(all(test, feature = "std"))]
7780mod open_batch_tests;
7781
7782#[cfg(all(test, feature = "std"))]
7783#[expect(clippy::expect_used, reason = "test code")]
7784mod clear_race_tests;
7785
7786#[cfg(all(test, feature = "std"))]
7787#[expect(clippy::expect_used, reason = "test code")]
7788mod insert_rotation_tests;
7789
7790#[cfg(all(test, feature = "std"))]
7791#[expect(clippy::expect_used, reason = "test code")]
7792mod overlapping_runs_tests;