Skip to main content

lsm_tree/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5// no-std foundation: when the `std` feature is OFF the crate root opts into
6// `no_std`. Default builds keep `std` enabled (file I/O, threading, system
7// clock all live in `std`), so existing consumers see no behaviour change.
8// The migration to a fully no-std-clean build is incremental. Two patterns
9// coexist while the port is in progress:
10//   1. Modules with std-only dependencies that have no consumers above the
11//      `fs` / `version` / `tree` layer stay gated behind `#[cfg(feature =
12//      "std")]` and are ported in isolation.
13//   2. Modules whose std-only dependencies cascade through every consumer
14//      (`manifest_blocks`, vendored `sfa`, others noted at their `pub mod`
15//      site) remain UNCONDITIONAL today — gating them would require
16//      `#[cfg]` annotations on dozens of call sites without changing the
17//      no-std error count, because those call sites are themselves
18//      std-bound for unrelated reasons. They migrate in lockstep with the
19//      consumer layer rather than ahead of it.
20// The CI job `no-std-check` exercises `cargo check --no-default-features
21// --features alloc` against a real no-std target (`thumbv7em-none-eabihf`)
22// and tracks remaining work via the error count, which must monotonically
23// decrease across PRs.
24#![cfg_attr(not(feature = "std"), no_std)]
25
26//! Embedded LSM-tree storage engine.
27//!
28//! Provides keyed point reads, prefix and range scans, MVCC snapshots, block
29//! and file-descriptor caching, and a configurable compaction subsystem. No
30//! write-ahead log — durability is the caller's responsibility (`flush_active_memtable`
31//! forces persistence when needed).
32//!
33//! ## Highlights
34//!
35//! - **AMQ filter**: `BuRR` (Bumped Ribbon Retrieval, Walzer & Dillinger 2022) for
36//!   per-key and per-prefix membership checks. ~30% smaller filter blocks than a
37//!   same-FPR Bloom filter, or ~10× tighter FPR at the same memory budget.
38//! - **Compression**: pure-Rust zstd (incl. dictionary mode), LZ4, or none —
39//!   per-table and per-level policy.
40//! - **Encryption at rest**: AES-256-GCM block encryption with a caller-supplied
41//!   key.
42//! - **Range tombstones**: `delete_range` / `delete_prefix` (SST-encoded; the
43//!   feature was added in disk format V4 and remains supported in the current
44//!   V5 format — the V5 break extends the block header for per-block Reed-
45//!   Solomon Page ECC, not the tombstone encoding).
46//! - **Merge operators**: commutative-merge LSM operations with lazy resolution.
47//! - **K/V separation (`BlobTree`)**: large-value workloads with automatic GC.
48//! - **Pluggable `Fs`**: standard, in-memory, `io_uring`, or custom backends.
49//! - **MVCC**: snapshot reads at a chosen `SeqNo`, custom `UserComparator`.
50//! - **Concurrency**: thread-safe `BTreeMap`-like API.
51//!
52//! Keys: up to 65,535 bytes (`u16` length field). Values: up to 4,294,967,295
53//! bytes (`u32` length field, `2³² − 1`). Larger keys and values
54//! carry a proportional performance cost.
55//!
56//! ## Quick start
57//!
58//! ```no_run
59//! use lsm_tree::{AbstractTree, Config, SequenceNumberCounter};
60//!
61//! let folder = tempfile::tempdir().unwrap();
62//! let seqno = SequenceNumberCounter::default();
63//! let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default())
64//!     .open()
65//!     .unwrap();
66//!
67//! tree.insert("key", "value", seqno.next());
68//! let value = tree.get("key", lsm_tree::SeqNo::MAX).unwrap();
69//! assert_eq!(value.map(|v| v.to_vec()), Some(b"value".to_vec()));
70//! ```
71//!
72//! ## On-disk format
73//!
74//! Current version: **V6**, the only one this engine reads or writes. Relative
75//! to V5, which the 5.x releases write, the version snapshot names each
76//! table's level and run in its own record, and the `BuRR` filter and
77//! retrieval-locator sections use the packed solution layout; see
78//! [`FormatVersion`] for the full list.
79//!
80//! The self-describing block types (`Meta` / `Manifest` / `ManifestFooter`)
81//! carry a `block_flags` byte whose `ECC_PARITY` bit marks a per-block
82//! Reed-Solomon parity trailer and whose `KV_CHECKSUM_FOOTER` bit marks a
83//! per-entry checksum footer. SST block types (`Data` / `Index` / `Filter` /
84//! `RangeTombstone`) keep the compact header WITHOUT that byte and derive
85//! parity / footer presence from the per-SST meta descriptors
86//! (`descriptor#page_ecc`, `descriptor#kv_checksum`). Any "byte-identical when
87//! off" guarantee in the feature docs is within the current format and
88//! payload-level (e.g. index entries when `seqno_in_index = false`), not a
89//! cross-version equivalence.
90//!
91//! Earlier formats, V5 included, are not readable by this version and vice
92//! versa. A V5 manifest is refused by the manifest version gate with
93//! [`Error::InvalidVersion`] at `Tree::open` and at `Config::repair`, before
94//! any section is parsed. Older manifests usually fail earlier, at the
95//! manifest footer (see [`FormatVersion`]); the engine knows no earlier
96//! layout, so it cannot tell such a manifest from a damaged one, and
97//! `Config::repair` treats it as unreadable. A V5 store is converted by the
98//! separate offline converter; the engine carries no V5 decoder.
99#![deny(clippy::all, missing_docs, clippy::cargo)]
100#![deny(clippy::unwrap_used)]
101#![deny(clippy::indexing_slicing)]
102#![warn(clippy::pedantic, clippy::nursery)]
103#![warn(clippy::expect_used)]
104#![allow(clippy::missing_const_for_fn)]
105#![warn(clippy::multiple_crate_versions)]
106#![allow(clippy::option_if_let_else)]
107#![warn(clippy::redundant_feature_names)]
108// The `#[test_log::test]` attribute macro expands to a body that places
109// `use` items after statements, which trips `items_after_statements` in
110// the test build (`clippy --all-targets`). The lint fires only on the
111// macro's generated code, not on anything hand-written, so allow it
112// crate-wide rather than annotating every instrumented test.
113#![allow(clippy::items_after_statements)]
114// Test fixtures routinely bind closely-named locals (`dict_a` / `dict_b`,
115// `tree_1` / `tree_2`) where the similarity is the point; `similar_names`
116// is pedantic noise on that style.
117#![allow(clippy::similar_names)]
118// Long scenario tests (fuzz harnesses, multi-phase integration cases)
119// legitimately run past the `too_many_lines` ceiling; splitting them
120// would obscure the scenario. The lint is only reached under
121// `--all-targets`, which lints test bodies.
122#![allow(clippy::too_many_lines)]
123// An item no other crate can reach is written `pub(crate)`, so its visibility
124// holds even if the module around it is ever made public. That is rustc's
125// `unreachable_pub`, which `redundant_pub_crate` contradicts item for item.
126// CI holds the count of `unreachable_pub` warnings to a baseline that only
127// goes down (`.github/scripts/unreachable-pub-ratchet.sh`); at zero the lint
128// is denied here.
129#![expect(
130    clippy::redundant_pub_crate,
131    reason = "unreachable_pub writes crate-visible items as pub(crate)"
132)]
133#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
134
135// `alloc` is the minimal hard dependency — the crate uses `Arc`, `Vec`,
136// `Box`, and other heap types throughout. `extern crate alloc` makes the
137// `alloc` crate root visible to `no_std` builds; under `std` it is a
138// no-op alias because the standard library re-exports the same types.
139#[macro_use]
140extern crate alloc;
141
142// f64 ceil / round: the native `f64` methods are std-only (they bind to
143// platform float intrinsics), so the `no_std` build routes through `libm`.
144// std keeps the native path (equal or faster); the results are identical.
145#[cfg(feature = "std")]
146#[inline]
147pub(crate) fn f64_ceil(x: f64) -> f64 {
148    x.ceil()
149}
150#[cfg(not(feature = "std"))]
151#[inline]
152pub(crate) fn f64_ceil(x: f64) -> f64 {
153    libm::ceil(x)
154}
155#[cfg(feature = "std")]
156#[inline]
157pub(crate) fn f32_round(x: f32) -> f32 {
158    x.round()
159}
160#[cfg(not(feature = "std"))]
161#[inline]
162pub(crate) fn f32_round(x: f32) -> f32 {
163    libm::roundf(x)
164}
165#[cfg(feature = "std")]
166#[inline]
167pub(crate) fn f32_ceil(x: f32) -> f32 {
168    x.ceil()
169}
170#[cfg(not(feature = "std"))]
171#[inline]
172pub(crate) fn f32_ceil(x: f32) -> f32 {
173    libm::ceilf(x)
174}
175#[cfg(feature = "std")]
176#[inline]
177pub(crate) fn f64_log2(x: f64) -> f64 {
178    x.log2()
179}
180#[cfg(not(feature = "std"))]
181#[inline]
182pub(crate) fn f64_log2(x: f64) -> f64 {
183    libm::log2(x)
184}
185#[cfg(feature = "std")]
186#[inline]
187pub(crate) fn f32_log2(x: f32) -> f32 {
188    x.log2()
189}
190#[cfg(not(feature = "std"))]
191#[inline]
192pub(crate) fn f32_log2(x: f32) -> f32 {
193    libm::log2f(x)
194}
195
196// `hashbrown` (not `std::collections`) so the crate-wide map / set aliases
197// compile on `no_std + alloc`. hashbrown IS the implementation std's HashMap
198// wraps, so the API and performance match; using it directly just drops the
199// std dependency. Hasher stays `FxHasher` (fast, non-DoS — internal keys are
200// not attacker-controlled).
201#[doc(hidden)]
202pub type HashMap<K, V> = hashbrown::HashMap<K, V, rustc_hash::FxBuildHasher>;
203
204pub(crate) type HashSet<K> = hashbrown::HashSet<K, rustc_hash::FxBuildHasher>;
205
206macro_rules! fail_iter {
207    ($e:expr) => {
208        match $e {
209            Ok(v) => v,
210            Err(e) => return Some(Err(e.into())),
211        }
212    };
213}
214
215macro_rules! unwrap {
216    ($x:expr) => {{ $x.expect("should read") }};
217}
218
219mod any_tree;
220
221mod abstract_tree;
222
223mod close;
224
225mod skip_key;
226
227pub(crate) mod deletion_pause;
228pub mod heal_hints;
229
230/// Computed write-backpressure verdict (caller-honoured, see [`backpressure`]).
231pub mod backpressure;
232
233// `checkpoint` is `pub(crate)`: it contains internal helpers
234// (`link_or_copy_cross_fs`, `prepare_target`, `run_checkpoint`) used by
235// `Tree::create_checkpoint` and `BlobTree::create_checkpoint`. Exposing
236// it via `pub` (even with `#[doc(hidden)]`) would lock the helpers into
237// the stable surface; tests that need to exercise them live inline as
238// unit tests inside `src/checkpoint.rs`.
239#[cfg(feature = "std")]
240pub(crate) mod checkpoint;
241
242// std-only like checkpoints: the restore writes the received tree through the
243// filesystem, and the pin's registry waits on a condition variable.
244#[cfg(feature = "std")]
245mod pin;
246#[cfg(feature = "std")]
247pub use pin::{PinnedEntry, PinnedFile, PinnedRangeTombstone, PinnedVersion};
248
249#[doc(hidden)]
250pub mod blob_tree;
251
252// Vendored, `no_std`-ported `byteview` (backs `Slice`); kept in-tree so the
253// engine carries no external dependency that fails to compile on `no_std`.
254mod byteview;
255
256mod comparator;
257
258#[cfg(feature = "diagnostics")]
259#[doc(hidden)]
260pub mod diagnostics;
261
262#[doc(hidden)]
263mod cache;
264
265/// In-tree sharded S3-FIFO cache backing `cache` and `descriptor_table`
266/// (replaces `quick_cache`; works on `std` and `no_std + alloc`).
267mod sharded_cache;
268
269/// Coordination of concurrent cold loads of one cached object, so a miss
270/// shared by many readers reads and decodes it once.
271mod inflight;
272
273#[doc(hidden)]
274pub mod checksum;
275
276#[doc(hidden)]
277pub mod coding;
278
279pub mod compaction;
280#[doc(hidden)]
281pub mod compression;
282
283/// Block-level encryption at rest.
284pub mod encryption;
285
286/// Configuration
287pub mod config;
288
289#[doc(hidden)]
290pub mod descriptor_table;
291
292/// Shard-based Page ECC (XOR single-parity and Reed-Solomon).
293///
294/// Gated behind the `page_ecc` cargo feature so the
295/// `reed-solomon-simd` dependency is only pulled in when the feature
296/// is enabled.
297#[cfg(feature = "page_ecc")]
298pub mod ecc;
299
300#[doc(hidden)]
301pub mod file_accessor;
302
303#[cfg(zstd_any)]
304pub(crate) mod dicts;
305
306mod double_ended_peekable;
307mod error;
308
309/// Building a tree's files from parts another store holds, for the offline
310/// converter from the previous major format.
311#[doc(hidden)]
312#[cfg(feature = "std")]
313pub mod import;
314
315#[doc(hidden)]
316pub mod file;
317
318pub(crate) mod filter_budget;
319
320/// Pluggable filesystem abstraction for I/O backends.
321pub mod fs;
322
323pub mod hash;
324
325/// Local I/O trait surface mirroring `std::io::{Read, Write, Seek}`.
326///
327/// Provides `Error` / `ErrorKind` / `SeekFrom` plus the three trait
328/// definitions so the bounds on the [`fs`] traits no longer carry
329/// `std::io::*` directly. Under the `std` feature, supertrait
330/// aliases + blanket impls forward to `std::io` types so existing
331/// std-backed backends satisfy the trait surface automatically; the
332/// alias form also propagates BACK to `std::io`, so a `dyn FsFile`
333/// bounded on `crate::io::Read` still flows into `std::io::BufReader`,
334/// `byteorder`, and friends.
335///
336/// Scope: this prerequisite slice (per #311) lifts only
337/// `Read`/`Write`/`Seek` out of the [`fs`] trait bounds. The
338/// `io::Result<T>` return types and `&Path` argument types in
339/// `fs::Fs` / `fs::FsFile` still resolve to `std::io::Result<T>` and
340/// `std::path::Path` and migrate in follow-up commits; the full
341/// `--no-default-features --features alloc` build of the `fs::*`
342/// surface arrives once those two follow-ups land.
343pub mod io;
344
345mod heap;
346mod ingestion;
347mod iter_guard;
348mod key;
349mod key_range;
350mod loser_tree;
351mod manifest;
352#[doc(hidden)]
353pub mod manifest_blocks;
354mod memtable;
355mod merge_operator;
356pub mod rate_limiter;
357mod reseek;
358mod run_reader;
359mod run_scanner;
360pub mod write_budget;
361// Vendored sfa is std-only internally (`std::io` / `std::fs` /
362// `std::path`). Unconditional for the same cascading reason as
363// `manifest_blocks` above: ~20 consumers across the table /
364// blob-file / inspect / verify / checkpoint paths reference sfa
365// types in unconditional code. Gating sfa alone explodes the
366// `no-std-check` error count via unresolved-module failures on
367// every consumer that hasn't been gated yet. Migration is the
368// whole std-bound layer at once (tracked as issue #358), not sfa
369// in isolation.
370#[doc(hidden)]
371pub mod sfa;
372
373// Shared on-disk forgery helpers for corruption tests (std: reads/writes
374// real files through std::fs like the tests that consume it).
375#[cfg(all(test, feature = "std"))]
376pub(crate) mod test_forge;
377
378#[doc(hidden)]
379pub mod merge;
380
381#[doc(hidden)]
382pub mod merge_source;
383#[doc(hidden)]
384pub mod seeking_merger;
385
386#[cfg(feature = "metrics")]
387pub(crate) mod metrics;
388
389// mod multi_reader;
390
391#[doc(hidden)]
392pub mod mvcc_stream;
393
394mod key_domain;
395mod path;
396mod pinnable_slice;
397mod prefix;
398
399#[doc(hidden)]
400pub mod range;
401
402/// Runtime-toggleable configuration (`RuntimeConfig` + atomic-swap handle).
403pub mod runtime_config;
404
405/// Disaster-recovery: rebuild a missing/corrupt manifest from on-disk SSTs.
406// std-only: scans table folders and rewrites the manifest via std::fs.
407#[cfg(feature = "std")]
408pub mod repair;
409
410// Tight-space restriction sidecar: `{id}.restrict-bound` records the exact
411// lower bound of a hole-punched SST next to it, so manifest repair recovers the
412// restriction without mutating (and thus invalidating the manifest checksum of)
413// the SST itself.
414#[cfg(feature = "std")]
415pub mod restrict_bound;
416
417// std-only: block-granular SST salvage; reads source blocks and writes a
418// recovered SST via std::fs (see also `crate::repair`, `crate::verify`).
419#[cfg(feature = "std")]
420pub mod salvage;
421
422/// Live progress counters for long-running recovery (repair / salvage).
423// std-only: its only producers (repair, salvage) are std-gated.
424#[cfg(feature = "std")]
425pub mod recovery_progress;
426
427pub(crate) mod active_tombstone_set;
428pub(crate) mod range_tombstone;
429pub(crate) mod range_tombstone_filter;
430
431#[doc(hidden)]
432pub mod table;
433
434mod background_deleter;
435mod scan_since;
436
437/// Single-error-correct / double-error-detect word codecs for the Page ECC
438/// read path (pluggable SEC-DED shapes; default Hsiao `(72, 64)`).
439///
440/// Gated behind `page_ecc` and crate-internal: the codec only runs on the
441/// Page ECC recovery path. Trailer sizing on the read path uses a plain
442/// `ceil(len / 8)` so reading a SEC-DED SST does not require this module.
443#[cfg(feature = "page_ecc")]
444pub(crate) mod secded;
445
446mod seqno;
447mod slice;
448#[cfg(test)]
449mod slice_windows;
450
451#[doc(hidden)]
452pub mod stop_signal;
453
454mod format_version;
455mod time;
456mod tree;
457
458pub use tree::resumable;
459
460pub use time::Clock;
461#[cfg(feature = "std")]
462pub use time::SystemClock;
463#[cfg(not(feature = "std"))]
464pub use time::set_clock;
465
466/// Utility functions
467pub mod util;
468
469mod value;
470mod value_type;
471mod write_batch;
472
473/// Integrity verification for SST and blob files.
474///
475/// The block-level scrub (per-block + per-KV checksum walking) runs over the
476/// injected [`Fs`](crate::fs::Fs) backend through `crate::io`, so it compiles on
477/// `no_std` (serial path; the multi-threaded fan-out and the full-file
478/// hash-by-path convenience stay behind `std`).
479pub mod verify;
480
481/// Out-of-band inspection of a single SST file.
482///
483/// Public read-only view of stored metadata (table id, key range,
484/// counts, compression, timestamp) without spinning up a `Tree`.
485/// Used by `sst-dump properties` and similar diagnostic tools. See
486/// the module docs for the recovery semantics (mirrors
487/// `Table::recover`'s TAIL-first / MID-fallback path from #295).
488#[cfg(feature = "std")]
489pub mod inspect;
490
491/// ECC patrol scrub: a proactive sweep over Page-ECC-protected SST blocks.
492///
493/// Reads blocks to detect and correct latent bit-rot before it accumulates
494/// past the parity budget. Std-gated for the same reason as [`verify`]: the
495/// sweep needs real filesystem I/O and thread-based parallelism.
496#[cfg(feature = "std")]
497pub mod scrub;
498
499pub mod storage_stats;
500pub use storage_stats::{
501    ApproximateRangeStats, BlobReferenceStats, FilterMemory, LevelStats, RangeCardinality,
502    SegmentStats, StorageStatistics, StorageStats, StorageStatus,
503};
504
505mod version;
506mod vlog;
507
508// L0 run placement, exposed for the `optimize_runs` fuzz target.
509#[doc(hidden)]
510pub use version::optimize_key_ranges;
511
512// Reproducible single-byte-bitrot heal/read fuzzer. `#[ignore]`d, so it is
513// excluded from the normal suite and run as its own CI step; needs crate-internal
514// `Table` / `Writer` access, so it lives here rather than in `tests/`.
515#[cfg(all(test, feature = "std"))]
516mod fuzz_heal;
517
518/// User defined key (byte array)
519pub type UserKey = Slice;
520
521/// User defined data (byte array)
522pub type UserValue = Slice;
523
524/// KV-tuple (key + value)
525pub type KvPair = (UserKey, UserValue);
526
527// The `#[doc(hidden)]` block below re-exports crate internals that are reachable
528// at the crate root for benchmarks and integration tests, but are NOT part of the
529// public API contract — they carry no semver guarantee and may be renamed, moved,
530// or removed without a major version bump. External callers that import these
531// hidden items do so at their own risk. `cargo doc` excludes them from generated
532// rustdoc output; only intra-crate test/bench code is expected to use them.
533#[doc(hidden)]
534pub use {
535    checksum::Checksum,
536    iter_guard::{IterGuardImpl, SeekableGuardIter},
537    key_range::KeyRange,
538    merge::BoxedIterator,
539    slice::Builder,
540    // Re-exported for `benches/lsp.rs` only — see hidden-block contract above.
541    table::util::longest_shared_prefix_length,
542    table::{GlobalTableId, Table, TableId},
543    value::InternalValue,
544};
545
546#[doc(hidden)]
547pub use {
548    blob_tree::{Guard as BlobGuard, handle::BlobIndirection},
549    tree::Guard as StandardGuard,
550    tree::inner::TreeId,
551};
552
553pub use encryption::EncryptionProvider;
554
555#[cfg(feature = "encryption")]
556pub use encryption::Aes256GcmProvider;
557
558#[doc(hidden)]
559#[cfg(feature = "std")]
560pub use background_deleter::BackgroundDeleter;
561pub use pinnable_slice::PinnableSlice;
562#[cfg(feature = "std")]
563pub use recovery_progress::{RecoveryPhase, RecoveryProgress, RecoveryProgressSnapshot};
564#[cfg(feature = "std")]
565pub use repair::{RepairPolicy, RepairReport, WalReplayScope};
566pub use write_batch::WriteBatch;
567
568pub use {
569    cache::Cache,
570    comparator::{DefaultUserComparator, SharedComparator, UserComparator},
571    compression::CompressionType,
572    config::{BlobLocalityRelocation, Config, KvSeparationOptions, TreeType},
573    error::{Error, FormatPart, Result},
574    format_version::FormatVersion,
575    iter_guard::IterGuard as Guard,
576    key_domain::KeyDomains,
577    memtable::{Memtable, MemtableId},
578    merge_operator::MergeOperator,
579    prefix::PrefixExtractor,
580    seqno::{
581        MAX_SEQNO, SequenceNumberCounter, SequenceNumberGenerator, SharedSequenceNumberGenerator,
582    },
583    slice::Slice,
584    value::SeqNo,
585    value_type::ValueType,
586};
587
588pub use {
589    abstract_tree::{AbstractTree, CheckpointInfo},
590    any_tree::AnyTree,
591    blob_tree::BlobTree,
592    close::{TreeClose, WeakTree},
593    descriptor_table::DescriptorTable,
594    ingestion::AnyIngestion,
595    scan_since::ScanSinceEvent,
596    tree::Tree,
597    vlog::BlobFile,
598};
599
600#[cfg(feature = "columnar")]
601pub use tree::columnar_scan::{
602    ColumnarScan,
603    projection::{Absent, ProjectedField, ProjectedRow, Projection, ValueProjector},
604};
605
606#[cfg(zstd_any)]
607pub use compression::{ZstdDictionaries, ZstdDictionary};
608
609#[cfg(feature = "metrics")]
610pub use metrics::{AcceptedWrites, CacheStats, Metrics};
611
612pub use backpressure::{Backpressure, BackpressureSignals, BackpressureThresholds};
613#[cfg(feature = "std")]
614pub use backpressure::{Unflushed, WriteBufferBudget};
615
616pub use compaction::capacity::CapacityPool;
617
618#[cfg(feature = "std")]
619#[doc(hidden)]
620#[must_use]
621#[allow(missing_docs, clippy::missing_errors_doc, clippy::unwrap_used)]
622pub fn get_tmp_folder() -> tempfile::TempDir {
623    if let Ok(p) = std::env::var("LSMT_TMP_FOLDER") {
624        tempfile::tempdir_in(p)
625    } else {
626        tempfile::tempdir()
627    }
628    .unwrap()
629}