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