lsm_tree/config/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5pub(crate) mod blob_lifetime;
6mod block_size;
7mod column_encoding;
8mod compression;
9mod delete_strategy;
10mod filter;
11mod flush_durability;
12mod hash_ratio;
13mod locator;
14mod pinning;
15mod restart_interval;
16
17pub use blob_lifetime::{LifetimeGroups, LifetimeHint, LifetimeHintFn};
18pub use block_size::{BlockSizePolicy, MAX_BLOCK_SIZE};
19pub use column_encoding::{ColumnEncoding, ColumnEncodingPolicy};
20pub use compression::CompressionPolicy;
21pub use delete_strategy::{DeleteStrategy, DeleteStrategyPolicy};
22pub use filter::{BloomConstructionPolicy, FilterAdvisor, FilterPolicy, FilterPolicyEntry};
23pub use flush_durability::FlushDurability;
24pub use hash_ratio::HashRatioPolicy;
25pub use locator::{LocatorPolicy, LocatorPolicyEntry, LocatorPrecision};
26pub use pinning::PinningPolicy;
27pub use restart_interval::RestartIntervalPolicy;
28
29/// Partitioning policy for indexes and filters
30pub type PartitioningPolicy = PinningPolicy;
31
32#[cfg(feature = "std")]
33use crate::fs::StdFs;
34use crate::path::PathBuf;
35use crate::{
36 AnyTree, BlobTree, Cache, CompressionType, DescriptorTable, SharedSequenceNumberGenerator,
37 Tree,
38 compaction::filter::Factory,
39 comparator::SharedComparator,
40 encryption::EncryptionProvider,
41 file::TABLES_FOLDER,
42 fs::{Fs, SyncMode},
43 merge_operator::MergeOperator,
44 path::absolute_path,
45 prefix::PrefixExtractor,
46};
47// std-only: used solely by the std-gated `Config::default` / `Config::new`
48// constructors (the no_std path builds `Config` field-by-field).
49#[cfg(feature = "std")]
50use crate::{SequenceNumberCounter, comparator, path::Path, version::DEFAULT_LEVEL_COUNT};
51use alloc::sync::Arc;
52#[cfg(not(feature = "std"))]
53use alloc::vec::Vec;
54use core::ops::Range;
55
56/// The size a columnar row group is cut at unless
57/// [`Config::columnar_row_group_size_policy`] says otherwise.
58///
59/// Four row pages of [`DEFAULT_COLUMNAR_PAGE_SIZE`]: the smallest group that
60/// reads fewer bytes than one-page groups on a near-full scan and a point
61/// read without reading more on a sparse scan. The measured grid it was chosen
62/// from is in `docs/columnar-page-format.md`.
63pub const DEFAULT_COLUMNAR_ROW_GROUP_SIZE: u32 = 16 * 1_024;
64
65/// The size a columnar row group's rows are cut into row pages at unless
66/// [`Config::columnar_page_size_policy`] says otherwise.
67pub const DEFAULT_COLUMNAR_PAGE_SIZE: u32 = 4_096;
68
69/// How a columnar read fetches the pages it wants: how much one request may
70/// ask for, and how many requests it keeps in flight at once.
71///
72/// A property of the reader, not of the file: the same table reads under any
73/// budget and returns the same rows, and a larger budget takes the pages in
74/// fewer requests, because a column's pages lie next to each other and a run
75/// of them is one request up to the I/O buffer. Set per tree with
76/// [`Config::columnar_read_budget`].
77///
78/// # Examples
79///
80/// ```
81/// use lsm_tree::config::ReadBudget;
82///
83/// // 256 KiB per request, eight requests in flight at once.
84/// let budget = ReadBudget::new(256 * 1_024, 8);
85/// assert_eq!(budget.io_buffer(), 256 * 1_024);
86/// assert_eq!(budget.in_flight(), 8);
87/// ```
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub struct ReadBudget {
90 io_buffer: u32,
91 in_flight: u16,
92}
93
94impl ReadBudget {
95 /// The least a request may ask for: a block header's worth, below which
96 /// no request holds even the header of the block it reads.
97 pub const MIN_IO_BUFFER: u32 = 64;
98
99 /// A budget of `io_buffer` bytes per request and `in_flight` requests at
100 /// once. A budget below [`Self::MIN_IO_BUFFER`] is raised to it, and a
101 /// zero `in_flight` to one: a read needs at least one request of at least
102 /// a header to make progress.
103 ///
104 /// A request never splits a block: a directory, zone block or page larger
105 /// than the I/O buffer is still one request, and blocks are asked for
106 /// together only while they fit it.
107 #[must_use]
108 pub const fn new(io_buffer: u32, in_flight: u16) -> Self {
109 Self {
110 io_buffer: if io_buffer < Self::MIN_IO_BUFFER {
111 Self::MIN_IO_BUFFER
112 } else {
113 io_buffer
114 },
115 in_flight: if in_flight == 0 { 1 } else { in_flight },
116 }
117 }
118
119 /// Bytes one request may ask for.
120 #[must_use]
121 pub const fn io_buffer(&self) -> u32 {
122 self.io_buffer
123 }
124
125 /// Requests submitted together, which a backend with batched I/O keeps
126 /// in flight at once.
127 #[must_use]
128 pub const fn in_flight(&self) -> u16 {
129 self.in_flight
130 }
131}
132
133impl Default for ReadBudget {
134 /// 1 MiB per request, the I/O buffer Vortex reads a column with, and 16
135 /// requests in flight.
136 fn default() -> Self {
137 Self::new(1 << 20, 16)
138 }
139}
140
141/// The page bytes a projected columnar scan holds at once unless
142/// [`Config::columnar_scan_budget`] says otherwise: what one read request of
143/// the default [`ReadBudget`] fetches.
144pub const DEFAULT_COLUMNAR_SCAN_BUDGET: u64 = 1 << 20;
145
146/// The default of [`Config::multi_get_metadata_budget`].
147///
148/// Room for the filters of a few large tables in flight together, which is
149/// all the overlap the reads of a level gain from.
150pub const DEFAULT_MULTI_GET_METADATA_BUDGET: u64 = 8 << 20;
151
152/// The default of [`Config::table_target_size`]: 64 MiB, the size the leveled
153/// and tiered strategies cut compaction output at.
154pub const DEFAULT_TABLE_TARGET_SIZE: u64 = 64 * 1_024 * 1_024;
155
156/// Per-level filesystem routing entry for tiered storage.
157///
158/// Maps a range of LSM levels to a base directory and filesystem backend.
159/// Tables at these levels are stored under `path/tables/`.
160///
161/// # Example
162///
163/// ```
164/// use lsm_tree::config::LevelRoute;
165/// use lsm_tree::fs::StdFs;
166/// use std::sync::Arc;
167///
168/// // Hot tier: L0-L1 on NVMe
169/// let hot = LevelRoute {
170/// levels: 0..2,
171/// path: "/mnt/nvme/db".into(),
172/// fs: Arc::new(StdFs),
173/// };
174///
175/// // Cold tier: L4-L6 on HDD
176/// let cold = LevelRoute {
177/// levels: 4..7,
178/// path: "/mnt/hdd/db".into(),
179/// fs: Arc::new(StdFs),
180/// };
181/// ```
182#[derive(Clone)]
183pub struct LevelRoute {
184 /// LSM levels this route covers (e.g., `0..2` for L0–L1).
185 pub levels: Range<u8>,
186
187 /// Base data directory for tables at these levels.
188 pub path: PathBuf,
189
190 /// Filesystem backend for I/O at these levels.
191 pub fs: Arc<dyn Fs>,
192}
193
194impl core::fmt::Debug for LevelRoute {
195 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196 f.debug_struct("LevelRoute")
197 .field("levels", &self.levels)
198 .field("path", &self.path)
199 .finish_non_exhaustive()
200 }
201}
202
203/// Policy governing what `Tree::open` does when the manifest's edit log ends
204/// inside a record.
205///
206/// The manifest is a snapshot plus an append-only edit log. The snapshot's
207/// sections are checksummed blocks its checksummed footer indexes, so they
208/// are read whole or not at all and no mode relaxes them. The only damage an
209/// append can leave that is not already covered is a torn last record: power
210/// was lost mid-append, so the operation it records was never acknowledged.
211/// This is the one thing a mode chooses.
212///
213/// Corruption of anything already committed fails the open in every mode.
214/// Rolling a committed edit back would roll back every committed edit after
215/// it, and the open then deletes the tables those edits added as orphans,
216/// losing acknowledged data for good. `RocksDB` and Pebble refuse the same
217/// way; the remedy is [`Config::repair`], which rebuilds the manifest from
218/// the table files themselves.
219#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
220pub enum ManifestRecoveryMode {
221 /// Any defect fails the open, a torn last edit included, so an operator
222 /// decides how to resolve it ([`Config::repair`] recovers every table on
223 /// disk). The default.
224 #[default]
225 AbsoluteConsistency,
226
227 /// A torn last edit is rolled back and the tree opens at the durable
228 /// prefix, with a `warn!`. The torn edit was never acknowledged, so
229 /// nothing a caller was told is durable is lost. Any other defect still
230 /// fails the open.
231 TolerateCorruptedTailRecords,
232}
233
234/// LSM-tree type
235#[derive(Copy, Clone, Debug, PartialEq, Eq)]
236pub enum TreeType {
237 /// Standard LSM-tree, see [`Tree`]
238 Standard,
239
240 /// Key-value separated LSM-tree, see [`BlobTree`]
241 Blob,
242}
243
244impl From<TreeType> for u8 {
245 fn from(val: TreeType) -> Self {
246 match val {
247 TreeType::Standard => 0,
248 TreeType::Blob => 1,
249 }
250 }
251}
252
253impl TryFrom<u8> for TreeType {
254 type Error = ();
255
256 fn try_from(value: u8) -> Result<Self, Self::Error> {
257 match value {
258 0 => Ok(Self::Standard),
259 1 => Ok(Self::Blob),
260 _ => Err(()),
261 }
262 }
263}
264
265#[cfg_attr(
266 not(feature = "std"),
267 allow(
268 dead_code,
269 reason = "default data-folder path used only on the std-gated default-config path"
270 )
271)]
272const DEFAULT_FILE_FOLDER: &str = ".lsm.data";
273
274/// Options for key-value separation
275///
276/// Blob compression is not among them: it changes on a live tree, so it lives in
277/// [`RuntimeConfig::blob_compression`](crate::runtime_config::RuntimeConfig::blob_compression),
278/// seeded by [`Config::blob_compression`].
279#[derive(Clone, Debug, PartialEq)]
280pub struct KvSeparationOptions {
281 /// Blob file target size in bytes
282 #[doc(hidden)]
283 pub file_target_size: u64,
284
285 /// Key-value separation threshold in bytes
286 #[doc(hidden)]
287 pub separation_threshold: u32,
288
289 /// Separation thresholds of the fields of rows written as cells, as
290 /// `(column, bytes)` pairs; a column without one uses
291 /// `separation_threshold`. See [`Self::cell_separation_threshold`].
292 #[doc(hidden)]
293 pub cell_separation_thresholds: alloc::vec::Vec<(u16, u32)>,
294
295 #[doc(hidden)]
296 pub staleness_threshold: f32,
297
298 #[doc(hidden)]
299 pub age_cutoff: f32,
300
301 /// How many upcoming values a scan reads ahead in one coalesced batch.
302 ///
303 /// `0` disables read-ahead. See
304 /// [`scan_prefetch`](KvSeparationOptions::scan_prefetch).
305 #[doc(hidden)]
306 pub scan_prefetch: u16,
307
308 /// Relocation of blob files for scan locality, off when `None`. See
309 /// [`Self::relocate_for_locality`].
310 #[doc(hidden)]
311 pub locality_relocation: Option<BlobLocalityRelocation>,
312
313 /// How many lifetime groups values are split into. See
314 /// [`Self::lifetime_groups`].
315 #[doc(hidden)]
316 pub lifetime_groups: LifetimeGroups,
317
318 /// The caller's lifetime class per key, over the observed one. See
319 /// [`Self::lifetime_hint`].
320 #[doc(hidden)]
321 pub lifetime_hint: Option<LifetimeHint>,
322
323 /// A zstd dictionary to register with the tree at open, for the blob
324 /// compression to name. See [`Self::dict`].
325 #[cfg(zstd_any)]
326 #[doc(hidden)]
327 pub zstd_dictionary: Option<alloc::sync::Arc<crate::compression::ZstdDictionary>>,
328}
329
330/// Whether a value read from a blob file goes into the shared cache.
331///
332/// Lookups are the same either way: a value already cached is served from
333/// it. Set on a live tree through
334/// [`RuntimeConfig::blob_cache_fill`](crate::runtime_config::RuntimeConfig::blob_cache_fill),
335/// seeded by [`Config::blob_cache_fill`].
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
337pub enum BlobCacheFill {
338 /// Every value read from a blob file is cached, and a scan reads its
339 /// values ahead into the cache.
340 Always,
341 /// No value read from a blob file is cached: values large enough to be
342 /// separated are mostly read once, and cached they would evict the index,
343 /// filter and data blocks of every tree sharing the cache. A scan still
344 /// reads its values ahead, into a store of its own holding at most one
345 /// coalesced read, from which it hands them out.
346 #[default]
347 Never,
348}
349
350impl BlobCacheFill {
351 /// Whether a value read now goes into the cache.
352 #[must_use]
353 pub const fn fills(self) -> bool {
354 matches!(self, Self::Always)
355 }
356}
357
358/// When and how much a compaction relocates blob files to restore scan
359/// locality. Set through [`KvSeparationOptions::relocate_for_locality`].
360#[derive(Copy, Clone, Debug, PartialEq)]
361pub struct BlobLocalityRelocation {
362 /// The blob reference depth a compaction's inputs may reach before it
363 /// relocates for locality. Relocation fires only above it.
364 pub max_depth: core::num::NonZeroU64,
365
366 /// The extra I/O relocation may add, as a fraction of the bytes the
367 /// compaction writes anyway.
368 pub budget: f32,
369}
370
371/// The threshold `thresholds` gives `column`, else `default`.
372pub(crate) fn cell_threshold(thresholds: &[(u16, u32)], default: u32, column: u16) -> u32 {
373 thresholds
374 .iter()
375 .find(|(set, _)| *set == column)
376 .map_or(default, |(_, bytes)| *bytes)
377}
378
379impl Default for KvSeparationOptions {
380 fn default() -> Self {
381 Self {
382 file_target_size: /* 64 MiB */ 64 * 1_024 * 1_024,
383 separation_threshold: /* 1 KiB */ 1_024,
384 cell_separation_thresholds: alloc::vec::Vec::new(),
385
386 staleness_threshold: 0.25,
387 age_cutoff: 0.25,
388
389 scan_prefetch: 64,
390
391 locality_relocation: None,
392
393 lifetime_groups: LifetimeGroups::MAX,
394 lifetime_hint: None,
395
396 #[cfg(zstd_any)]
397 zstd_dictionary: None,
398 }
399 }
400}
401
402impl KvSeparationOptions {
403 /// Sets the target size of blob files.
404 ///
405 /// Smaller blob files allow more granular garbage collection
406 /// which allows lower space amp for lower write I/O cost.
407 ///
408 /// Larger blob files decrease the number of files on disk and maintenance
409 /// overhead.
410 ///
411 /// Defaults to 64 MiB.
412 #[must_use]
413 pub fn file_target_size(mut self, bytes: u64) -> Self {
414 self.file_target_size = bytes;
415 self
416 }
417
418 /// Sets the key-value separation threshold in bytes.
419 ///
420 /// Smaller value will reduce compaction overhead and thus write amplification,
421 /// at the cost of lower read performance.
422 ///
423 /// Defaults to 1 KiB.
424 #[must_use]
425 pub fn separation_threshold(mut self, bytes: u32) -> Self {
426 self.separation_threshold = bytes;
427 self
428 }
429
430 /// Sets the separation threshold of `column` in rows written as cells
431 /// (see [`BlobTree::insert_cells`](crate::BlobTree::insert_cells)): a
432 /// value field of that column goes to a blob file when it is at least
433 /// `bytes` long. Setting a column again replaces its threshold; a column
434 /// never set uses [`Self::separation_threshold`].
435 ///
436 /// A field that is always read with the row's compact attributes can stay
437 /// inline whatever its size (`u32::MAX`), and a field that is rarely read
438 /// can go to a blob file however small (`0`).
439 ///
440 /// # Examples
441 ///
442 /// ```
443 /// use lsm_tree::KvSeparationOptions;
444 ///
445 /// // Column 3 (status) never separates; column 4 (body) separates from 256 bytes.
446 /// let opts = KvSeparationOptions::default()
447 /// .cell_separation_threshold(3, u32::MAX)
448 /// .cell_separation_threshold(4, 256);
449 /// ```
450 #[must_use]
451 pub fn cell_separation_threshold(mut self, column: u16, bytes: u32) -> Self {
452 match self
453 .cell_separation_thresholds
454 .iter_mut()
455 .find(|(set, _)| *set == column)
456 {
457 Some(entry) => entry.1 = bytes,
458 None => self.cell_separation_thresholds.push((column, bytes)),
459 }
460 self
461 }
462
463 /// What picks the lifetime class of a value written now.
464 #[must_use]
465 pub(crate) fn lifetime_classifier(&self) -> blob_lifetime::LifetimeClassifier<'_> {
466 blob_lifetime::LifetimeClassifier {
467 groups: self.lifetime_groups,
468 hint: self.lifetime_hint.as_ref(),
469 }
470 }
471
472 /// The separation threshold of the fields of `column`.
473 #[must_use]
474 pub(crate) fn cell_threshold(&self, column: u16) -> u32 {
475 cell_threshold(
476 &self.cell_separation_thresholds,
477 self.separation_threshold,
478 column,
479 )
480 }
481
482 /// Sets how many upcoming values a scan reads ahead in one batch.
483 ///
484 /// A scan resolves separated values one at a time, and each is its own read
485 /// of a few hundred bytes. Values sit in the blob file in the order the
486 /// flush wrote them, which is key order, so a scan's next values are
487 /// usually its immediate on-disk neighbours: reading a window of them at
488 /// once collapses that stream of small reads into a few large ones.
489 ///
490 /// Read-ahead starts only once the scan's caller resolves a value
491 /// unconditionally, so a scan that reads keys alone never pays for it, and
492 /// neither does one that decides per key whether the value is worth
493 /// reading. Larger windows amortize better on long scans; smaller ones
494 /// waste less on a scan that stops early. `0` disables it.
495 ///
496 /// Applies to the sequential scans: `iter`, `range`, `prefix` and
497 /// `batch_range_scan`. NOT to the seekable iterator, where a seek would
498 /// throw away whatever was read ahead, and which exists for jumping around
499 /// rather than walking a run.
500 ///
501 /// Defaults to 64.
502 #[must_use]
503 pub fn scan_prefetch(mut self, items: u16) -> Self {
504 self.scan_prefetch = items;
505 self
506 }
507
508 /// Lets compactions relocate blob files to restore scan locality.
509 ///
510 /// Values keep the order they were flushed in, so keys written in
511 /// separate batches end up interleaved across many blob files, and a range
512 /// scan alternates between all of them. A compaction whose input tables
513 /// reference files interleaved deeper than `max_depth` (see
514 /// [`BlobReferenceStats::depth`](crate::BlobReferenceStats::depth)) rewrites
515 /// the deepest ones in key order, so each output file holds one run of
516 /// adjacent keys. A table a strategy would only move to another level is
517 /// merged instead when this applies to it, so the interleaving is not
518 /// carried down unchanged. Files written in consecutive key runs have a
519 /// depth of one and are never relocated for this, however many there are.
520 ///
521 /// Relocation copies values as they are stored, so files written under
522 /// different blob codecs are rewritten into one file per codec and do not
523 /// merge further; a file is relocated only together with another of its
524 /// codec that it overlaps.
525 ///
526 /// Relocation is bounded three ways:
527 /// - only whole files are rewritten, and only those no table outside the
528 /// compaction references;
529 /// - reading and writing them together may cost at most `budget` times the
530 /// bytes the compaction writes anyway (its tables, plus the stale blob
531 /// files it already relocates, which are always served first);
532 /// - nothing is relocated for locality when the output would not leave the
533 /// reserved free space intact.
534 ///
535 /// A `budget` of zero, negative or NaN relocates nothing. Off by default:
536 /// compactions then behave and write exactly as without this option.
537 ///
538 /// # Examples
539 ///
540 /// ```
541 /// use core::num::NonZeroU64;
542 /// use lsm_tree::KvSeparationOptions;
543 ///
544 /// let depth = NonZeroU64::new(4).expect("non-zero");
545 /// let opts = KvSeparationOptions::default().relocate_for_locality(depth, 0.5);
546 /// assert!(opts.locality_relocation.is_some());
547 /// ```
548 #[must_use]
549 pub fn relocate_for_locality(mut self, max_depth: core::num::NonZeroU64, budget: f32) -> Self {
550 self.locality_relocation = Some(BlobLocalityRelocation { max_depth, budget });
551 self
552 }
553
554 /// Splits blob values into `groups` by how long they are expected to
555 /// live, each group written to its own blob files.
556 ///
557 /// A blob file is reclaimed as a whole, so a value that lives long and
558 /// shares a file with values that are overwritten quickly is copied
559 /// forward every time that file is collected. Grouping keeps the two apart:
560 ///
561 /// - group `0` takes values a flush finds overwritten within the memtable
562 /// it writes, which are likely to be overwritten again soon;
563 /// - group `1` takes every other value written for the first time;
564 /// - a value relocated out of a file moves one group up, having outlived
565 /// it, up to the last group.
566 ///
567 /// [`Self::lifetime_hint`] overrides the observed group per key. On Linux
568 /// the device is also told each file's expected lifetime
569 /// (`F_SET_RW_HINT`), which a flash device uses to keep data that dies
570 /// together in the same erase blocks.
571 ///
572 /// The group only decides where a value is written. Which values are
573 /// visible, and when a blob file may be removed, follows references and
574 /// retention exactly as without grouping. Separating values by lifetime
575 /// spreads keys that a range scan reads together over more files, so it
576 /// trades scan locality for less relocation work.
577 ///
578 /// Defaults to [`LifetimeGroups::MAX`], four groups. [`LifetimeGroups::ONE`]
579 /// turns grouping off: every value then shares the same files and no
580 /// device hint is given. Any count from one to [`LifetimeGroups::MAX`] is
581 /// accepted ([`LifetimeGroups::new`] refuses the rest), and a flush or a
582 /// relocation keeps at most that many files open per blob codec.
583 ///
584 /// The setting applies to the files written from now on and may change
585 /// between opens: a file keeps the class it was written with, and a class
586 /// above the configured groups counts as the last group.
587 ///
588 /// # Examples
589 ///
590 /// ```
591 /// use lsm_tree::{KvSeparationOptions, config::LifetimeGroups};
592 ///
593 /// // Grouped by default.
594 /// assert_eq!(
595 /// KvSeparationOptions::default().lifetime_groups,
596 /// LifetimeGroups::MAX,
597 /// );
598 ///
599 /// // Fewer groups, or none at all.
600 /// let groups = LifetimeGroups::new(2).expect("within the bound");
601 /// let two = KvSeparationOptions::default().lifetime_groups(groups);
602 /// assert_eq!(two.lifetime_groups.get(), 2);
603 /// let off = KvSeparationOptions::default().lifetime_groups(LifetimeGroups::ONE);
604 /// assert_eq!(off.lifetime_groups.get(), 1);
605 /// ```
606 #[must_use]
607 pub fn lifetime_groups(mut self, groups: LifetimeGroups) -> Self {
608 self.lifetime_groups = groups;
609 self
610 }
611
612 /// Sets the caller's estimate of each key's lifetime class, used over
613 /// the observed one wherever it answers (see [`Self::lifetime_groups`]).
614 ///
615 /// `hint` returns the class for a key, `0` for the shortest-lived; a class
616 /// past the last group goes to the last group, and `None` leaves the key
617 /// to the observed estimate. The hint is advisory: a wrong one costs
618 /// relocation work and never changes what a read returns. It has no effect
619 /// with a single group.
620 ///
621 /// # Examples
622 ///
623 /// ```
624 /// use lsm_tree::{
625 /// KvSeparationOptions,
626 /// config::{LifetimeGroups, LifetimeHint},
627 /// };
628 ///
629 /// // Session payloads churn, archived objects stay.
630 /// let hint = LifetimeHint::new(|key: &[u8]| {
631 /// if key.starts_with(b"session/") {
632 /// Some(0)
633 /// } else if key.starts_with(b"archive/") {
634 /// Some(3)
635 /// } else {
636 /// None
637 /// }
638 /// });
639 /// let opts = KvSeparationOptions::default()
640 /// .lifetime_groups(LifetimeGroups::MAX)
641 /// .lifetime_hint(hint);
642 /// assert!(opts.lifetime_hint.is_some());
643 /// ```
644 #[must_use]
645 pub fn lifetime_hint(mut self, hint: LifetimeHint) -> Self {
646 self.lifetime_hint = Some(hint);
647 self
648 }
649
650 /// Sets the staleness threshold percentage.
651 ///
652 /// The staleness percentage determines how much a blob file needs to be fragmented to be
653 /// picked up by the garbage collection.
654 ///
655 /// Defaults to 33%.
656 #[must_use]
657 pub fn staleness_threshold(mut self, ratio: f32) -> Self {
658 self.staleness_threshold = ratio;
659 self
660 }
661
662 /// Sets the age cutoff threshold.
663 ///
664 /// Defaults to 20%.
665 #[must_use]
666 pub fn age_cutoff(mut self, ratio: f32) -> Self {
667 self.age_cutoff = ratio;
668 self
669 }
670
671 /// Supplies a zstd dictionary for the blob compression to name.
672 ///
673 /// The open registers it with the tree, which stores it, so a later open
674 /// needs nothing supplied; this is also how the bytes reach a tree the
675 /// first time. Which dictionary blob files are written against is decided
676 /// by [`Config::blob_compression`]: a [`CompressionType::ZstdDict`] there
677 /// names one by id, and [`Config::open`] refuses one the tree does not hold
678 /// with [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch).
679 #[cfg(zstd_any)]
680 #[must_use]
681 pub fn dict(
682 mut self,
683 dictionary: alloc::sync::Arc<crate::compression::ZstdDictionary>,
684 ) -> Self {
685 self.zstd_dictionary = Some(dictionary);
686 self
687 }
688}
689
690/// Where a compaction ends its outputs.
691#[derive(Clone, Copy, Debug, PartialEq, Eq)]
692pub(crate) enum OutputCuts {
693 /// On the boundaries between the tables of the level the outputs are
694 /// merged into next, within half the target size and twice it.
695 Aligned,
696 /// At the target size alone.
697 TargetSize,
698}
699
700/// Tree configuration builder
701// Clone: every shared handle is `Arc`-backed, so a clone is a cheap second
702// reference to the same backends — which is what lets `open_or_repair` retry
703// an `open(self)` after a repair.
704#[derive(Clone)]
705pub struct Config {
706 /// Folder path
707 #[doc(hidden)]
708 pub path: PathBuf,
709
710 /// Default filesystem backend for levels without an explicit route.
711 ///
712 /// Defaults to [`StdFs`]. Use [`Config::with_fs`] to plug in an
713 /// alternative backend such as [`MemFs`](crate::fs::MemFs).
714 ///
715 /// Both fresh tree creation and reopening (recovery) are supported
716 /// for any backend that implements [`Fs`].
717 #[doc(hidden)]
718 pub fs: Arc<dyn Fs>,
719
720 /// Per-level filesystem routing for tiered storage.
721 ///
722 /// When set, tables at different LSM levels can be stored on different
723 /// storage devices (e.g., NVMe for L0–L1, SSD for L2–L4, HDD for L5–L6).
724 /// Each entry maps a range of levels to a base directory and filesystem
725 /// backend. Uncovered levels fall back to the primary `path` and `fs`.
726 ///
727 /// Zero additional overhead when `None` — only a single branch check;
728 /// path construction allocations are unchanged.
729 #[doc(hidden)]
730 pub level_routes: Option<Vec<LevelRoute>>,
731
732 /// Block cache to use
733 #[doc(hidden)]
734 pub cache: Arc<Cache>,
735
736 /// Descriptor table to use
737 #[doc(hidden)]
738 pub descriptor_table: Option<Arc<DescriptorTable>>,
739
740 /// Number of levels of the LSM tree (depth of tree)
741 ///
742 /// Once set, the level count is fixed (in the manifest snapshot `v<id>`)
743 pub level_count: u8,
744
745 /// Restart interval inside data blocks
746 pub data_block_restart_interval_policy: RestartIntervalPolicy,
747
748 /// Restart interval inside index blocks
749 pub index_block_restart_interval_policy: RestartIntervalPolicy,
750
751 /// Block size of data blocks
752 pub data_block_size_policy: BlockSizePolicy,
753
754 /// Uncompressed size a columnar table's row groups are cut at, per level:
755 /// the columnar counterpart of [`Self::data_block_size_policy`], which a
756 /// columnar table does not use for its data. A row group is split into
757 /// independently readable column pages, so it is sized for what a read of
758 /// one page is worth rather than for what a read of the whole group costs.
759 pub columnar_row_group_size_policy: BlockSizePolicy,
760
761 /// Uncompressed bytes of row data a columnar row page holds, per level.
762 /// A row group's rows are cut into row pages shared by every column, so a
763 /// read of a few rows decodes only the pages that hold them.
764 pub columnar_page_size_policy: BlockSizePolicy,
765
766 /// How a columnar table's column pages store their values, per level;
767 /// see [`ColumnEncoding`].
768 pub column_encoding_policy: ColumnEncodingPolicy,
769
770 /// How columnar reads of this tree fetch their pages; see [`ReadBudget`].
771 pub columnar_read_budget: ReadBudget,
772
773 /// The page bytes one projected columnar scan holds at once, shared by
774 /// the segments it merges; see [`Self::columnar_scan_budget`].
775 pub columnar_scan_budget: u64,
776
777 /// The filter and index bytes a multi-get holds at once across the tables
778 /// of a level it reads stage by stage; see
779 /// [`Self::multi_get_metadata_budget`].
780 pub multi_get_metadata_budget: u64,
781
782 /// Whether to pin index blocks
783 pub index_block_pinning_policy: PinningPolicy,
784
785 /// Whether to pin filter blocks
786 pub filter_block_pinning_policy: PinningPolicy,
787
788 /// Whether to pin top level index of partitioned index
789 pub top_level_index_block_pinning_policy: PinningPolicy,
790
791 /// Whether to pin top level index of partitioned filter
792 pub top_level_filter_block_pinning_policy: PinningPolicy,
793
794 /// Data block hash ratio
795 pub data_block_hash_ratio_policy: HashRatioPolicy,
796
797 /// Whether to partition index blocks
798 pub index_block_partitioning_policy: PartitioningPolicy,
799
800 /// Whether to partition filter blocks
801 pub filter_block_partitioning_policy: PartitioningPolicy,
802
803 /// Partition size when using partitioned indexes, per level; see
804 /// [`Self::index_block_partition_size_policy`].
805 pub index_block_partition_size_policy: BlockSizePolicy,
806
807 /// Partition size when using partitioned filters, per level; see
808 /// [`Self::filter_block_partition_size_policy`].
809 pub filter_block_partition_size_policy: BlockSizePolicy,
810
811 /// If `true`, the last level will not build filters, reducing the filter size of a database
812 /// by ~90% typically
813 pub(crate) expect_point_read_hits: bool,
814
815 /// Where a compaction ends its outputs; see
816 /// [`Self::compaction_output_alignment`].
817 pub(crate) compaction_output_cuts: OutputCuts,
818
819 /// Per-block Page ECC. When `true`, every block on disk carries a parity
820 /// trailer; on read, if the block's XXH3 disagrees with the on-disk bytes,
821 /// the reader attempts recovery from the trailer before surfacing the
822 /// corruption. The correction scheme is selected at runtime
823 /// (`update_runtime_config`): per-word SEC-DED (the default), single XOR
824 /// parity, or Reed-Solomon. Requires the `page_ecc` cargo feature — opening a
825 /// tree with `page_ecc = true` on a build without the feature returns
826 /// [`crate::Error::PageEccUnsupported`].
827 ///
828 /// Off by default. `RocksDB` ships per-block ECC as an operator-
829 /// chosen knob (typically off on RAID-protected media, on on
830 /// single-drive) and the cost is non-trivial on the write path,
831 /// so the default keeps the existing behaviour.
832 pub(crate) page_ecc: bool,
833
834 /// Initial [`crate::runtime_config::RuntimeConfig`] snapshot
835 /// the tree starts with. Seeds both the first
836 /// `persist_version` call and the Tree's
837 /// `RuntimeConfigHandle`, so a non-default value supplied via
838 /// [`Config::with_runtime_config`] is honoured from byte zero
839 /// of the manifest. Defaults to `RuntimeConfig::default()` —
840 /// matches the pre-existing implicit behaviour.
841 #[expect(
842 clippy::struct_field_names,
843 reason = "name mirrors the type for grep-ability across the persist + Tree handle init wiring"
844 )]
845 pub(crate) initial_runtime_config: crate::runtime_config::RuntimeConfig,
846
847 /// Filter construction policy
848 pub filter_policy: FilterPolicy,
849
850 /// Allocates filter memory by measured negative-probe load; `None` (the
851 /// default) leaves every filter to [`Self::filter_policy`].
852 pub filter_advisor: Option<FilterAdvisor>,
853
854 /// Retrieval-ribbon locator policy (per level). Defaults to
855 /// [`LocatorPolicy::block_level`]: written SSTs carry an optional `locator`
856 /// section mapping each key to its data block for O(1) point reads (skipping
857 /// the index-block binary search). Set [`LocatorPolicy::disabled`] to opt
858 /// out — disabled levels produce byte-identical SSTs (no section).
859 pub locator_policy: LocatorPolicy,
860
861 /// Compaction filter factory
862 pub compaction_filter_factory: Option<Arc<dyn Factory>>,
863
864 /// Prefix extractor for prefix bloom filters.
865 ///
866 /// When set, the bloom filter indexes extracted prefixes in addition to
867 /// full keys, allowing prefix scans to skip segments that contain no
868 /// matching prefixes.
869 pub prefix_extractor: Option<Arc<dyn PrefixExtractor>>,
870
871 /// Key domains no table spans (see [`Config::key_domains`]).
872 pub key_domains: Option<Arc<dyn crate::KeyDomains>>,
873
874 /// Merge operator for commutative operations
875 ///
876 /// When set, enables `merge()` operations that store partial updates
877 /// which are lazily combined during reads and compaction.
878 pub merge_operator: Option<Arc<dyn MergeOperator>>,
879
880 #[doc(hidden)]
881 pub kv_separation_opts: Option<KvSeparationOptions>,
882
883 /// Custom user key comparator.
884 ///
885 /// When set, all key comparisons use this comparator instead of the
886 /// default lexicographic byte ordering. Once a tree is opened with a
887 /// comparator, it must always be re-opened with the same comparator.
888 // Not `pub` — use `Config::comparator()` builder method as the public API.
889 #[doc(hidden)]
890 pub(crate) comparator: SharedComparator,
891
892 /// Block-level encryption provider for encryption at rest.
893 ///
894 /// When set, all blocks (data, index, filter, meta) are encrypted
895 /// using this provider after compression and before checksumming.
896 pub(crate) encryption: Option<Arc<dyn EncryptionProvider>>,
897
898 /// The wall clock of this tree; `None` reads the process clock (see
899 /// [`Config::with_clock`]).
900 pub(crate) clock: Option<Arc<dyn crate::Clock>>,
901
902 /// Policy governing what `Tree::open` does when the on-disk
903 /// MANIFEST contains corrupt records. Defaults to
904 /// [`ManifestRecoveryMode::AbsoluteConsistency`], the only
905 /// production-safe choice — any corruption aborts the open. The only
906 /// other mode, [`ManifestRecoveryMode::TolerateCorruptedTailRecords`],
907 /// additionally drops a torn edit-log tail (an append the writer never
908 /// finished); damage to anything committed fails the open in both.
909 pub(crate) manifest_recovery_mode: ManifestRecoveryMode,
910
911 /// Durability level for every fsync the tree issues (SST writes,
912 /// manifest, version persist, directory syncs).
913 ///
914 /// Defaults to [`SyncMode::Normal`] (plain `fsync`), matching the
915 /// out-of-the-box durability of `RocksDB` and `SQLite`. Only observable on
916 /// macOS, where [`SyncMode::Full`] opts into the much slower
917 /// `F_FULLFSYNC` barrier and [`SyncMode::Barrier`] into ordered syncs;
918 /// on other platforms all modes are plain `fsync`. Set via
919 /// [`Config::sync_mode`].
920 pub(crate) sync_mode: SyncMode,
921
922 /// When a flush's tables become durable. Set via
923 /// [`Config::flush_durability`].
924 pub(crate) flush_durability: FlushDurability,
925
926 /// Bytes a table or blob file gathers between the writebacks its writer
927 /// starts. Set via [`Config::writeback_bytes`].
928 pub(crate) writeback_bytes: u64,
929
930 /// Size a flush or an ingestion cuts its output tables at. Set via
931 /// [`Config::table_target_size`].
932 pub(crate) table_target_size: u64,
933
934 /// When `true` (the default), [`Config::open`] and [`Config::repair`]
935 /// acquire an exclusive cross-process lock on a `LOCK` file in the tree
936 /// directory (an advisory OS file lock) and hold it for the lifetime of the
937 /// [`Tree`] (open) or the duration of the call (repair). A
938 /// second process attempting to open / repair the same directory fails fast
939 /// with [`Error::Locked`](crate::Error::Locked) instead of racing on the
940 /// manifest. Set `false` via [`Config::with_directory_lock`] only when the
941 /// embedder already enforces exclusive directory ownership at a higher layer
942 /// (e.g. a keyspace / journal manager). Best-effort per `Fs` backend: real
943 /// on-disk backends enforce it, in-memory backends are single-process and
944 /// satisfy it vacuously.
945 pub(crate) directory_lock: bool,
946
947 /// Shared live-progress counters the repair / salvage paths tick while
948 /// they run, or `None` (the default) to skip publishing. Set via
949 /// [`Config::with_recovery_progress`]; observed by polling
950 /// [`RecoveryProgress::snapshot`](crate::RecoveryProgress::snapshot) from
951 /// another thread.
952 #[cfg(feature = "std")]
953 pub(crate) recovery_progress: Option<Arc<crate::RecoveryProgress>>,
954
955 /// Edit-log size (bytes) past which the next manifest persist rotates: it
956 /// writes a fresh full snapshot and starts an empty log instead of appending
957 /// another [`VersionEdit`](crate::version::edit::VersionEdit). Bounds both
958 /// recovery replay time (edits to re-apply) and log disk use, while keeping
959 /// the common per-flush path a tiny `O(changed-levels)` append rather than an
960 /// `O(all-SSTs)` full manifest rewrite.
961 ///
962 /// Defaults to 1 MiB (≈ tens of thousands of edits). Set via
963 /// [`Config::manifest_log_rotate_bytes`]. A smaller value rotates more
964 /// often (shorter recovery, more frequent full-snapshot writes); `0` rotates
965 /// on every upgrade, degenerating to the full-rewrite-per-version behaviour.
966 pub(crate) manifest_log_rotate_bytes: u64,
967
968 /// Empty table files each tables folder creates at a time, ahead of the
969 /// tables written into them, with one folder sync for the batch. Set via
970 /// [`Config::table_file_pool`].
971 pub(crate) table_file_pool: usize,
972
973 /// Where a table file pool refills: off the claiming write's thread, or
974 /// on it, as it does without `std`. Set via
975 /// [`Config::table_file_refill_inline`].
976 pub(crate) table_file_refill: crate::table::file_pool::RefillOn,
977
978 /// Retention floor a manifest REPAIR seeds the rebuilt version with: the
979 /// highest snapshot seqno the repaired tree refuses to serve. The lost
980 /// manifest was the only record of the floor a past GC compaction or
981 /// `clear` established, and the tables cannot stand in for it, so the
982 /// deployment that ran those compactions (and knows the watermark it
983 /// passed) supplies it here. Defaults to `0`: a repaired tree serves
984 /// every snapshot, which is right only if history was never collected.
985 /// Set via [`Config::repair_retention_floor`].
986 pub(crate) repair_retention_floor: crate::SeqNo,
987
988 /// Compaction I/O rate limit in bytes per second.
989 ///
990 /// Caps the rate at which the compaction worker is allowed to issue
991 /// I/O, so background compaction cannot saturate the device and starve
992 /// user point reads / range scans (P99 stability). `0` (the default)
993 /// means unlimited — no throttling, no behaviour change. Flush and
994 /// user reads are never throttled, only compaction. Set via
995 /// [`Config::compaction_rate_limit`].
996 pub(crate) compaction_rate_limit: u64,
997
998 /// Optional shared compaction rate limiter. `None` (default) = the tree
999 /// builds its own from [`Self::compaction_rate_limit`]. `Some` = every
1000 /// tree holding this `Arc` is bounded by it together, at the rate it
1001 /// holds. Set via [`Config::compaction_rate_limiter`].
1002 pub(crate) compaction_rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
1003
1004 /// The limiter the open tree throttles its compactions with, set at open
1005 /// for a strategy to read the live rate. Apart from
1006 /// [`Self::compaction_rate_limiter`], so a copy of the tree's
1007 /// configuration opened as another tree does not share it.
1008 pub(crate) live_compaction_rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
1009
1010 /// Optional pool compactions reserve their room in, shared with other
1011 /// trees. `None` (default) = the tree keeps a pool of its own. Set via
1012 /// [`Config::compaction_capacity`].
1013 pub(crate) compaction_capacity: Option<Arc<crate::CapacityPool>>,
1014
1015 /// Bytes per second writes are paced to while the backpressure verdict is
1016 /// in its slowdown tier; `0` (the default) leaves the slowdown delay to
1017 /// the ramp. Set via [`Config::write_rate_limit`].
1018 pub(crate) write_rate_limit: u64,
1019
1020 /// Optional write rate limiter shared with other trees, used in place of
1021 /// the per-tree one built from [`Self::write_rate_limit`]. Set via
1022 /// [`Config::write_rate_limiter`].
1023 pub(crate) write_rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
1024
1025 /// Optional write-buffer budget shared with other trees, which the
1026 /// memtable axis of the backpressure verdict compares in place of this
1027 /// tree's own unflushed bytes. Set via [`Config::write_buffer_budget`].
1028 // no-std: the budget reads members through their std-only arc-swap mirror
1029 #[cfg(feature = "std")]
1030 pub(crate) write_buffer_budget: Option<Arc<crate::backpressure::WriteBufferBudget>>,
1031
1032 /// Worker-thread count for compaction parallelism (`std` only), used two
1033 /// ways: it sizes the per-tree pool built at open when
1034 /// [`Self::compaction_pool`] is `None`, which prepares data blocks and
1035 /// builds partitioned-filter partitions, and it caps how many range-parallel
1036 /// sub-compactions a single compaction is split into. Default
1037 /// `max(1, available_parallelism / 2)` — leaves half the cores for
1038 /// application work. `1` forces the serial path for both. Without the
1039 /// `parallel` feature there is no built-in pool, so block compression and
1040 /// sub-compaction ranges run serially even for a value > 1. Set via
1041 /// [`Config::compaction_threads`].
1042 #[cfg(feature = "std")] // no-std: parallel compaction unavailable (no threads)
1043 pub(crate) compaction_threads: usize,
1044
1045 /// Optional shared compaction thread pool. `None` (default) = a per-tree
1046 /// pool is built at [`crate::Tree::open`] sized by [`Self::compaction_threads`]
1047 /// (predictable, matches the per-DB pattern). `Some` = caller-supplied
1048 /// executor shared across every tree holding this `Arc`, bounding total
1049 /// threads regardless of tree count. Set via [`Config::compaction_pool`].
1050 #[cfg(feature = "std")]
1051 pub(crate) compaction_pool: Option<Arc<dyn crate::table::writer::CompactionSpawner>>,
1052
1053 /// Payload size (bytes) below which a block of a parallel-compressed table
1054 /// is prepared on the writer thread instead of a worker: its transform
1055 /// costs less than handing it to a worker and back. `None` (default)
1056 /// derives it from the table's block codec. Set via
1057 /// [`Config::parallel_compression_inline_below`].
1058 #[cfg(feature = "std")]
1059 pub(crate) parallel_compression_inline_below: Option<u32>,
1060
1061 /// Minimum total input size (bytes) for a compaction to be split into
1062 /// parallel sub-compactions. Below it the compaction stays single-threaded
1063 /// (per-thread setup + extra output tables outweigh the parallelism on small
1064 /// compactions). Default
1065 /// [`SUBCOMPACTION_MIN_INPUT_BYTES`](crate::compaction::worker::SUBCOMPACTION_MIN_INPUT_BYTES)
1066 /// (8 MiB). Set via [`Config::subcompaction_min_bytes`].
1067 #[cfg(feature = "std")]
1068 pub(crate) subcompaction_min_bytes: u64,
1069
1070 /// Test-only failpoint: when armed, the first parallel sub-compaction range
1071 /// that observes it returns an error and disarms it, so the crash-safety
1072 /// rollback paths (sibling output rollback, input restore) can be exercised
1073 /// deterministically. Behind `cfg(test)`, never compiled into release builds.
1074 #[cfg(all(test, feature = "std"))]
1075 pub(crate) fail_one_subcompaction: Arc<core::sync::atomic::AtomicBool>,
1076
1077 /// Test-only failpoint: when armed, the serial merge raises the stop signal
1078 /// after writing its first item and disarms itself, so the interrupted-merge
1079 /// path is reachable without racing a tree drop against an in-flight
1080 /// compaction. Behind `cfg(test)`, never compiled into release builds.
1081 #[cfg(all(test, feature = "std"))]
1082 pub(crate) stop_serial_merge_after_first_item: Arc<core::sync::atomic::AtomicBool>,
1083
1084 /// Test-only failpoint: when armed, a tight-space compaction returns an error
1085 /// immediately after durably installing (and punching) its FIRST slice, so
1086 /// the crash-mid-loop recovery path (reopen a tree whose manifest carries a
1087 /// persisted input restriction) can be exercised deterministically. Behind
1088 /// `cfg(test)`, never compiled into release builds.
1089 #[cfg(all(test, feature = "std"))]
1090 pub(crate) fail_tight_after_first_slice: Arc<core::sync::atomic::AtomicBool>,
1091
1092 /// Test-only: bytes another tree sharing the capacity pool takes on the
1093 /// destination volume right before a tight-space pass reserves its first
1094 /// slice, after the pass measured the room. Behind `cfg(test)`.
1095 #[cfg(all(test, feature = "std"))]
1096 pub(crate) tight_slice_squeeze: Arc<core::sync::atomic::AtomicU64>,
1097
1098 /// Test-only failpoint: when armed, a tight-space relocation fails at the
1099 /// restricted-blob reopen step of its current slice — after the slice's
1100 /// outputs were finalized but before the install — so the pre-install
1101 /// rollback (retract the finalized-but-unreferenced outputs) can be
1102 /// exercised deterministically. Behind `cfg(test)`, never compiled into
1103 /// release builds.
1104 #[cfg(all(test, feature = "std"))]
1105 pub(crate) fail_tight_blob_reopen: Arc<core::sync::atomic::AtomicBool>,
1106
1107 /// Test-only failpoint: run once, then disarmed, between the moment a write
1108 /// has finished its output files and the moment it installs them (a flush,
1109 /// an ingestion, a parallel or tight-space compaction), so a test can put a
1110 /// concurrent operation into that window deterministically. Behind
1111 /// `cfg(test)`, never compiled into release builds.
1112 #[cfg(all(test, feature = "std"))]
1113 pub(crate) before_output_install: Arc<std::sync::Mutex<Option<OutputInstallHook>>>,
1114
1115 /// Test-only failpoint: run once, then disarmed, while a compaction
1116 /// finishes its outputs, after their last write and before their reopen.
1117 #[cfg(all(test, feature = "std"))]
1118 pub(crate) while_outputs_finish: Arc<std::sync::Mutex<Option<OutputInstallHook>>>,
1119
1120 /// A dictionary supplied for the data block policy to name. The open
1121 /// registers it with the tree, so it survives the reopen without being
1122 /// supplied again.
1123 ///
1124 /// Supply only: which dictionary new blocks are written against is the
1125 /// policy's to say, by id, and a writer resolves that id from
1126 /// [`Self::zstd_dictionaries`] when it starts. A slot filled at open could
1127 /// not follow a policy that changes on a live tree.
1128 #[cfg(zstd_any)]
1129 pub(crate) zstd_dictionary: Option<Arc<crate::compression::ZstdDictionary>>,
1130
1131 /// Every dictionary the tree can decompress against: the ones it has
1132 /// registered, plus [`Self::zstd_dictionary`] when one was supplied.
1133 ///
1134 /// Populated at open from the tree's own `dicts/` folder, so a reopen needs
1135 /// no dictionary from the caller at all. A table resolves the id it
1136 /// recorded against this set rather than being compared to a single
1137 /// configured dictionary, which is what lets one tree hold data written
1138 /// under more than one.
1139 ///
1140 /// Shared and swappable, because registration happens on a LIVE tree: a
1141 /// plain field would leave a freshly registered dictionary unresolvable
1142 /// until the next open, so the very tables the registration was made for
1143 /// would fail to read.
1144 #[cfg(zstd_any)]
1145 pub(crate) zstd_dictionaries: Arc<arc_swap::ArcSwap<crate::compression::ZstdDictionaries>>,
1146
1147 /// The global sequence number generator.
1148 ///
1149 /// Should be shared between multiple trees of a database.
1150 pub(crate) seqno: SharedSequenceNumberGenerator,
1151
1152 /// Sequence number watermark that is visible to readers.
1153 ///
1154 /// Used for MVCC snapshots and to control which updates are
1155 /// observable in a given view of the database.
1156 pub(crate) visible_seqno: SharedSequenceNumberGenerator,
1157}
1158
1159// TODO: remove default?
1160// std-only: the default backend is `StdFs` and the default path is resolved
1161// via std::path::absolute. no_std callers construct `Config` explicitly with a
1162// caller-provided `Fs`.
1163#[cfg(feature = "std")]
1164impl Default for Config {
1165 fn default() -> Self {
1166 Self {
1167 path: absolute_path(Path::new(DEFAULT_FILE_FOLDER)),
1168 fs: Arc::new(StdFs),
1169 level_routes: None,
1170 descriptor_table: Some(Arc::new(DescriptorTable::new(256))),
1171 seqno: SharedSequenceNumberGenerator::from(SequenceNumberCounter::default()),
1172 visible_seqno: SharedSequenceNumberGenerator::from(SequenceNumberCounter::default()),
1173
1174 cache: Arc::new(Cache::with_capacity_bytes(
1175 /* 16 MiB */ 16 * 1_024 * 1_024,
1176 )),
1177
1178 data_block_restart_interval_policy: RestartIntervalPolicy::all(16),
1179 index_block_restart_interval_policy: RestartIntervalPolicy::all(1),
1180
1181 level_count: DEFAULT_LEVEL_COUNT,
1182
1183 data_block_size_policy: BlockSizePolicy::all(4_096),
1184
1185 columnar_row_group_size_policy: BlockSizePolicy::all(DEFAULT_COLUMNAR_ROW_GROUP_SIZE),
1186
1187 columnar_page_size_policy: BlockSizePolicy::all(DEFAULT_COLUMNAR_PAGE_SIZE),
1188
1189 column_encoding_policy: ColumnEncodingPolicy::default(),
1190
1191 columnar_read_budget: ReadBudget::default(),
1192 columnar_scan_budget: DEFAULT_COLUMNAR_SCAN_BUDGET,
1193 multi_get_metadata_budget: DEFAULT_MULTI_GET_METADATA_BUDGET,
1194
1195 index_block_pinning_policy: PinningPolicy::new([true, true, false]),
1196 filter_block_pinning_policy: PinningPolicy::new([true, false]),
1197
1198 top_level_index_block_pinning_policy: PinningPolicy::all(true), // TODO: implement
1199 top_level_filter_block_pinning_policy: PinningPolicy::all(true), // TODO: implement
1200
1201 // Partitioned at every level so a bit-flip inside one
1202 // sub-index block only takes out the keys covered by that
1203 // partition, not the entire SST. A full-index SST has no
1204 // within-block redundancy: one corrupt byte in the single
1205 // index block makes every data block in the table
1206 // unreachable. See tests/partitioned_index_blast_radius.rs
1207 // for the isolation property this default relies on.
1208 index_block_partitioning_policy: PinningPolicy::all(true),
1209 // Filter-block default intentionally left at the pre-#329
1210 // shape (L3+ only). A corrupt filter block can produce a
1211 // false negative (filter says "not present" → read short-
1212 // circuits → caller misses an existing key), which is a
1213 // correctness hazard distinct from index corruption (where
1214 // the read fails loudly). Flipping this default is tracked
1215 // as a separate decision pending a filter blast-radius /
1216 // false-negative analysis; symmetry with index is not
1217 // sufficient justification on its own.
1218 filter_block_partitioning_policy: PinningPolicy::new([false, false, false, true]),
1219
1220 index_block_partition_size_policy: BlockSizePolicy::all(4_096),
1221 filter_block_partition_size_policy: BlockSizePolicy::all(4_096),
1222
1223 data_block_hash_ratio_policy: HashRatioPolicy::all(0.0),
1224
1225 locator_policy: LocatorPolicy::block_level(),
1226 filter_policy: FilterPolicy::all(FilterPolicyEntry::Bloom(
1227 BloomConstructionPolicy::BitsPerKey(10.0),
1228 )),
1229 filter_advisor: None,
1230
1231 compaction_filter_factory: None,
1232 merge_operator: None,
1233
1234 prefix_extractor: None,
1235 key_domains: None,
1236
1237 expect_point_read_hits: false,
1238
1239 compaction_output_cuts: OutputCuts::Aligned,
1240
1241 page_ecc: false,
1242
1243 initial_runtime_config: crate::runtime_config::RuntimeConfig::default(),
1244
1245 kv_separation_opts: None,
1246
1247 #[cfg(zstd_any)]
1248 zstd_dictionary: None,
1249 #[cfg(zstd_any)]
1250 zstd_dictionaries: Arc::new(arc_swap::ArcSwap::from_pointee(
1251 crate::compression::ZstdDictionaries::new(),
1252 )),
1253
1254 comparator: comparator::default_comparator(),
1255 encryption: None,
1256 clock: None,
1257 manifest_recovery_mode: ManifestRecoveryMode::AbsoluteConsistency,
1258 sync_mode: SyncMode::Normal,
1259 flush_durability: FlushDurability::Synced,
1260 writeback_bytes: 1_024 * 1_024,
1261 table_target_size: DEFAULT_TABLE_TARGET_SIZE,
1262 directory_lock: true,
1263 #[cfg(feature = "std")]
1264 recovery_progress: None,
1265 manifest_log_rotate_bytes: 1024 * 1024,
1266 table_file_pool: crate::table::file_pool::DEFAULT_TABLE_FILE_POOL,
1267 table_file_refill: crate::table::file_pool::RefillOn::Background,
1268 repair_retention_floor: 0,
1269 compaction_rate_limit: 0,
1270 compaction_rate_limiter: None,
1271 live_compaction_rate_limiter: None,
1272 compaction_capacity: None,
1273 write_rate_limit: 0,
1274 write_rate_limiter: None,
1275 #[cfg(feature = "std")]
1276 write_buffer_budget: None,
1277
1278 #[cfg(feature = "std")]
1279 compaction_threads: std::thread::available_parallelism()
1280 .map_or(1, |n| (n.get() / 2).max(1)),
1281 #[cfg(feature = "std")]
1282 compaction_pool: None,
1283 #[cfg(feature = "std")]
1284 parallel_compression_inline_below: None,
1285 #[cfg(feature = "std")]
1286 subcompaction_min_bytes: crate::compaction::worker::SUBCOMPACTION_MIN_INPUT_BYTES,
1287 #[cfg(all(test, feature = "std"))]
1288 fail_one_subcompaction: Arc::new(core::sync::atomic::AtomicBool::new(false)),
1289 #[cfg(all(test, feature = "std"))]
1290 stop_serial_merge_after_first_item: Arc::new(core::sync::atomic::AtomicBool::new(
1291 false,
1292 )),
1293 #[cfg(all(test, feature = "std"))]
1294 fail_tight_after_first_slice: Arc::new(core::sync::atomic::AtomicBool::new(false)),
1295 #[cfg(all(test, feature = "std"))]
1296 tight_slice_squeeze: Arc::new(core::sync::atomic::AtomicU64::new(0)),
1297 #[cfg(all(test, feature = "std"))]
1298 fail_tight_blob_reopen: Arc::new(core::sync::atomic::AtomicBool::new(false)),
1299 #[cfg(all(test, feature = "std"))]
1300 before_output_install: Arc::new(std::sync::Mutex::new(None)),
1301 #[cfg(all(test, feature = "std"))]
1302 while_outputs_finish: Arc::new(std::sync::Mutex::new(None)),
1303 }
1304 }
1305}
1306
1307/// The callback the [`Config::before_output_install`] failpoint runs.
1308#[cfg(all(test, feature = "std"))]
1309pub(crate) type OutputInstallHook = Box<dyn FnOnce() + Send>;
1310
1311#[cfg(all(test, feature = "std"))]
1312impl Config {
1313 /// Arms the [`Self::before_output_install`] failpoint with `hook`.
1314 pub(crate) fn arm_before_output_install(&self, hook: impl FnOnce() + Send + 'static) {
1315 #[expect(clippy::unwrap_used, reason = "test-only seam")]
1316 self.before_output_install
1317 .lock()
1318 .unwrap()
1319 .replace(Box::new(hook));
1320 }
1321
1322 /// Runs the [`Self::before_output_install`] failpoint once, if armed.
1323 pub(crate) fn fire_before_output_install(&self) {
1324 #[expect(clippy::unwrap_used, reason = "test-only seam")]
1325 let hook = self.before_output_install.lock().unwrap().take();
1326 if let Some(hook) = hook {
1327 hook();
1328 }
1329 }
1330
1331 /// Arms the [`Self::while_outputs_finish`] failpoint with `hook`.
1332 pub(crate) fn arm_while_outputs_finish(&self, hook: impl FnOnce() + Send + 'static) {
1333 #[expect(clippy::unwrap_used, reason = "test-only seam")]
1334 self.while_outputs_finish
1335 .lock()
1336 .unwrap()
1337 .replace(Box::new(hook));
1338 }
1339
1340 /// Runs the [`Self::while_outputs_finish`] failpoint once, if armed.
1341 pub(crate) fn fire_while_outputs_finish(&self) {
1342 #[expect(clippy::unwrap_used, reason = "test-only seam")]
1343 let hook = self.while_outputs_finish.lock().unwrap().take();
1344 if let Some(hook) = hook {
1345 hook();
1346 }
1347 }
1348}
1349
1350/// Name of the lock file created in a tree directory for the cross-process
1351/// exclusive directory lock.
1352#[cfg_attr(
1353 not(feature = "std"),
1354 allow(
1355 dead_code,
1356 reason = "directory-lock filename used only by the std-gated lock-acquisition path"
1357 )
1358)]
1359pub(crate) const DIRECTORY_LOCK_FILE: &str = "LOCK";
1360
1361/// How an open comes by the directory lock.
1362#[cfg(feature = "std")]
1363pub(crate) enum DirectoryLock {
1364 /// The open takes it.
1365 Acquire,
1366 /// The caller already holds it (`None` when the config disables the lock),
1367 /// having written the tree's files under it.
1368 Held(Option<Box<dyn crate::fs::FsFile>>),
1369}
1370
1371/// Acquires the cross-process exclusive directory lock when `enabled`.
1372///
1373/// Opens (creating if absent) a `LOCK` file under `dir` and takes a
1374/// non-blocking exclusive advisory lock on it through the `Fs` backend. Returns
1375/// the locked handle to hold for as long as exclusivity is required; dropping it
1376/// releases the lock (the OS frees an advisory lock when the fd / handle
1377/// closes). `Ok(None)` when `enabled` is false. Fails with
1378/// [`Error::Locked`](crate::Error::Locked) when another live instance holds the
1379/// lock. The directory must already exist (the caller creates it for a fresh
1380/// tree before acquiring).
1381#[cfg(feature = "std")]
1382pub(crate) fn acquire_directory_lock(
1383 fs: &dyn Fs,
1384 dir: &Path,
1385 enabled: bool,
1386) -> crate::Result<Option<Box<dyn crate::fs::FsFile>>> {
1387 if !enabled {
1388 return Ok(None);
1389 }
1390 let lock_path = dir.join(DIRECTORY_LOCK_FILE);
1391 let file = fs.open(
1392 &lock_path,
1393 &crate::fs::FsOpenOptions::new()
1394 .read(true)
1395 .write(true)
1396 .create(true)
1397 .write_origin(crate::fs::WriteOrigin::Metadata),
1398 )?;
1399 if file.try_lock_exclusive()? {
1400 Ok(Some(file))
1401 } else {
1402 Err(crate::Error::Locked(dir.display().to_string()))
1403 }
1404}
1405
1406impl Config {
1407 /// Initializes a new config
1408 // std-only: seeds the remaining fields from `Config::default`, whose
1409 // default `Fs` is `StdFs`. no_std callers build `Config` field-by-field
1410 // with a caller-provided `Fs`.
1411 #[cfg(feature = "std")]
1412 pub fn new<P: AsRef<Path>>(
1413 path: P,
1414 seqno: SequenceNumberCounter,
1415 visible_seqno: SequenceNumberCounter,
1416 ) -> Self {
1417 Self {
1418 path: absolute_path(path.as_ref()),
1419 seqno: Arc::new(seqno),
1420 visible_seqno: Arc::new(visible_seqno),
1421 ..Default::default()
1422 }
1423 }
1424
1425 /// Sets the default filesystem backend used for levels without an explicit route.
1426 ///
1427 /// Defaults to [`StdFs`]. Use [`MemFs`](crate::fs::MemFs) for
1428 /// in-memory trees (testing, ephemeral indexes).
1429 ///
1430 /// # Example
1431 ///
1432 /// ```
1433 /// # fn main() -> lsm_tree::Result<()> {
1434 /// use lsm_tree::{Config, SequenceNumberCounter};
1435 /// use lsm_tree::fs::MemFs;
1436 ///
1437 /// let tree = Config::new(
1438 /// "/virtual/tree",
1439 /// SequenceNumberCounter::default(),
1440 /// SequenceNumberCounter::default(),
1441 /// )
1442 /// .with_fs(MemFs::new())
1443 /// .open()?;
1444 /// # Ok(())
1445 /// # }
1446 /// ```
1447 #[must_use]
1448 pub fn with_fs<F: Fs>(mut self, fs: F) -> Self {
1449 self.fs = Arc::new(fs);
1450 self
1451 }
1452
1453 /// Sets the default filesystem backend from an existing shared handle.
1454 ///
1455 /// Useful when multiple configs should reuse the same backend
1456 /// instance, including trait objects and backends that are not `Clone`.
1457 ///
1458 #[must_use]
1459 pub fn with_shared_fs(mut self, fs: Arc<dyn Fs>) -> Self {
1460 self.fs = fs;
1461 self
1462 }
1463
1464 /// Opens a tree using the config.
1465 ///
1466 /// # Errors
1467 ///
1468 /// Will return `Err` if an IO error occurs.
1469 /// Returns [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch) if
1470 /// the compression policy references a `dict_id` that doesn't match the
1471 /// configured dictionary.
1472 pub fn open(self) -> crate::Result<AnyTree> {
1473 self.open_with(
1474 #[cfg(feature = "std")]
1475 DirectoryLock::Acquire,
1476 crate::tree::NewFolderNames::Synced,
1477 )
1478 .map(|(tree, _)| tree)
1479 }
1480
1481 /// Opens a tree for each config, making each one that does not exist
1482 /// yet, and makes the new trees' folder names durable with one sync of
1483 /// each folder that holds them, after all of them are made, rather than
1484 /// one sync of it per tree. A consumer that makes many trees in one data
1485 /// directory at once pays one device flush for that directory.
1486 ///
1487 /// The trees come back in the order of `configs`. They are durable once
1488 /// this returns: before that a power loss may take back any new tree of
1489 /// the batch, with everything in it. A batch that fails takes back every
1490 /// tree it made and closes the ones it opened, so a retry finds what was
1491 /// there before it.
1492 ///
1493 /// # Errors
1494 ///
1495 /// As [`Self::open`], for the first config whose tree fails to open, or a
1496 /// failed sync of a folder holding a new tree's name.
1497 ///
1498 /// # Examples
1499 ///
1500 /// ```
1501 /// use lsm_tree::fs::MemFs;
1502 /// use lsm_tree::{AbstractTree, Config, SequenceNumberCounter};
1503 /// use std::sync::Arc;
1504 ///
1505 /// let fs = Arc::new(MemFs::new());
1506 /// let config = |name: &str| {
1507 /// Config::new(
1508 /// format!("/data/{name}"),
1509 /// SequenceNumberCounter::default(),
1510 /// SequenceNumberCounter::default(),
1511 /// )
1512 /// .with_shared_fs(fs.clone())
1513 /// };
1514 /// let trees = Config::open_many([config("nodes"), config("edges")])?;
1515 /// trees[0].insert("a", "node", 0);
1516 /// assert!(trees[1].is_empty(lsm_tree::SeqNo::MAX, None)?);
1517 /// # Ok::<(), lsm_tree::Error>(())
1518 /// ```
1519 pub fn open_many(configs: impl IntoIterator<Item = Self>) -> crate::Result<Vec<AnyTree>> {
1520 crate::tree::open_batch(configs)
1521 }
1522
1523 /// [`Self::open`], coming by the directory lock as `lock` says, and
1524 /// leaving the new tree folder's name to its caller to sync as `names`
1525 /// says; with the tree, when this open made it, what takes it back.
1526 pub(crate) fn open_with(
1527 self,
1528 #[cfg(feature = "std")] lock: DirectoryLock,
1529 names: crate::tree::NewFolderNames,
1530 ) -> crate::Result<(AnyTree, Option<crate::tree::Creation>)> {
1531 // The dictionary registry is loaded and the compression policy validated
1532 // inside `Tree::open`, under the directory lock — NOT here. Both read the
1533 // tree's `dicts/` folder, and the load SWEEPS unpublished temps; doing
1534 // that before the lock lets an opener that is about to fail with
1535 // `Locked` delete a live owner's in-flight registration.
1536
1537 // On a zstd build the live block path seals encrypted blocks through
1538 // the AAD-bound envelope, so the configured provider MUST implement it.
1539 // Reject an opaque-only provider here, at open time, instead of letting
1540 // it fail on the first encrypted read/write.
1541 #[cfg(zstd_any)]
1542 if self
1543 .encryption
1544 .as_ref()
1545 .is_some_and(|enc| !enc.supports_aad_block_path())
1546 {
1547 return Err(crate::Error::Encrypt(
1548 "encryption provider does not implement the AAD-bound block path \
1549 (encrypt_block_aad / decrypt_block_aad) required for encrypted \
1550 blocks on a zstd build",
1551 ));
1552 }
1553
1554 Ok(if self.kv_separation_opts.is_some() {
1555 let (tree, creation) = BlobTree::open(
1556 self,
1557 #[cfg(feature = "std")]
1558 lock,
1559 names,
1560 )?;
1561 (AnyTree::Blob(tree), creation)
1562 } else {
1563 let (tree, creation) = Tree::open(
1564 self,
1565 #[cfg(feature = "std")]
1566 lock,
1567 names,
1568 )?;
1569 (AnyTree::Standard(tree), creation)
1570 })
1571 }
1572
1573 /// Builds a tree at [`Self::path`] from a [`PinnedVersion`](crate::PinnedVersion)
1574 /// taken elsewhere, and opens it.
1575 ///
1576 /// Before the call, the pin's files lie in the tree's folders under their
1577 /// ids: each of [`tables`](crate::PinnedVersion::tables) as `tables/{id}`
1578 /// (with its [`heal_attestation`](crate::PinnedFile::heal_attestation),
1579 /// when it has one, as `tables/{id}.heal-attest`), each of
1580 /// [`blob_files`](crate::PinnedVersion::blob_files) as `blobs/{id}`, each of
1581 /// [`dictionaries`](crate::PinnedVersion::dictionaries) as `dicts/{id}`.
1582 /// `layout` is the pin's [`layout`](crate::PinnedVersion::layout), and
1583 /// `entries` and `range_tombstones` are its
1584 /// [`memtable_entries`](crate::PinnedVersion::memtable_entries) and
1585 /// [`memtable_range_tombstones`](crate::PinnedVersion::memtable_range_tombstones).
1586 ///
1587 /// The directory lock is taken first and held into the open, so no other
1588 /// opener sees the folder half restored. Until the restored memtable
1589 /// contents are durable a `restoring` marker lies in the folder: an
1590 /// ordinary [`Self::open`] refuses it, and calling this again with the
1591 /// same pin finishes an interrupted restore. The files are synced and the
1592 /// layout published as the tree's manifest, then the memtable contents are
1593 /// written with their sequence numbers and flushed. The returned tree reads
1594 /// as the source did at the pin, for every key and every snapshot the
1595 /// source served then. The config's sequence number counters are raised to
1596 /// the pin's seqno (and the seqno counter past every restored version), so
1597 /// later writes land above the restored data. The config must match the
1598 /// source's where it shapes the files: the tree type, comparator,
1599 /// encryption provider and dictionaries.
1600 ///
1601 /// # Errors
1602 ///
1603 /// [`Error::InvalidHeader`](crate::Error::InvalidHeader) for a layout too
1604 /// short to hold its header; [`Error::Locked`](crate::Error::Locked) when
1605 /// another instance holds the folder; an `AlreadyExists` I/O error, before
1606 /// anything is written, when the folder already holds a tree (a manifest
1607 /// snapshot or edit log) that no interrupted restore left; otherwise
1608 /// the failures of the syncs, the
1609 /// manifest write, [`Self::open`] and the flush.
1610 #[cfg(feature = "std")]
1611 pub fn restore_pinned(
1612 self,
1613 layout: &[u8],
1614 entries: impl IntoIterator<Item = crate::PinnedEntry>,
1615 range_tombstones: impl IntoIterator<Item = crate::PinnedRangeTombstone>,
1616 ) -> crate::Result<AnyTree> {
1617 crate::pin::restore(self, layout, entries, range_tombstones)
1618 }
1619
1620 /// A snapshot of the dictionaries the tree can currently decompress
1621 /// against.
1622 ///
1623 /// One load per table open, which is where the set is needed; the returned
1624 /// value is a handle (one `Arc`), not a copy of the dictionaries.
1625 #[cfg(zstd_any)]
1626 pub(crate) fn current_zstd_dictionaries(&self) -> crate::compression::ZstdDictionaries {
1627 self.zstd_dictionaries.load().as_ref().clone()
1628 }
1629
1630 /// Gives this config a registry of its OWN, holding every dictionary the
1631 /// tree at [`Self::path`] stores plus the ones supplied through
1632 /// [`Self::zstd_dictionary`] and the KV-separation options.
1633 ///
1634 /// Every path that opens a table has to call this first: a table resolves
1635 /// the dictionary id it recorded against this set, so an unloaded one fails
1636 /// every dictionary-compressed table in the tree.
1637 ///
1638 /// The registry is REPLACED rather than stored into, because `Config` is
1639 /// `Clone` and a keyspace derives one config per partition from a base. The
1640 /// clones would otherwise share a single set, and the second tree's load
1641 /// would take over the first tree's registry — after which the first tree
1642 /// stops resolving the tables it wrote itself.
1643 ///
1644 /// # Errors
1645 ///
1646 /// Propagates the folder scan and the store's integrity check: a dictionary
1647 /// whose bytes no longer hash to its name fails the open rather than
1648 /// dropping silently out of the set.
1649 #[cfg(zstd_any)]
1650 pub(crate) fn install_own_zstd_dictionaries(&mut self) -> crate::Result<()> {
1651 let folder = self.path.join(crate::file::DICTS_FOLDER);
1652 // A `.tmp` is an unpublished registration referenced by no version, so a
1653 // crashed one is swept rather than left to linger.
1654 crate::dicts::sweep_temps(&*self.fs, &folder)?;
1655
1656 let dicts = crate::dicts::read_all(&*self.fs, &folder, self.encryption.as_deref())?;
1657 self.install_zstd_dictionary_set(dicts);
1658 Ok(())
1659 }
1660
1661 /// [`Self::install_own_zstd_dictionaries`] for a repair: a dictionary whose
1662 /// bytes no longer hash to its name is left out of the set instead of
1663 /// failing the load, and its id is returned. Nothing is changed on disk.
1664 ///
1665 /// # Errors
1666 ///
1667 /// Propagates the folder scan and every read failure other than the
1668 /// integrity check.
1669 #[cfg(zstd_any)]
1670 pub(crate) fn install_own_zstd_dictionaries_skipping_damaged(
1671 &mut self,
1672 ) -> crate::Result<alloc::vec::Vec<crate::file::DictId>> {
1673 let folder = self.path.join(crate::file::DICTS_FOLDER);
1674 crate::dicts::sweep_temps(&*self.fs, &folder)?;
1675
1676 let (dicts, damaged) = crate::dicts::read_all_skipping_damaged(
1677 &*self.fs,
1678 &folder,
1679 self.encryption.as_deref(),
1680 )?;
1681 self.install_zstd_dictionary_set(dicts);
1682 Ok(damaged)
1683 }
1684
1685 /// Joins the supplied dictionaries to `dicts` and installs it as this
1686 /// config's own registry.
1687 ///
1688 /// The supplied ones join the SAME set the tree's stored ones are in, and
1689 /// every read and write resolves out of it by id: a table or blob file by
1690 /// the id it recorded, a writer by the id its policy names.
1691 #[cfg(zstd_any)]
1692 fn install_zstd_dictionary_set(&mut self, mut dicts: crate::compression::ZstdDictionaries) {
1693 if let Some(supplied) = self.zstd_dictionary.clone() {
1694 dicts = dicts.with(supplied);
1695 }
1696 if let Some(supplied) = self
1697 .kv_separation_opts
1698 .as_ref()
1699 .and_then(|o| o.zstd_dictionary.clone())
1700 {
1701 dicts = dicts.with(supplied);
1702 }
1703 self.zstd_dictionaries = Arc::new(arc_swap::ArcSwap::from_pointee(dicts));
1704 }
1705
1706 /// Like [`Config::new`], but accepts pre-built shared generators.
1707 ///
1708 /// This is useful when the caller already has
1709 /// [`SharedSequenceNumberGenerator`] instances (e.g., from a higher-level
1710 /// database that shares generators across multiple trees).
1711 // std-only: see [`Config::new`] — seeds via `Config::default` (`StdFs`).
1712 #[cfg(feature = "std")]
1713 pub fn new_with_generators<P: AsRef<Path>>(
1714 path: P,
1715 seqno: SharedSequenceNumberGenerator,
1716 visible_seqno: SharedSequenceNumberGenerator,
1717 ) -> Self {
1718 Self {
1719 path: absolute_path(path.as_ref()),
1720 seqno,
1721 visible_seqno,
1722 ..Default::default()
1723 }
1724 }
1725}
1726
1727#[cfg(all(test, zstd_any))]
1728mod tests;
1729
1730impl Config {
1731 /// The [`level_routes`](Self::level_routes) entry covering `level`, if any.
1732 ///
1733 /// The single place the level→route lookup lives, so the path a level's
1734 /// tables are written to and the backend its reads are submitted to cannot
1735 /// disagree about which route covers it.
1736 fn route_for_level(&self, level: u8) -> Option<&LevelRoute> {
1737 self.level_routes
1738 .as_ref()?
1739 .iter()
1740 .find(|route| route.levels.contains(&level))
1741 }
1742
1743 /// Returns the tables folder path and [`Fs`] backend for the given level.
1744 ///
1745 /// If [`level_routes`](Self::level_routes) has an entry covering this
1746 /// level, uses that entry's path and `Fs`. Otherwise falls back to the
1747 /// primary [`path`](Self::path) and [`fs`](Self::fs).
1748 #[must_use]
1749 pub fn tables_folder_for_level(&self, level: u8) -> (PathBuf, Arc<dyn Fs>) {
1750 match self.route_for_level(level) {
1751 Some(route) => (route.path.join(TABLES_FOLDER), route.fs.clone()),
1752 None => (self.path.join(TABLES_FOLDER), self.fs.clone()),
1753 }
1754 }
1755
1756 /// Best-effort minimum free space (bytes) across every filesystem this tree
1757 /// writes to: the primary [`path`](Self::path) plus each
1758 /// [`level_routes`](Self::level_routes) volume.
1759 ///
1760 /// The tightest volume bounds storage admission and compaction space gating,
1761 /// since a full routed (cold-tier) volume fails a flush / compaction
1762 /// targeting it even while the primary still has room. A backend that cannot
1763 /// report free space (or an I/O hiccup) contributes `u64::MAX`, so a probe
1764 /// failure never fabricates disk pressure.
1765 #[must_use]
1766 pub(crate) fn min_available_space(&self) -> u64 {
1767 let mut free = self.fs.available_space(&self.path).unwrap_or(u64::MAX);
1768 if let Some(routes) = &self.level_routes {
1769 for route in routes {
1770 free = free.min(route.fs.available_space(&route.path).unwrap_or(u64::MAX));
1771 }
1772 }
1773 free
1774 }
1775
1776 /// Returns all unique tables folders that need to be scanned during
1777 /// recovery: the primary folder plus every [`LevelRoute`] folder.
1778 #[must_use]
1779 pub fn all_tables_folders(&self) -> Vec<(PathBuf, Arc<dyn Fs>)> {
1780 let primary_fs: Arc<dyn Fs> = self.fs.clone();
1781 let mut folders: Vec<(PathBuf, Arc<dyn Fs>)> =
1782 vec![(self.path.join(TABLES_FOLDER), primary_fs)];
1783
1784 if let Some(routes) = &self.level_routes {
1785 for route in routes {
1786 let folder = route.path.join(TABLES_FOLDER);
1787 // Dedup by path: scanning the same directory twice would cause
1788 // already-recovered tables to be classified as orphans and
1789 // deleted. Routing the same path through different Fs backends
1790 // is a configuration error (level_routes validation in
1791 // Config::level_routes rejects overlapping ranges).
1792 if !folders.iter().any(|(p, _)| *p == folder) {
1793 folders.push((folder, route.fs.clone()));
1794 }
1795 }
1796 }
1797
1798 folders
1799 }
1800
1801 /// Configures per-level filesystem routing for tiered storage.
1802 ///
1803 /// Each [`LevelRoute`] maps a range of LSM levels to a base directory
1804 /// and filesystem backend. Levels not covered by any route fall back to
1805 /// the primary `path` and `fs`.
1806 ///
1807 /// # Reopen contract
1808 ///
1809 /// The route configuration is **not persisted** in the manifest.
1810 /// On reopen, the [`Config`] must specify `level_routes` such that
1811 /// [`all_tables_folders`](Self::all_tables_folders) includes every
1812 /// directory and filesystem pair that may contain existing SST files
1813 /// for this tree.
1814 ///
1815 /// Changing the mapping from levels to paths is allowed as long as
1816 /// the previously used folders remain covered. If old folders are
1817 /// omitted, recovery may fail with
1818 /// [`RouteMismatch`](crate::Error::RouteMismatch) (when all missing
1819 /// tables are on uncovered levels) or
1820 /// [`Unrecoverable`](crate::Error::Unrecoverable) (when some missing
1821 /// tables are on levels that are still covered).
1822 ///
1823 /// # Panics
1824 ///
1825 /// Panics if any route has an empty range or if any two routes have
1826 /// overlapping level ranges.
1827 #[must_use]
1828 pub fn level_routes(mut self, routes: Vec<LevelRoute>) -> Self {
1829 // Validate no empty/inverted ranges
1830 for route in &routes {
1831 assert!(
1832 route.levels.start < route.levels.end,
1833 "empty or inverted level route range: {:?}",
1834 route.levels,
1835 );
1836 }
1837
1838 // Validate no overlapping ranges
1839 for (i, a) in routes.iter().enumerate() {
1840 for b in routes.iter().skip(i + 1) {
1841 assert!(
1842 a.levels.end <= b.levels.start || b.levels.end <= a.levels.start,
1843 "overlapping level routes: {:?} and {:?}",
1844 a.levels,
1845 b.levels,
1846 );
1847 }
1848 }
1849 self.level_routes = if routes.is_empty() {
1850 None
1851 } else {
1852 // Normalize paths the same way Config::new normalizes self.path
1853 Some(
1854 routes
1855 .into_iter()
1856 .map(|mut r| {
1857 r.path = absolute_path(&r.path);
1858 r
1859 })
1860 .collect(),
1861 )
1862 };
1863 self
1864 }
1865
1866 /// Overrides the sequence number generator.
1867 ///
1868 /// By default, [`SequenceNumberCounter`] is used. This allows plugging in
1869 /// a custom generator (e.g., HLC for distributed databases).
1870 #[must_use]
1871 pub fn seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
1872 self.seqno = generator;
1873 self
1874 }
1875
1876 /// Overrides the visible sequence number generator.
1877 #[must_use]
1878 pub fn visible_seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
1879 self.visible_seqno = generator;
1880 self
1881 }
1882
1883 /// Sets the global cache.
1884 ///
1885 /// You can create a global [`Cache`] and share it between multiple
1886 /// trees to cap global cache memory usage.
1887 ///
1888 /// Defaults to a cache with 16 MiB of capacity *per tree*.
1889 #[must_use]
1890 pub fn use_cache(mut self, cache: Arc<Cache>) -> Self {
1891 self.cache = cache;
1892 self
1893 }
1894
1895 /// Sets the file descriptor cache.
1896 ///
1897 /// Can be shared across trees.
1898 #[must_use]
1899 pub fn use_descriptor_table(mut self, descriptor_table: Option<Arc<DescriptorTable>>) -> Self {
1900 self.descriptor_table = descriptor_table;
1901 self
1902 }
1903
1904 /// If `true`, the last level will not build filters, reducing the filter size of a database
1905 /// by ~90% typically.
1906 ///
1907 /// **Enable this only if you know that point reads generally are expected to find a key-value pair.**
1908 #[must_use]
1909 pub fn expect_point_read_hits(mut self, b: bool) -> Self {
1910 self.expect_point_read_hits = b;
1911 self
1912 }
1913
1914 /// Whether a compaction ends its outputs on the boundaries between the
1915 /// tables of the level they are merged into next.
1916 ///
1917 /// An output straddling such a boundary drags both tables into that next
1918 /// merge, which reads and rewrites the extra one. Aligned, an output ends
1919 /// at the first boundary past half the target size; each boundary it
1920 /// passes without ending raises that share by five percent, up to ninety;
1921 /// it ends at twice the target whatever the boundaries, and at the target
1922 /// when no boundary is left ahead. A user key's versions stay in one output
1923 /// above every size bound. Off, outputs end at the target size alone.
1924 ///
1925 /// Default = `true`.
1926 ///
1927 /// # Examples
1928 ///
1929 /// ```
1930 /// # let folder = tempfile::tempdir()?;
1931 /// use lsm_tree::{Config, SequenceNumberCounter};
1932 ///
1933 /// let config = Config::new(
1934 /// folder,
1935 /// SequenceNumberCounter::default(),
1936 /// SequenceNumberCounter::default(),
1937 /// )
1938 /// .compaction_output_alignment(false);
1939 /// # let _ = config;
1940 /// # Ok::<(), Box<dyn std::error::Error>>(())
1941 /// ```
1942 #[must_use]
1943 pub fn compaction_output_alignment(mut self, enabled: bool) -> Self {
1944 self.compaction_output_cuts = if enabled {
1945 OutputCuts::Aligned
1946 } else {
1947 OutputCuts::TargetSize
1948 };
1949 self
1950 }
1951
1952 /// Enables per-block Page ECC.
1953 ///
1954 /// When enabled, every block written by this tree carries a parity
1955 /// trailer; on read, if the block's XXH3 disagrees with the on-disk
1956 /// bytes, the reader attempts recovery from the trailer before surfacing
1957 /// the corruption. The correction scheme defaults to per-word SEC-DED and
1958 /// is selectable at runtime (`update_runtime_config`): per-word SEC-DED,
1959 /// single XOR parity, or Reed-Solomon.
1960 ///
1961 /// Opening a tree with `page_ecc = true` on a build that does not
1962 /// have the `page_ecc` cargo feature enabled returns
1963 /// [`crate::Error::PageEccUnsupported`] at `Tree::open` — the
1964 /// reader has no way to honour the parity trailer without the
1965 /// codec, so silently downgrading integrity is not an option.
1966 ///
1967 /// Wired into the on-disk write path via `MultiWriter::use_page_ecc`
1968 /// at every `Tree::open` / `Tree::ingestion` / compaction-worker
1969 /// `MultiWriter` construction site. With this flag set, every
1970 /// `Block::write_into` call those writers make upgrades its
1971 /// `BlockTransform` to the matching `*Ecc` variant — emitting the
1972 /// configured scheme's parity trailer and setting the `ECC_PARITY` flag
1973 /// in each block header (the trailer length is derived from
1974 /// `data_length`, not stored).
1975 #[must_use]
1976 pub fn page_ecc(mut self, enabled: bool) -> Self {
1977 self.page_ecc = enabled;
1978 self
1979 }
1980
1981 /// Enables or disables the cross-process directory lock (default: enabled).
1982 ///
1983 /// When enabled, [`Config::open`] and [`Config::repair`] acquire an
1984 /// exclusive advisory lock on a `LOCK` file in the tree directory, so a
1985 /// second process opening / repairing the same directory fails fast with
1986 /// [`Error::Locked`](crate::Error::Locked) rather than corrupting the shared
1987 /// manifest. Disable ONLY when exclusive directory ownership is already
1988 /// guaranteed at a higher layer (e.g. an embedding keyspace / journal
1989 /// manager that opens each directory at most once per host).
1990 #[must_use]
1991 pub fn with_directory_lock(mut self, enabled: bool) -> Self {
1992 self.directory_lock = enabled;
1993 self
1994 }
1995
1996 /// Wires shared live-progress counters into the repair / salvage paths.
1997 ///
1998 /// A repair over a large store streams every SST and blob file and can run
1999 /// for a long time; the handle set here is ticked as files are discovered
2000 /// and blocks / rows are recovered, so another thread can poll
2001 /// [`RecoveryProgress::snapshot`](crate::RecoveryProgress::snapshot) while
2002 /// [`Config::repair`] (or a salvage it triggers) runs. Without it, repair
2003 /// publishes no progress (zero overhead).
2004 ///
2005 /// # Examples
2006 ///
2007 /// ```no_run
2008 /// use lsm_tree::{Config, RecoveryProgress, SequenceNumberCounter};
2009 /// use std::sync::Arc;
2010 ///
2011 /// let progress = Arc::new(RecoveryProgress::default());
2012 /// let config = Config::new(
2013 /// "my-tree",
2014 /// SequenceNumberCounter::default(),
2015 /// SequenceNumberCounter::default(),
2016 /// )
2017 /// .with_recovery_progress(progress.clone());
2018 /// // spawn the repair, then poll `progress.snapshot()` elsewhere
2019 /// let report = config.repair()?;
2020 /// # Ok::<_, lsm_tree::Error>(())
2021 /// ```
2022 #[cfg(feature = "std")]
2023 #[must_use]
2024 pub fn with_recovery_progress(mut self, progress: Arc<crate::RecoveryProgress>) -> Self {
2025 self.recovery_progress = Some(progress);
2026 self
2027 }
2028
2029 /// Sets the Page ECC scheme used when [`Self::page_ecc`] is enabled.
2030 ///
2031 /// ECC is off until `page_ecc(true)`. When on, this picks the
2032 /// algorithm:
2033 /// [`EccScheme::Secded`](crate::runtime_config::EccScheme::Secded)
2034 /// (per-word single-bit correct / double-bit detect, the default, supported
2035 /// at Block granularity),
2036 /// [`EccScheme::Xor`](crate::runtime_config::EccScheme::Xor) (RAID-5
2037 /// single-parity), or
2038 /// [`EccScheme::ReedSolomon`](crate::runtime_config::EccScheme::ReedSolomon).
2039 /// There is no implicit RS(4,2) default.
2040 #[must_use]
2041 pub fn ecc_scheme(mut self, scheme: crate::runtime_config::EccScheme) -> Self {
2042 self.initial_runtime_config.ecc_scheme = scheme;
2043 self
2044 }
2045
2046 /// Sets whether the writer clears per-file copy-on-write on newly created
2047 /// SST / blob files when the backing filesystem is copy-on-write (Btrfs).
2048 ///
2049 /// Default `true`: write-once SSTs gain no benefit from `CoW` but suffer a
2050 /// fragmentation penalty (~20% write throughput on Btrfs), so clearing it
2051 /// recovers the ext4-equivalent baseline. A no-op on non-`CoW` filesystems.
2052 /// Set `false` to preserve `CoW` (e.g. Btrfs subvolume snapshots that depend
2053 /// on it). See [`crate::runtime_config::RuntimeConfig::disable_cow_on_sst_files`].
2054 #[must_use]
2055 pub fn disable_cow_on_sst_files(mut self, enabled: bool) -> Self {
2056 self.initial_runtime_config.disable_cow_on_sst_files = enabled;
2057 self
2058 }
2059
2060 /// Sets whether [`crate::AbstractTree::create_checkpoint`] clones files via
2061 /// reflink (`FICLONE` / `clonefile`) when the filesystem supports it,
2062 /// falling back to a hard link otherwise.
2063 ///
2064 /// Default `true`: a reflinked checkpoint has an independent inode (no
2065 /// max-links constraint, modifications never touch the original) at O(1)
2066 /// cost via copy-on-write block sharing. A no-op (hard-link path) on
2067 /// filesystems without reflink. See
2068 /// [`crate::runtime_config::RuntimeConfig::use_reflink_for_checkpoint`].
2069 #[must_use]
2070 pub fn use_reflink_for_checkpoint(mut self, enabled: bool) -> Self {
2071 self.initial_runtime_config.use_reflink_for_checkpoint = enabled;
2072 self
2073 }
2074
2075 /// Sets the initial [`crate::runtime_config::RuntimeConfig`]
2076 /// snapshot the tree will start with.
2077 ///
2078 /// Seeds both the first manifest write and the live
2079 /// `RuntimeConfigHandle` exposed via
2080 /// [`crate::Tree::runtime_config`].
2081 ///
2082 /// Replaces the snapshot, including what the builders that seed it set
2083 /// earlier ([`Self::ecc_scheme`] and the other `RuntimeConfig`-backed
2084 /// builders), EXCEPT the three compression policies: those keep what
2085 /// [`Self::data_block_compression_policy`],
2086 /// [`Self::index_block_compression_policy`] and [`Self::blob_compression`]
2087 /// set, in whichever order they are called, and the policies in `runtime`
2088 /// are ignored. A caller that set its compression through the builders and
2089 /// its runtime toggles through this does not lose the former to the
2090 /// latter's defaults. On a live tree, all three change through
2091 /// [`crate::Tree::update_runtime_config`].
2092 ///
2093 /// **Manifest-hardening toggles** in the supplied snapshot
2094 /// that are currently wired through the writer
2095 /// (`manifest_footer_mirror`, `page_ecc` *as consumed by
2096 /// `manifest_blocks::writer` when picking the `BlockTransform`
2097 /// variant*) take effect from byte zero of the on-disk
2098 /// manifest rather than waiting for a post-open
2099 /// [`crate::Tree::update_runtime_config`] call. Subsequent
2100 /// updates still flow through the live handle and apply to
2101 /// the next manifest write.
2102 ///
2103 /// `manifest_kv_checksums` is plumbed in the snapshot but the
2104 /// writer does NOT yet consult or persist it (per-entry
2105 /// framing + footer-flag slot land in a follow-up). Setting
2106 /// it here today has no on-disk effect; it is exposed for
2107 /// forward-compat with no behaviour break.
2108 ///
2109 /// **Note on data-block ECC:** `RuntimeConfig::page_ecc`
2110 /// currently affects manifest Blocks only — data-block ECC is
2111 /// still gated by [`Config::page_ecc`] at tree-open time. The
2112 /// SST writer path consumes the tree-static config, not the
2113 /// runtime handle. Wiring through SST emission is a follow-up.
2114 #[must_use]
2115 pub fn with_runtime_config(mut self, runtime: crate::runtime_config::RuntimeConfig) -> Self {
2116 let kept = core::mem::replace(&mut self.initial_runtime_config, runtime);
2117 self.initial_runtime_config.data_block_compression_policy =
2118 kept.data_block_compression_policy;
2119 self.initial_runtime_config.index_block_compression_policy =
2120 kept.index_block_compression_policy;
2121 self.initial_runtime_config.blob_compression = kept.blob_compression;
2122 self
2123 }
2124
2125 /// Sets the partitioning policy for filter blocks.
2126 #[must_use]
2127 pub fn filter_block_partitioning_policy(mut self, policy: PinningPolicy) -> Self {
2128 self.filter_block_partitioning_policy = policy;
2129 self
2130 }
2131
2132 /// Sets the partitioning policy for index blocks.
2133 #[must_use]
2134 pub fn index_block_partitioning_policy(mut self, policy: PinningPolicy) -> Self {
2135 self.index_block_partitioning_policy = policy;
2136 self
2137 }
2138
2139 /// Sets the pinning policy for filter blocks.
2140 #[must_use]
2141 pub fn filter_block_pinning_policy(mut self, policy: PinningPolicy) -> Self {
2142 self.filter_block_pinning_policy = policy;
2143 self
2144 }
2145
2146 /// Sets the pinning policy for index blocks.
2147 #[must_use]
2148 pub fn index_block_pinning_policy(mut self, policy: PinningPolicy) -> Self {
2149 self.index_block_pinning_policy = policy;
2150 self
2151 }
2152
2153 /// Sets the restart interval inside data blocks.
2154 ///
2155 /// A higher restart interval saves space while increasing lookup times
2156 /// inside data blocks.
2157 ///
2158 /// Default = 16
2159 ///
2160 /// # Panics
2161 ///
2162 /// Panics if any restart interval in `policy` is zero.
2163 #[must_use]
2164 pub fn data_block_restart_interval_policy(mut self, policy: RestartIntervalPolicy) -> Self {
2165 assert!(
2166 policy.iter().all(|interval| *interval > 0),
2167 "data block restart interval must be greater than zero",
2168 );
2169 self.data_block_restart_interval_policy = policy;
2170 self
2171 }
2172
2173 /// Sets the restart interval inside index blocks.
2174 ///
2175 /// A higher restart interval saves space while increasing lookup times
2176 /// inside index blocks.
2177 ///
2178 /// Default = 1
2179 ///
2180 /// # Panics
2181 ///
2182 /// Panics if any restart interval in `policy` is zero.
2183 #[must_use]
2184 pub fn index_block_restart_interval_policy(mut self, policy: RestartIntervalPolicy) -> Self {
2185 assert!(
2186 policy.iter().all(|interval| *interval > 0),
2187 "index block restart interval must be greater than zero",
2188 );
2189 self.index_block_restart_interval_policy = policy;
2190 self
2191 }
2192
2193 /// Sets the filter construction policy.
2194 #[must_use]
2195 pub fn filter_policy(mut self, policy: FilterPolicy) -> Self {
2196 self.filter_policy = policy;
2197 self
2198 }
2199
2200 /// Allocates filter memory by measured negative-probe load; see
2201 /// [`FilterAdvisor`]. `None` (the default) leaves every filter to the
2202 /// [`FilterPolicy`] and counts nothing on the read path, so the tables it
2203 /// writes are the ones a tree without the advisor writes.
2204 ///
2205 /// With an advisor, each flush and compaction chooses the width of the
2206 /// filters it writes; [`AbstractTree::filter_memory`](crate::AbstractTree::filter_memory)
2207 /// reports the serialised and resident filter bytes against the budget.
2208 ///
2209 /// # Examples
2210 ///
2211 /// ```
2212 /// use lsm_tree::{Config, config::FilterAdvisor};
2213 ///
2214 /// # let folder = tempfile::tempdir()?;
2215 /// // 64 MiB of filters, widths from 8 to 16 bits per key.
2216 /// let advisor = FilterAdvisor::new(64 << 20).with_bits_per_key([8, 10, 12, 14, 16]);
2217 /// let tree = Config::new(&folder, Default::default(), Default::default())
2218 /// .filter_advisor(Some(advisor))
2219 /// .open()?;
2220 /// # Ok::<(), lsm_tree::Error>(())
2221 /// ```
2222 #[must_use]
2223 pub fn filter_advisor(mut self, advisor: Option<FilterAdvisor>) -> Self {
2224 self.filter_advisor = advisor;
2225 self
2226 }
2227
2228 /// Sets the retrieval-ribbon locator policy.
2229 ///
2230 /// On by default at [`LocatorPrecision::Block`] (see
2231 /// [`LocatorPolicy::block_level`]). When enabled for a level, written SSTs on
2232 /// that level carry an optional `locator` section mapping each key to its
2233 /// data block (and, at finer precisions, its slot), letting point reads skip
2234 /// the index-block binary search. Set [`LocatorPolicy::disabled`] to opt out;
2235 /// disabled levels emit byte-identical SSTs.
2236 #[must_use]
2237 pub fn locator_policy(mut self, policy: LocatorPolicy) -> Self {
2238 self.locator_policy = policy;
2239 self
2240 }
2241
2242 /// Sets the compression the tree starts with for data blocks.
2243 ///
2244 /// The initial value of
2245 /// [`RuntimeConfig::data_block_compression_policy`](crate::runtime_config::RuntimeConfig::data_block_compression_policy),
2246 /// which a live tree changes with [`crate::Tree::update_runtime_config`].
2247 #[must_use]
2248 pub fn data_block_compression_policy(mut self, policy: CompressionPolicy) -> Self {
2249 self.initial_runtime_config.data_block_compression_policy = policy;
2250 self
2251 }
2252
2253 /// Sets the compression the tree starts with for index blocks.
2254 ///
2255 /// The initial value of
2256 /// [`RuntimeConfig::index_block_compression_policy`](crate::runtime_config::RuntimeConfig::index_block_compression_policy),
2257 /// which a live tree changes with [`crate::Tree::update_runtime_config`].
2258 #[must_use]
2259 pub fn index_block_compression_policy(mut self, policy: CompressionPolicy) -> Self {
2260 self.initial_runtime_config.index_block_compression_policy = policy;
2261 self
2262 }
2263
2264 /// Sets the compression a KV-separated tree starts with for blob files.
2265 ///
2266 /// The initial value of
2267 /// [`RuntimeConfig::blob_compression`](crate::runtime_config::RuntimeConfig::blob_compression),
2268 /// which a live tree changes with [`crate::BlobTree::update_runtime_config`].
2269 /// A standard tree ignores it.
2270 #[must_use]
2271 pub fn blob_compression(mut self, compression: CompressionType) -> Self {
2272 self.initial_runtime_config.blob_compression = compression;
2273 self
2274 }
2275
2276 /// Sets whether a KV-separated tree starts caching the values it reads
2277 /// from blob files. It does not by default.
2278 ///
2279 /// The initial value of
2280 /// [`RuntimeConfig::blob_cache_fill`](crate::runtime_config::RuntimeConfig::blob_cache_fill),
2281 /// which a live tree changes with [`crate::BlobTree::update_runtime_config`].
2282 /// A standard tree ignores it.
2283 ///
2284 /// # Examples
2285 ///
2286 /// ```
2287 /// use lsm_tree::{Config, KvSeparationOptions, SequenceNumberCounter, config::BlobCacheFill};
2288 ///
2289 /// # let folder = tempfile::tempdir()?;
2290 /// let config = Config::new(&folder, SequenceNumberCounter::default(), SequenceNumberCounter::default())
2291 /// .with_kv_separation(Some(KvSeparationOptions::default()))
2292 /// .blob_cache_fill(BlobCacheFill::Always);
2293 /// # Ok::<(), Box<dyn std::error::Error>>(())
2294 /// ```
2295 #[must_use]
2296 pub fn blob_cache_fill(mut self, fill: BlobCacheFill) -> Self {
2297 self.initial_runtime_config.blob_cache_fill = fill;
2298 self
2299 }
2300
2301 // TODO: level count is fixed to 7 right now
2302 // /// Sets the number of levels of the LSM tree (depth of tree).
2303 // ///
2304 // /// Defaults to 7, like `LevelDB` and `RocksDB`.
2305 // ///
2306 // /// Cannot be changed once set.
2307 // ///
2308 // /// # Panics
2309 // ///
2310 // /// Panics if `n` is 0.
2311 // #[must_use]
2312 // pub fn level_count(mut self, n: u8) -> Self {
2313 // assert!(n > 0);
2314
2315 // self.level_count = n;
2316 // self
2317 // }
2318
2319 /// Sets the data block size policy.
2320 #[must_use]
2321 pub fn data_block_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2322 self.data_block_size_policy = policy;
2323 self
2324 }
2325
2326 /// Sets the size a partitioned block index cuts its partitions at, per
2327 /// level. Applies where [`Self::index_block_partitioning_policy`]
2328 /// partitions the index. The default is 4 KiB at every level.
2329 ///
2330 /// # Examples
2331 ///
2332 /// ```
2333 /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2334 /// # let folder = tempfile::tempdir()?;
2335 /// let config = Config::new(
2336 /// folder.path(),
2337 /// SequenceNumberCounter::default(),
2338 /// SequenceNumberCounter::default(),
2339 /// )
2340 /// .index_block_partition_size_policy(BlockSizePolicy::all(16 * 1_024));
2341 /// # Ok::<(), Box<dyn std::error::Error>>(())
2342 /// ```
2343 #[must_use]
2344 pub fn index_block_partition_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2345 self.index_block_partition_size_policy = policy;
2346 self
2347 }
2348
2349 /// Sets the size a partitioned filter cuts its partitions at, per level,
2350 /// in the bytes a partition takes on disk. Applies where
2351 /// [`Self::filter_block_partitioning_policy`] partitions the filter. A
2352 /// point read that misses the block cache loads one partition, so a
2353 /// smaller partition costs less per read and more top-level index
2354 /// entries. The default is 4 KiB at every level.
2355 ///
2356 /// # Examples
2357 ///
2358 /// ```
2359 /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2360 /// # let folder = tempfile::tempdir()?;
2361 /// let config = Config::new(
2362 /// folder.path(),
2363 /// SequenceNumberCounter::default(),
2364 /// SequenceNumberCounter::default(),
2365 /// )
2366 /// .filter_block_partition_size_policy(BlockSizePolicy::all(16 * 1_024));
2367 /// # Ok::<(), Box<dyn std::error::Error>>(())
2368 /// ```
2369 #[must_use]
2370 pub fn filter_block_partition_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2371 self.filter_block_partition_size_policy = policy;
2372 self
2373 }
2374
2375 /// Sets the size a columnar table's row groups are cut at, per level.
2376 ///
2377 /// Applies to the tables written while the runtime `columnar` switch is
2378 /// on; a row-major table cuts its blocks at
2379 /// [`Self::data_block_size_policy`]. The default is
2380 /// [`DEFAULT_COLUMNAR_ROW_GROUP_SIZE`] at every level.
2381 ///
2382 /// # Examples
2383 ///
2384 /// ```
2385 /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2386 /// # let folder = tempfile::tempdir()?;
2387 /// let config = Config::new(
2388 /// folder.path(),
2389 /// SequenceNumberCounter::default(),
2390 /// SequenceNumberCounter::default(),
2391 /// )
2392 /// .columnar_row_group_size_policy(BlockSizePolicy::all(256 * 1_024));
2393 /// # Ok::<(), Box<dyn std::error::Error>>(())
2394 /// ```
2395 #[must_use]
2396 pub fn columnar_row_group_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2397 self.columnar_row_group_size_policy = policy;
2398 self
2399 }
2400
2401 /// Sets the size a columnar row group's rows are cut into row pages at,
2402 /// per level.
2403 ///
2404 /// A row page closes once its rows reach this many uncompressed bytes, so
2405 /// a page holds at least one row and a group at least one page. A size at
2406 /// or above the row group size writes one row page per group the writer
2407 /// closes at its size; a group an ingested batch took past it is still cut
2408 /// into pages of this size. The default is [`DEFAULT_COLUMNAR_PAGE_SIZE`]
2409 /// at every level, four row pages to a group of the default size.
2410 ///
2411 /// # Examples
2412 ///
2413 /// ```
2414 /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2415 /// # let folder = tempfile::tempdir()?;
2416 /// let config = Config::new(
2417 /// folder.path(),
2418 /// SequenceNumberCounter::default(),
2419 /// SequenceNumberCounter::default(),
2420 /// )
2421 /// .columnar_row_group_size_policy(BlockSizePolicy::all(256 * 1_024))
2422 /// .columnar_page_size_policy(BlockSizePolicy::all(8 * 1_024));
2423 /// # Ok::<(), Box<dyn std::error::Error>>(())
2424 /// ```
2425 #[must_use]
2426 pub fn columnar_page_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2427 self.columnar_page_size_policy = policy;
2428 self
2429 }
2430
2431 /// Sets how a columnar table's column pages store their values, per
2432 /// level.
2433 ///
2434 /// The default is [`ColumnEncoding::Plain`] at every level: a read serves
2435 /// a plain page as a view of itself, while an encoded one is built back
2436 /// into its layout by each read that wants the column whole. Choose
2437 /// [`ColumnEncoding::Auto`] for the levels whose tables are mostly stored
2438 /// rather than read, as a stronger compression is chosen for them. A table
2439 /// reads the same under either; only what its pages hold differs.
2440 ///
2441 /// # Examples
2442 ///
2443 /// ```
2444 /// use lsm_tree::{
2445 /// Config, SequenceNumberCounter,
2446 /// config::{ColumnEncoding, ColumnEncodingPolicy},
2447 /// };
2448 /// # let folder = tempfile::tempdir()?;
2449 /// // Plain in the first three levels, encoded below them.
2450 /// let config = Config::new(
2451 /// folder.path(),
2452 /// SequenceNumberCounter::default(),
2453 /// SequenceNumberCounter::default(),
2454 /// )
2455 /// .column_encoding_policy(ColumnEncodingPolicy::new([
2456 /// ColumnEncoding::Plain,
2457 /// ColumnEncoding::Plain,
2458 /// ColumnEncoding::Plain,
2459 /// ColumnEncoding::Auto,
2460 /// ]));
2461 /// # Ok::<(), Box<dyn std::error::Error>>(())
2462 /// ```
2463 #[must_use]
2464 pub fn column_encoding_policy(mut self, policy: ColumnEncodingPolicy) -> Self {
2465 self.column_encoding_policy = policy;
2466 self
2467 }
2468
2469 /// Sets how columnar reads of this tree fetch their pages: the bytes one
2470 /// request may ask for and the requests kept in flight at once. A reader
2471 /// setting, not a format one: a tree reopened under another budget reads
2472 /// the same tables and returns the same rows. The default is
2473 /// [`ReadBudget::default`].
2474 ///
2475 /// # Examples
2476 ///
2477 /// ```
2478 /// use lsm_tree::{Config, SequenceNumberCounter, config::ReadBudget};
2479 /// # let folder = tempfile::tempdir()?;
2480 /// let config = Config::new(
2481 /// folder.path(),
2482 /// SequenceNumberCounter::default(),
2483 /// SequenceNumberCounter::default(),
2484 /// )
2485 /// .columnar_read_budget(ReadBudget::new(64 * 1_024, 4));
2486 /// # Ok::<(), Box<dyn std::error::Error>>(())
2487 /// ```
2488 #[must_use]
2489 pub fn columnar_read_budget(mut self, budget: ReadBudget) -> Self {
2490 self.columnar_read_budget = budget;
2491 self
2492 }
2493
2494 /// Sets the page bytes a projected columnar scan of this tree holds at
2495 /// once. A scan over overlapping segments shares it between them, so a
2496 /// scan that merges more of them reads each in smaller runs of row pages
2497 /// rather than holding proportionally more. A row page is the least a
2498 /// read takes, so one larger than a segment's share is read on its own
2499 /// and counted, not refused; so is a row group a pushed-down predicate
2500 /// reads whole to select its row pages. A reader setting: the rows returned are the
2501 /// same under any budget. The default is [`DEFAULT_COLUMNAR_SCAN_BUDGET`].
2502 ///
2503 /// # Examples
2504 ///
2505 /// ```
2506 /// use lsm_tree::{Config, SequenceNumberCounter};
2507 /// # let folder = tempfile::tempdir()?;
2508 /// let config = Config::new(
2509 /// folder.path(),
2510 /// SequenceNumberCounter::default(),
2511 /// SequenceNumberCounter::default(),
2512 /// )
2513 /// .columnar_scan_budget(256 * 1_024);
2514 /// # Ok::<(), Box<dyn std::error::Error>>(())
2515 /// ```
2516 #[must_use]
2517 pub fn columnar_scan_budget(mut self, bytes: u64) -> Self {
2518 self.columnar_scan_budget = bytes;
2519 self
2520 }
2521
2522 /// Sets the filter and index bytes a multi-get, blocking or resumable,
2523 /// holds at once, in flight or read, across the tables of a level it
2524 /// reads stage by stage. A block
2525 /// in flight counts at the size it is read as, a block read at its decoded
2526 /// size. A table whose blocks would pass it waits until a table before it
2527 /// is planned and lets its blocks go; one that alone passes it is read
2528 /// when no other is in flight. A stage is let in at its on-disk size, so
2529 /// with compressed index blocks the read can go past the budget by what
2530 /// the stages let in together grow when decoded; no further stage is let
2531 /// in until it is back under. Independent of the block cache, so a small
2532 /// cache does not make the tables of a level wait for each other. A reader
2533 /// setting: the values returned are the same under any budget. The
2534 /// default is [`DEFAULT_MULTI_GET_METADATA_BUDGET`].
2535 ///
2536 /// # Examples
2537 ///
2538 /// ```
2539 /// use lsm_tree::{Config, SequenceNumberCounter};
2540 /// # let folder = tempfile::tempdir()?;
2541 /// let config = Config::new(
2542 /// folder.path(),
2543 /// SequenceNumberCounter::default(),
2544 /// SequenceNumberCounter::default(),
2545 /// )
2546 /// .multi_get_metadata_budget(8 * 1_024 * 1_024);
2547 /// # Ok::<(), Box<dyn std::error::Error>>(())
2548 /// ```
2549 #[must_use]
2550 pub fn multi_get_metadata_budget(mut self, bytes: u64) -> Self {
2551 self.multi_get_metadata_budget = bytes;
2552 self
2553 }
2554
2555 /// Sets the hash ratio policy for data blocks.
2556 ///
2557 /// If greater than 0.0, a hash index is embedded into data blocks that can speed up reads
2558 /// inside the data block.
2559 #[must_use]
2560 pub fn data_block_hash_ratio_policy(mut self, policy: HashRatioPolicy) -> Self {
2561 self.data_block_hash_ratio_policy = policy;
2562 self
2563 }
2564
2565 /// Toggles key-value separation.
2566 #[must_use]
2567 pub fn with_kv_separation(mut self, opts: Option<KvSeparationOptions>) -> Self {
2568 self.kv_separation_opts = opts;
2569 self
2570 }
2571
2572 /// Installs a custom compaction filter.
2573 #[must_use]
2574 pub fn with_compaction_filter_factory(mut self, factory: Option<Arc<dyn Factory>>) -> Self {
2575 self.compaction_filter_factory = factory;
2576 self
2577 }
2578
2579 /// Sets the prefix extractor for prefix bloom filters.
2580 ///
2581 /// When configured, bloom filters will index key prefixes returned by
2582 /// the extractor. Prefix scans can then skip segments whose bloom
2583 /// filter reports no match for the scan prefix.
2584 ///
2585 /// The extractor may be added or replaced on reopen. A table records the
2586 /// [`name`](PrefixExtractor::name) of the extractor its filter indexes,
2587 /// and a prefix scan skips by a table's filter only when that is the
2588 /// configured extractor: tables written without it, or under another,
2589 /// are read instead, until compaction rewrites them.
2590 #[must_use]
2591 pub fn prefix_extractor(mut self, extractor: Arc<dyn PrefixExtractor>) -> Self {
2592 self.prefix_extractor = Some(extractor);
2593 self
2594 }
2595
2596 /// Ends every table that flush, ingest and compaction write before the
2597 /// first key of the next of `domains`, even below the target size, so no
2598 /// new table holds keys of two domains.
2599 ///
2600 /// The policy may be added or replaced on reopen: tables written before it
2601 /// keep their keys until compaction rewrites them.
2602 #[must_use]
2603 pub fn key_domains(mut self, domains: Arc<dyn crate::KeyDomains>) -> Self {
2604 self.key_domains = Some(domains);
2605 self
2606 }
2607
2608 /// Installs a merge operator for commutative operations.
2609 ///
2610 /// When set, enables [`crate::AbstractTree::merge`] which stores partial updates
2611 /// (operands) that are lazily combined during reads and compaction.
2612 #[must_use]
2613 pub fn with_merge_operator(mut self, op: Option<Arc<dyn MergeOperator>>) -> Self {
2614 self.merge_operator = op;
2615 self
2616 }
2617
2618 /// Sets a custom user key comparator.
2619 ///
2620 /// When configured, all key ordering (memtable, block index, merge,
2621 /// range scans) uses this comparator instead of the default lexicographic
2622 /// byte ordering.
2623 ///
2624 /// # Important
2625 ///
2626 /// The comparator's [`crate::UserComparator::name`] is persisted when a tree is
2627 /// first created. On subsequent opens the stored name is compared against
2628 /// the supplied comparator's name — a mismatch causes the open to fail
2629 /// with [`Error::ComparatorMismatch`](crate::Error::ComparatorMismatch).
2630 #[must_use]
2631 pub fn comparator(mut self, comparator: SharedComparator) -> Self {
2632 self.comparator = comparator;
2633 self
2634 }
2635
2636 /// Sets the block-level encryption provider for encryption at rest.
2637 ///
2638 /// When set, all blocks written to SST files are encrypted after
2639 /// compression and before checksumming, using the provided
2640 /// [`EncryptionProvider`]. The compression dictionaries the tree stores
2641 /// are sealed with it too, since a dictionary keeps literal stretches of
2642 /// the records it was trained on.
2643 ///
2644 /// The caller is responsible for key management and rotation.
2645 /// See `crate::Aes256GcmProvider` (behind the `encryption` feature)
2646 /// for a ready-to-use AES-256-GCM implementation.
2647 ///
2648 /// **Important constraints:**
2649 /// - Encryption state is NOT recorded in SST metadata. Opening an
2650 /// encrypted tree without the correct provider (or vice versa) will
2651 /// cause block validation errors, not silent corruption.
2652 /// - Blob files (KV-separated large values) are NOT covered by
2653 /// block-level encryption. Large values stored via KV separation
2654 /// remain in plaintext on disk.
2655 #[must_use]
2656 pub fn with_encryption(mut self, encryption: Option<Arc<dyn EncryptionProvider>>) -> Self {
2657 self.encryption = encryption;
2658 self
2659 }
2660
2661 /// Sets the wall clock this tree reads: the creation time stamped on its
2662 /// tables and blob files, those a repair salvages included, and the time
2663 /// FIFO compaction measures TTL against.
2664 ///
2665 /// Without one the tree reads the process clock: [`SystemClock`](crate::SystemClock)
2666 /// under `std`, or the clock registered with `set_clock` under `no_std`.
2667 ///
2668 /// A fixed clock makes the files a tree writes reproducible: two trees
2669 /// given the same fixed clock, the same writes in the same order and the
2670 /// same configuration write byte-identical tables and blob files, wherever
2671 /// their folders are. Encryption breaks that, since every sealed block
2672 /// takes a fresh nonce.
2673 ///
2674 /// # Examples
2675 ///
2676 /// ```
2677 /// # use lsm_tree::{Clock, Config, SequenceNumberCounter};
2678 /// # use std::{sync::Arc, time::Duration};
2679 /// struct Epoch;
2680 /// impl Clock for Epoch {
2681 /// fn unix_time(&self) -> Duration {
2682 /// Duration::from_secs(1_700_000_000)
2683 /// }
2684 /// }
2685 /// # let folder = tempfile::tempdir()?;
2686 /// let config = Config::new(
2687 /// folder.path(),
2688 /// SequenceNumberCounter::default(),
2689 /// SequenceNumberCounter::default(),
2690 /// )
2691 /// .with_clock(Arc::new(Epoch));
2692 /// # Ok::<(), Box<dyn std::error::Error>>(())
2693 /// ```
2694 #[must_use]
2695 pub fn with_clock(mut self, clock: Arc<dyn crate::Clock>) -> Self {
2696 self.clock = Some(clock);
2697 self
2698 }
2699
2700 /// Sets what `Tree::open` does with a torn tail of the manifest edit log.
2701 ///
2702 /// The only difference between the modes is an edit-log record the writer
2703 /// never finished appending (power loss mid-append): the default
2704 /// ([`ManifestRecoveryMode::AbsoluteConsistency`]) fails the open,
2705 /// [`ManifestRecoveryMode::TolerateCorruptedTailRecords`] drops it and
2706 /// opens at the last complete edit. That record was never acknowledged, so
2707 /// dropping it loses no committed state. Damage to anything committed (a
2708 /// snapshot section, a fully written edit) fails the open in either mode;
2709 /// [`Config::repair`] rebuilds the manifest from the tables on disk.
2710 ///
2711 /// See the [`ManifestRecoveryMode`] doc for per-variant semantics.
2712 #[must_use]
2713 pub fn manifest_recovery_mode(mut self, mode: ManifestRecoveryMode) -> Self {
2714 self.manifest_recovery_mode = mode;
2715 self
2716 }
2717
2718 /// Sets the durability level for every fsync the tree issues.
2719 ///
2720 /// Defaults to [`SyncMode::Normal`] (plain `fsync`, matching `RocksDB` /
2721 /// `SQLite` defaults). Pass [`SyncMode::Full`] to force `F_FULLFSYNC` on
2722 /// macOS for power-loss durability without an external journal — at a
2723 /// large per-flush cost. Pass [`SyncMode::Barrier`] to keep writes in
2724 /// order at a barrier's cost and make them durable with
2725 /// [`AbstractTree::sync_devices`](crate::AbstractTree::sync_devices), once
2726 /// per batch of flushes. On non-macOS platforms all three are plain
2727 /// `fsync`.
2728 #[must_use]
2729 pub fn sync_mode(mut self, mode: SyncMode) -> Self {
2730 self.sync_mode = mode;
2731 self
2732 }
2733
2734 /// Sets when the tables a flush writes become durable.
2735 ///
2736 /// Defaults to [`FlushDurability::Synced`]: a flush that returns `Ok` is
2737 /// on disk. [`FlushDurability::Deferred`] leaves a flush unsynced until
2738 /// [`AbstractTree::persist_flushes`](crate::AbstractTree::persist_flushes),
2739 /// for a caller whose own write-ahead log already makes its writes
2740 /// durable.
2741 ///
2742 /// # Examples
2743 ///
2744 /// ```
2745 /// # let folder = tempfile::tempdir()?;
2746 /// use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, config::FlushDurability};
2747 ///
2748 /// let tree = Config::new(
2749 /// folder.path(),
2750 /// SequenceNumberCounter::default(),
2751 /// SequenceNumberCounter::default(),
2752 /// )
2753 /// .flush_durability(FlushDurability::Deferred)
2754 /// .open()?;
2755 /// tree.insert("a", "1", 0);
2756 /// // Written and readable, not yet durable: the caller's log still holds it.
2757 /// tree.flush_active_memtable(0)?;
2758 /// // Durable from here: the caller may drop its log up to this point.
2759 /// tree.persist_flushes()?;
2760 /// # Ok::<(), lsm_tree::Error>(())
2761 /// ```
2762 #[must_use]
2763 pub fn flush_durability(mut self, durability: FlushDurability) -> Self {
2764 self.flush_durability = durability;
2765 self
2766 }
2767
2768 /// Sets how many bytes a table or blob file gathers before its writer
2769 /// starts writing them back, without waiting (default 1 MiB; `0` starts
2770 /// none).
2771 ///
2772 /// The final sync of a large file otherwise writes all its dirty pages at
2773 /// once, and on a device shared with the caller's journal that burst
2774 /// delays the journal's own syncs. Writeback makes nothing durable; the
2775 /// final sync still does. Only Linux starts one
2776 /// (`sync_file_range(SYNC_FILE_RANGE_WRITE)`); elsewhere this is unused.
2777 ///
2778 /// # Examples
2779 ///
2780 /// ```
2781 /// use lsm_tree::{Config, SequenceNumberCounter};
2782 ///
2783 /// let folder = tempfile::tempdir()?;
2784 /// let tree = Config::new(
2785 /// &folder,
2786 /// SequenceNumberCounter::default(),
2787 /// SequenceNumberCounter::default(),
2788 /// )
2789 /// .writeback_bytes(4 * 1_024 * 1_024)
2790 /// .open()?;
2791 /// # Ok::<(), lsm_tree::Error>(())
2792 /// ```
2793 #[must_use]
2794 pub fn writeback_bytes(mut self, bytes: u64) -> Self {
2795 self.writeback_bytes = bytes;
2796 self
2797 }
2798
2799 /// Sets the size a flush or an ingestion cuts its output tables at
2800 /// (default [`DEFAULT_TABLE_TARGET_SIZE`], 64 MiB).
2801 ///
2802 /// A memtable larger than this flushes into several tables of one run;
2803 /// an ingestion cuts its stream the same way, at whatever level it writes
2804 /// to. Compaction output is sized by its strategy, not by this value.
2805 ///
2806 /// The leveled L0 trigger counts runs, so the cut does not change how
2807 /// often L0 merges into L1; it bounds how much per-key state a flush's
2808 /// writer holds. A flush of fewer, larger tables writes less in the
2809 /// merges that follow, so with a memtable much larger than the default a
2810 /// matching target is the cheaper shape, as far as the writer's memory
2811 /// allows.
2812 ///
2813 /// # Examples
2814 ///
2815 /// ```
2816 /// use lsm_tree::{Config, SequenceNumberCounter};
2817 ///
2818 /// let folder = tempfile::tempdir()?;
2819 /// let tree = Config::new(
2820 /// &folder,
2821 /// SequenceNumberCounter::default(),
2822 /// SequenceNumberCounter::default(),
2823 /// )
2824 /// .table_target_size(8 * 1_024 * 1_024)
2825 /// .open()?;
2826 /// # Ok::<(), lsm_tree::Error>(())
2827 /// ```
2828 #[must_use]
2829 pub fn table_target_size(mut self, bytes: u64) -> Self {
2830 self.table_target_size = bytes;
2831 self
2832 }
2833
2834 /// Sets the edit-log rotation threshold in bytes (default 1 MiB).
2835 ///
2836 /// Once the manifest edit log exceeds this size, the next version upgrade
2837 /// writes a fresh full snapshot and starts an empty log instead of appending
2838 /// another edit. Lower it to shorten recovery replay and cap log size at the
2839 /// cost of more frequent full-snapshot writes; `0` rotates on every upgrade.
2840 #[must_use]
2841 pub fn manifest_log_rotate_bytes(mut self, bytes: u64) -> Self {
2842 self.manifest_log_rotate_bytes = bytes;
2843 self
2844 }
2845
2846 /// Sets how many empty table files each tables folder creates at a time,
2847 /// ahead of the tables written into them (default 16).
2848 ///
2849 /// A new table's name has to be durable before the manifest edit that
2850 /// names it, which costs a sync of its folder. A pooled file's name was
2851 /// made durable with its batch, one folder sync for the whole batch, so a
2852 /// flush or a compaction into one syncs its tables and the manifest edit
2853 /// only. The next batch is made off the writing thread once a claim
2854 /// leaves half a batch or less. An unused pooled file is empty and takes
2855 /// no space; the next open
2856 /// uses it again. `0` creates each table file as its table is written and
2857 /// syncs the folder at every install that names one.
2858 ///
2859 /// # Examples
2860 ///
2861 /// ```
2862 /// use lsm_tree::{Config, SequenceNumberCounter};
2863 ///
2864 /// let folder = tempfile::tempdir()?;
2865 /// let tree = Config::new(
2866 /// &folder,
2867 /// SequenceNumberCounter::default(),
2868 /// SequenceNumberCounter::default(),
2869 /// )
2870 /// .table_file_pool(64)
2871 /// .open()?;
2872 /// # Ok::<(), lsm_tree::Error>(())
2873 /// ```
2874 #[must_use]
2875 pub fn table_file_pool(mut self, files: usize) -> Self {
2876 self.table_file_pool = files;
2877 self
2878 }
2879
2880 /// Refills a table file pool on the thread of the write that claims from
2881 /// it, rather than off it (default `false`). A write then waits on the
2882 /// batch it starts, and every sync the pool issues falls inside one: a
2883 /// test that counts an operation's syncs, or fails one of them, sees the
2884 /// same ones on every run.
2885 #[doc(hidden)]
2886 #[must_use]
2887 pub fn table_file_refill_inline(mut self, inline: bool) -> Self {
2888 use crate::table::file_pool::RefillOn;
2889 self.table_file_refill = if inline {
2890 RefillOn::ClaimingThread
2891 } else {
2892 RefillOn::Background
2893 };
2894 self
2895 }
2896
2897 /// Sets the retention floor a manifest repair seeds the rebuilt tree with
2898 /// (default `0`): after [`repair`](Self::repair) /
2899 /// [`open_or_repair`](Self::open_or_repair) every snapshot at or below
2900 /// `floor` is refused with
2901 /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention),
2902 /// exactly as the tree refused it before the manifest was lost.
2903 ///
2904 /// A normal open needs no help: the manifest carries the floor every
2905 /// retention-advancing install established (a GC compaction, `clear`, a
2906 /// table drop, a filtering compaction; see
2907 /// [`AbstractTree::retention_floor`](crate::AbstractTree::retention_floor)).
2908 /// A repair rebuilds the manifest from the tables, which do not record it
2909 /// (a GC compaction zeroes the seqnos of the rows it settles), so only the
2910 /// deployment knows it: record
2911 /// [`retention_floor()`](crate::AbstractTree::retention_floor) in your own
2912 /// durable state (it already folds in every operation that raised the
2913 /// boundary, so no per-operation bookkeeping is needed) and pass the last
2914 /// recorded value here. Left at `0`, a repaired tree serves every
2915 /// snapshot, which is correct only if history was never collected. Has no
2916 /// effect on an open that finds a manifest.
2917 #[must_use]
2918 pub fn repair_retention_floor(mut self, floor: crate::SeqNo) -> Self {
2919 self.repair_retention_floor = floor;
2920 self
2921 }
2922
2923 /// Sets the compaction I/O rate limit in bytes per second.
2924 ///
2925 /// Caps how fast the compaction worker may issue I/O so background
2926 /// compaction does not saturate the device and spike user read P99.
2927 /// `0` (the default) disables throttling. Only compaction is limited;
2928 /// flush and user reads always pass through.
2929 ///
2930 /// The tree builds its own limiter from this figure at open; change it
2931 /// later through [`AbstractTree::compaction_rate_limiter`](crate::AbstractTree::compaction_rate_limiter).
2932 /// Ignored when a shared limiter is supplied through
2933 /// [`Self::compaction_rate_limiter`].
2934 ///
2935 /// The figure is the share of the device the deployment gives compaction,
2936 /// and only the deployment knows it. Below what the write rate times the
2937 /// strategy's write amplification needs, compaction falls behind: runs
2938 /// pile up and every read visits more of them. The
2939 /// [`Planned`](crate::compaction::Planned) strategy reads the limit and
2940 /// plans a tree it can keep (see its section on the compaction budget);
2941 /// [`Leveled`](crate::compaction::Leveled) keeps its shape and lags.
2942 #[must_use]
2943 pub fn compaction_rate_limit(mut self, bytes_per_sec: u64) -> Self {
2944 self.compaction_rate_limit = bytes_per_sec;
2945 self
2946 }
2947
2948 /// Supplies a compaction rate limiter shared with other trees, used in
2949 /// place of the per-tree one built from [`Self::compaction_rate_limit`].
2950 ///
2951 /// Hand the same `Arc` to every tree on one device: their compactions are
2952 /// then bounded by it together, at the rate it holds, whatever each
2953 /// tree's configured [`Self::compaction_rate_limit`] says. A retune through
2954 /// [`RateLimiter::set_rate`](crate::rate_limiter::RateLimiter::set_rate)
2955 /// on any holder applies to all of them on their next request.
2956 ///
2957 /// # Examples
2958 ///
2959 /// ```
2960 /// use lsm_tree::rate_limiter::RateLimiter;
2961 /// use lsm_tree::Config;
2962 /// use std::sync::Arc;
2963 ///
2964 /// # let a = tempfile::tempdir()?;
2965 /// # let b = tempfile::tempdir()?;
2966 /// let device = Arc::new(RateLimiter::new(64 * 1_024 * 1_024));
2967 /// let first = Config::new(a.path(), Default::default(), Default::default())
2968 /// .compaction_rate_limiter(Arc::clone(&device))
2969 /// .open()?;
2970 /// let second = Config::new(b.path(), Default::default(), Default::default())
2971 /// .compaction_rate_limiter(Arc::clone(&device))
2972 /// .open()?;
2973 /// // Both trees now share the device's 64 MiB/s.
2974 /// # drop((first, second));
2975 /// # Ok::<(), lsm_tree::Error>(())
2976 /// ```
2977 #[must_use]
2978 pub fn compaction_rate_limiter(
2979 mut self,
2980 limiter: Arc<crate::rate_limiter::RateLimiter>,
2981 ) -> Self {
2982 self.compaction_rate_limiter = Some(limiter);
2983 self
2984 }
2985
2986 /// Supplies the pool this tree's compactions reserve the room for their
2987 /// outputs in, shared with other trees, in place of a pool of its own.
2988 ///
2989 /// Hand the same `Arc` to every tree whose files share volumes: each
2990 /// compaction then reserves against what the others have still to write,
2991 /// rather than two of them counting on the same free bytes. See
2992 /// [`CapacityPool`](crate::CapacityPool).
2993 #[must_use]
2994 pub fn compaction_capacity(mut self, pool: Arc<crate::CapacityPool>) -> Self {
2995 self.compaction_capacity = Some(pool);
2996 self
2997 }
2998
2999 /// The pool a tree opened with this configuration reserves its
3000 /// compactions' room in: the shared one if supplied, else its own.
3001 pub(crate) fn tree_compaction_capacity(&self) -> Arc<crate::CapacityPool> {
3002 self.compaction_capacity.clone().unwrap_or_default()
3003 }
3004
3005 /// Sets the byte rate writes are paced to while the tree's backpressure
3006 /// verdict is in its slowdown tier (default `0`: no byte pacing).
3007 ///
3008 /// With a rate set,
3009 /// [`AbstractTree::write_backpressure_for`](crate::AbstractTree::write_backpressure_for)
3010 /// answers a slowdown with what the write's bytes owe at this rate, so a
3011 /// caller honouring it writes no faster than the rate however large or
3012 /// small its writes are; below the slowdown thresholds nothing is debited
3013 /// and writes run at full speed. The tree builds its own limiter from
3014 /// this figure at open; change it later through
3015 /// [`AbstractTree::write_rate_limiter`](crate::AbstractTree::write_rate_limiter).
3016 /// Ignored when a shared limiter is supplied through
3017 /// [`Self::write_rate_limiter`].
3018 ///
3019 /// # Examples
3020 ///
3021 /// ```
3022 /// use lsm_tree::{Config, SequenceNumberCounter};
3023 ///
3024 /// let folder = tempfile::tempdir()?;
3025 /// let tree = Config::new(
3026 /// &folder,
3027 /// SequenceNumberCounter::default(),
3028 /// SequenceNumberCounter::default(),
3029 /// )
3030 /// .write_rate_limit(16 * 1_024 * 1_024)
3031 /// .open()?;
3032 /// # Ok::<(), lsm_tree::Error>(())
3033 /// ```
3034 #[must_use]
3035 pub fn write_rate_limit(mut self, bytes_per_sec: u64) -> Self {
3036 self.write_rate_limit = bytes_per_sec;
3037 self
3038 }
3039
3040 /// Supplies a write rate limiter shared with other trees, used in place of
3041 /// the per-tree one built from [`Self::write_rate_limit`]: writes paced
3042 /// under slowdown by any of them draw on one budget, at the rate it holds.
3043 #[must_use]
3044 pub fn write_rate_limiter(mut self, limiter: Arc<crate::rate_limiter::RateLimiter>) -> Self {
3045 self.write_rate_limiter = Some(limiter);
3046 self
3047 }
3048
3049 /// Joins the tree to a [`WriteBufferBudget`](crate::WriteBufferBudget)
3050 /// shared with other trees: the memtable axis of its backpressure verdict
3051 /// then compares the unflushed memtable bytes of every member together.
3052 #[cfg(feature = "std")]
3053 #[must_use]
3054 pub fn write_buffer_budget(
3055 mut self,
3056 budget: Arc<crate::backpressure::WriteBufferBudget>,
3057 ) -> Self {
3058 self.write_buffer_budget = Some(budget);
3059 self
3060 }
3061
3062 /// The limiter a tree opened with this configuration paces slowed-down
3063 /// writes with: the shared one if supplied, else its own built from
3064 /// [`Self::write_rate_limit`].
3065 pub(crate) fn tree_write_rate_limiter(&self) -> Arc<crate::rate_limiter::RateLimiter> {
3066 self.write_rate_limiter.clone().unwrap_or_else(|| {
3067 Arc::new(crate::rate_limiter::RateLimiter::new(self.write_rate_limit))
3068 })
3069 }
3070
3071 /// The limiter a tree opened with this configuration throttles its
3072 /// compactions with: the shared one if supplied, else its own built from
3073 /// [`Self::compaction_rate_limit`].
3074 pub(crate) fn tree_compaction_rate_limiter(&self) -> Arc<crate::rate_limiter::RateLimiter> {
3075 self.compaction_rate_limiter.clone().unwrap_or_else(|| {
3076 Arc::new(crate::rate_limiter::RateLimiter::new(
3077 self.compaction_rate_limit,
3078 ))
3079 })
3080 }
3081
3082 /// Sets the compaction worker-thread count.
3083 ///
3084 /// Under `std` this both sizes the per-tree pool built at open when no
3085 /// shared pool is supplied (see [`Self::compaction_pool`]), which prepares
3086 /// a table's data blocks and builds its partitioned filter's partitions,
3087 /// and caps how many range-parallel sub-compactions a compaction splits
3088 /// into.
3089 /// `1` keeps compaction serial. Default is `max(1, available_parallelism /
3090 /// 2)`. Without the `parallel` feature there is no built-in pool, so the
3091 /// work runs serially even for a value > 1.
3092 #[cfg(feature = "std")]
3093 #[must_use]
3094 pub fn compaction_threads(mut self, threads: usize) -> Self {
3095 // Clamp to >= 1: the documented semantics treat `1` as "serial", and a
3096 // 0-thread pool would be an invalid state.
3097 self.compaction_threads = threads.max(1);
3098 self
3099 }
3100
3101 /// Sets the payload size (bytes) below which a block of a table compressed
3102 /// in parallel is prepared on the writer thread instead of a worker. A
3103 /// block that small costs less to compress than to hand to a worker and
3104 /// back. `None` (default) derives the threshold from the table's block
3105 /// codec: 1 KiB for lz4; 8 KiB with no codec, where only encryption or
3106 /// page ECC brings a table to the workers and costs less per block than
3107 /// lz4; and 0 for zstd, whose per-frame setup makes any block worth a
3108 /// worker. `Some(0)` sends every block to the workers.
3109 ///
3110 /// # Examples
3111 ///
3112 /// ```
3113 /// use lsm_tree::{Config, SequenceNumberCounter};
3114 /// # let folder = tempfile::tempdir()?;
3115 /// let config = Config::new(
3116 /// folder.path(),
3117 /// SequenceNumberCounter::default(),
3118 /// SequenceNumberCounter::default(),
3119 /// )
3120 /// .parallel_compression_inline_below(Some(1_024));
3121 /// # Ok::<(), Box<dyn std::error::Error>>(())
3122 /// ```
3123 #[cfg(feature = "std")]
3124 #[must_use]
3125 pub fn parallel_compression_inline_below(mut self, bytes: Option<u32>) -> Self {
3126 self.parallel_compression_inline_below = bytes;
3127 self
3128 }
3129
3130 /// The parallel block compression a table writer of this tree uses, or
3131 /// `None` when no pool is configured.
3132 #[cfg(feature = "std")]
3133 pub(crate) fn parallel_compression(&self) -> Option<crate::table::writer::ParallelCompression> {
3134 self.compaction_pool
3135 .clone()
3136 .map(|spawner| crate::table::writer::ParallelCompression {
3137 spawner,
3138 threads: self.compaction_threads,
3139 inline_below: self.parallel_compression_inline_below,
3140 })
3141 }
3142
3143 /// Sets the minimum total input size (bytes) for a compaction to be split
3144 /// into parallel sub-compactions. Default 8 MiB. `0` splits every eligible
3145 /// compaction; a large value effectively disables sub-compaction (block
3146 /// compression still parallelizes via [`Self::compaction_threads`]).
3147 #[cfg(feature = "std")]
3148 #[must_use]
3149 pub fn subcompaction_min_bytes(mut self, bytes: u64) -> Self {
3150 self.subcompaction_min_bytes = bytes;
3151 self
3152 }
3153
3154 /// Supplies a shared compaction thread pool, used in place of the per-tree
3155 /// default. Pass one [`crate::table::writer::CompactionSpawner`] (e.g. a
3156 /// `RayonSpawner` wrapping a shared rayon thread pool) to several trees so
3157 /// the total worker-thread count stays bounded by the pool size rather than
3158 /// the number of open trees.
3159 #[cfg(feature = "std")]
3160 #[must_use]
3161 pub fn compaction_pool(
3162 mut self,
3163 pool: Option<Arc<dyn crate::table::writer::CompactionSpawner>>,
3164 ) -> Self {
3165 self.compaction_pool = pool;
3166 self
3167 }
3168
3169 /// Supplies a pre-trained zstd dictionary for the data block policy to
3170 /// name.
3171 ///
3172 /// The open registers it with the tree, which stores it, so a later open
3173 /// needs nothing supplied. Which blocks are written against it is decided
3174 /// by the policy: a [`CompressionType::ZstdDict`] entry names a dictionary
3175 /// by id, and every block reads back through the id it recorded. The
3176 /// dictionary should be trained on representative data samples for best
3177 /// results.
3178 ///
3179 /// Create a dictionary with [`ZstdDictionary::new`](crate::ZstdDictionary::new),
3180 /// then use [`CompressionType::zstd_dict`] to create a matching
3181 /// compression type:
3182 ///
3183 /// ```ignore
3184 /// use lsm_tree::{CompressionType, ZstdDictionary};
3185 ///
3186 /// let dict = ZstdDictionary::new(&training_data);
3187 /// let compression = CompressionType::zstd_dict(3, dict.id()).unwrap();
3188 ///
3189 /// config
3190 /// .zstd_dictionary(Some(Arc::new(dict)))
3191 /// .data_block_compression_policy(CompressionPolicy::all(compression));
3192 /// ```
3193 #[cfg(zstd_any)]
3194 #[must_use]
3195 pub fn zstd_dictionary(
3196 mut self,
3197 dictionary: Option<Arc<crate::compression::ZstdDictionary>>,
3198 ) -> Self {
3199 self.zstd_dictionary = dictionary;
3200 self
3201 }
3202}
3203
3204#[cfg(test)]
3205mod builder_tests;