Skip to main content

turbo_persistence/
db.rs

1use std::{
2    borrow::Cow,
3    collections::HashSet,
4    fmt::Display,
5    io::{BufWriter, ErrorKind, Write},
6    mem::take,
7    ops::RangeInclusive,
8    path::{Path, PathBuf},
9    sync::{
10        OnceLock,
11        atomic::{AtomicBool, AtomicU32, Ordering},
12    },
13};
14
15use anyhow::{Context, Result, bail};
16use byteorder::{BE, ReadBytesExt, WriteBytesExt};
17use dashmap::DashSet;
18use fs_err::{self as fs, File, OpenOptions, ReadDir};
19use jiff::Timestamp;
20use memmap2::Mmap;
21use nohash_hasher::BuildNoHashHasher;
22use parking_lot::{Mutex, RwLock};
23use serde::{Deserialize, Serialize};
24use smallvec::SmallVec;
25use tracing::span::EnteredSpan;
26
27pub use crate::compaction::selector::CompactConfig;
28use crate::{
29    DbConfig, FamilyKind, QueryKey,
30    arc_bytes::ArcBytes,
31    compaction::selector::{Compactable, get_merge_segments},
32    compression::{checksum_block, decompress_into_arc},
33    constants::{
34        DATA_THRESHOLD_PER_COMPACTED_FILE, KEY_BLOCK_AVG_SIZE, KEY_BLOCK_CACHE_SIZE,
35        MAX_ENTRIES_PER_COMPACTED_FILE, VALUE_BLOCK_AVG_SIZE, VALUE_BLOCK_CACHE_SIZE,
36    },
37    key::{StoreKey, hash_key},
38    lookup_entry::{IterValue, LookupEntry, LookupValue},
39    merge_iter::MergeIter,
40    meta_file::{MetaEntryFlags, MetaFile, MetaLookupResult, StaticSortedFileRange},
41    meta_file_builder::MetaFileBuilder,
42    mmap_helper::advise_mmap_for_persistence,
43    parallel_scheduler::ParallelScheduler,
44    rc_bytes::RcBytes,
45    sst_filter::SstFilter,
46    static_sorted_file::{BlockCache, SstLookupResult, StaticSortedFileIter},
47    static_sorted_file_builder::{StaticSortedFileBuilderMeta, StreamingSstWriter},
48    write_batch::{FinishResult, NewFile, WriteBatch},
49};
50
51#[cfg(feature = "stats")]
52#[derive(Debug)]
53pub struct CacheStatistics {
54    pub hit_rate: f32,
55    pub fill: f32,
56    pub items: usize,
57    pub size: u64,
58    pub hits: u64,
59    pub misses: u64,
60}
61
62#[cfg(feature = "stats")]
63impl CacheStatistics {
64    fn new<Key, Val, We, B, L>(cache: &quick_cache::sync::Cache<Key, Val, We, B, L>) -> Self
65    where
66        Key: Eq + std::hash::Hash,
67        Val: Clone,
68        We: quick_cache::Weighter<Key, Val> + Clone,
69        B: std::hash::BuildHasher + Clone,
70        L: quick_cache::Lifecycle<Key, Val> + Clone,
71    {
72        let size = cache.weight();
73        let hits = cache.hits();
74        let misses = cache.misses();
75        Self {
76            hit_rate: hits as f32 / (hits + misses) as f32,
77            fill: size as f32 / cache.capacity() as f32,
78            items: cache.len(),
79            size,
80            hits,
81            misses,
82        }
83    }
84}
85
86#[cfg(feature = "stats")]
87#[derive(Debug)]
88pub struct Statistics {
89    pub meta_files: usize,
90    pub sst_files: usize,
91    pub key_block_cache: CacheStatistics,
92    pub value_block_cache: CacheStatistics,
93    pub hits: u64,
94    pub misses: u64,
95    pub miss_family: u64,
96    pub miss_range: u64,
97    pub miss_amqf: u64,
98    pub miss_key: u64,
99}
100
101#[cfg(feature = "stats")]
102#[derive(Default)]
103struct TrackedStats {
104    hits_deleted: std::sync::atomic::AtomicU64,
105    hits_small: std::sync::atomic::AtomicU64,
106    hits_blob: std::sync::atomic::AtomicU64,
107    miss_family: std::sync::atomic::AtomicU64,
108    miss_range: std::sync::atomic::AtomicU64,
109    miss_amqf: std::sync::atomic::AtomicU64,
110    miss_key: std::sync::atomic::AtomicU64,
111    miss_global: std::sync::atomic::AtomicU64,
112}
113
114/// State of the active write slot.
115enum ActiveWriteState {
116    /// A write operation or compaction is in progress.
117    /// The string is a human-readable name used in error messages.
118    Active(&'static str),
119    /// A previous write or compaction failed and recovery also failed.
120    /// No further writes are possible.
121    Error,
122}
123
124/// A single superseded file whose deletion failed and is being retried.
125///
126/// On Linux/macOS, deleting a memory-mapped file is safe and this list is
127/// normally empty. On Windows, open memory maps prevent deletion; failed files
128/// are collected here and retried on the next commit or shutdown.
129enum DeferredDeletion {
130    Sst(u32),
131    Meta(u32),
132    Blob(u32),
133}
134
135/// RAII guard for an active write operation.
136///
137/// When dropped without [`WriteOperationGuard::success`] being called first, the guard rolls back
138/// the operation by deleting any files whose sequence number exceeds `seq_before` (the sequence
139/// number at the time the operation started). If rollback itself fails the write slot is set to
140/// [`ActiveWriteState::Error`], permanently disabling further writes.
141pub(crate) struct WriteOperationGuard<'a> {
142    /// Reference to the active-write-operation slot, so we can clear or error it on drop.
143    active: &'a Mutex<Option<ActiveWriteState>>,
144    /// Database directory path, needed for orphan-file deletion during rollback.
145    path: &'a Path,
146    /// Sequence number at the time the operation started (= the last committed seq on disk).
147    /// Files with seq > this were created by the current operation and must be deleted on
148    /// rollback.
149    seq_before: u32,
150    /// Set to `true` by [`WriteOperationGuard::success`] to skip rollback on drop.
151    succeeded: bool,
152}
153
154impl WriteOperationGuard<'_> {
155    /// Mark the operation as successfully completed.
156    ///
157    /// After this call the guard's `Drop` impl will release the write slot without rolling back.
158    pub(crate) fn success(&mut self) {
159        self.succeeded = true;
160    }
161}
162
163/// The contents of the `CURRENT` file: which sequence number is committed, and when that commit
164/// happened.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct CurrentDbVersion {
167    /// The highest sequence number that is part of the committed database.
168    pub max_sequence_number: u32,
169    /// When this database was last committed to.
170    pub commit_time: Timestamp,
171}
172
173/// Reads the `CURRENT` file in the database directory `path`.
174///
175/// Returns `Ok(None)` if the file doesn't exist, which for a writable database means "not
176/// initialized yet".
177pub fn read_current_version(path: &Path) -> Result<Option<CurrentDbVersion>> {
178    let current_path = path.join("CURRENT");
179    let content = match fs::read(&current_path) {
180        Ok(content) => content,
181        Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
182        Err(e) => return Err(e).context("Failed to read CURRENT file"),
183    };
184
185    serde_json::from_slice::<CurrentDbVersion>(&content)
186        .with_context(|| {
187            format!(
188                "CURRENT file at {} is corrupt ({} bytes)",
189                current_path.display(),
190                content.len()
191            )
192        })
193        .map(Some)
194}
195
196/// Durably and atomically updates the `CURRENT` file in the database directory `path` to point at
197/// `seq`, stamping it as used now.
198///
199/// The write is made atomic by writing to a temporary `CURRENT.next` file, flushing it, and then
200/// `rename`ing it over `CURRENT`. A `rename` within a directory is atomic on POSIX and replaces the
201/// destination on Windows, so a concurrent or crashing writer can never observe a torn `CURRENT`
202/// (in-place overwrites, by contrast, can leave a partially-written value on a crash mid-write).
203/// After the rename we fsync the directory so the new `CURRENT` → inode mapping survives a crash.
204fn commit_current(path: &Path, seq: u32) -> Result<()> {
205    let version: &CurrentDbVersion = &CurrentDbVersion {
206        max_sequence_number: seq,
207        commit_time: Timestamp::now(),
208    };
209    let mut contents =
210        serde_json::to_string(version).context("Failed to serialize the CURRENT file")?;
211    contents.push('\n');
212    let next_path = path.join("CURRENT.next");
213    let mut next_file = File::create(&next_path)?;
214    next_file.write_all(contents.as_bytes())?;
215    next_file.sync_data()?;
216    drop(next_file);
217    fs::rename(&next_path, path.join("CURRENT"))?;
218    // Fsync the directory. This is the single durability barrier for a commit: by the time we get
219    // here every file created earlier in the commit (SST/meta/blob and any `.del` file) already
220    // exists, so this one fsync flushes *all* of their directory entries together with the CURRENT
221    // rename. Because the file *contents* were already `sync_data`'d before this call and the
222    // rename is the last directory mutation, a crash can never leave a durable CURRENT pointing at
223    // files whose directory entries were lost. Callers therefore do not need a separate directory
224    // fsync before invoking this.
225    //
226    // Skipped on Windows: `sync_data` on a directory handle fails with ERROR_ACCESS_DENIED (the
227    // handle `File::open` returns for a directory has no write access).Apparently metadata changes
228    // are always atomic on windows so this is simply unneeded.
229    #[cfg(not(windows))]
230    File::open(path)
231        .and_then(|dir| dir.sync_data())
232        .context("Failed to sync database directory after updating CURRENT")?;
233    Ok(())
234}
235
236/// Deletes all files in `path` whose numeric stem is greater than `seq_before`.
237///
238/// Called on rollback to clean up any SST, meta, blob, or del files written during a
239/// failed write operation or compaction.
240fn delete_orphan_files(path: &Path, seq_before: u32) -> Result<()> {
241    // Restore CURRENT to seq_before first, so the on-disk state is consistent before we start
242    // deleting the orphan files that a failed write/compaction left behind.
243    commit_current(path, seq_before).context("Unable to restore CURRENT file")?;
244
245    for entry in fs::read_dir(path)? {
246        let entry = entry?;
247        let path = entry.path();
248        if let Some(ext) = path.extension().and_then(|s| s.to_str())
249            && let Some(seq) = path
250                .file_stem()
251                .and_then(|s| s.to_str())
252                .and_then(|s| s.parse::<u32>().ok())
253            && seq > seq_before
254        {
255            match ext {
256                "sst" | "meta" | "blob" | "del" => fs::remove_file(&path)?,
257                _ => {}
258            }
259        }
260    }
261    Ok(())
262}
263
264impl Drop for WriteOperationGuard<'_> {
265    fn drop(&mut self) {
266        if self.succeeded {
267            // Happy path: just release the slot.
268            *self.active.lock() = None;
269            return;
270        }
271
272        // Unhappy path: the operation failed (or was dropped without commit).
273        // Delete every file that was created during this operation (seq > seq_before).
274        match delete_orphan_files(self.path, self.seq_before) {
275            Ok(()) => *self.active.lock() = None,
276            Err(_) => *self.active.lock() = Some(ActiveWriteState::Error),
277        }
278    }
279}
280
281/// TurboPersistence is a persistent key-value store. It is limited to a single writer at a time
282/// using a single write batch. It allows for concurrent reads.
283pub struct TurboPersistence<S: ParallelScheduler, const FAMILIES: usize> {
284    parallel_scheduler: S,
285    /// The path to the directory where the database is stored
286    path: PathBuf,
287    /// If true, the database is opened in read-only mode. In this mode, no writes are allowed and
288    /// no modification on the database is performed.
289    read_only: bool,
290    /// The inner state of the database. Writing will update that.
291    inner: RwLock<Inner<FAMILIES>>,
292    /// A flag to indicate if the database is empty (no meta files). This is an atomic mirror of
293    /// `inner.meta_files.is_empty()` to avoid taking a lock on the hot path.
294    is_empty: AtomicBool,
295    /// Tracks whether a write operation is in progress or has permanently failed.
296    /// `None` = idle, `Some(Active)` = in progress, `Some(Error)` = permanently disabled.
297    active_write_operation: Mutex<Option<ActiveWriteState>>,
298    /// Files from superseded commits whose deletion failed (e.g. on Windows due to open memory
299    /// maps) and will be retried on the next commit or at shutdown.
300    /// Protected by `active_write_operation` (only mutated inside a write operation).
301    deferred_deletions: Mutex<Vec<DeferredDeletion>>,
302    /// A cache for decompressed key blocks. Allocated lazily on first read via
303    /// [`Self::key_block_cache`] so write-only or empty sessions never pay the cache's fixed
304    /// hash-table overhead.
305    key_block_cache: OnceLock<BlockCache>,
306    /// A cache for decompressed value blocks. Allocated lazily on first read via
307    /// [`Self::value_block_cache`]; see [`Self::key_block_cache`].
308    value_block_cache: OnceLock<BlockCache>,
309    /// Per-family configuration for file limits.
310    config: DbConfig<FAMILIES>,
311    /// Statistics for the database.
312    #[cfg(feature = "stats")]
313    stats: TrackedStats,
314}
315
316/// The inner state of the database.
317struct Inner<const FAMILIES: usize> {
318    /// The list of meta files in the database. This is used to derive the SST files.
319    meta_files: Vec<MetaFile>,
320    /// The current sequence number for the database.
321    current_sequence_number: u32,
322    /// The in progress set of hashes of keys that have been accessed.
323    /// It will be flushed onto disk (into a meta file) on next commit.
324    /// It's a dashset to allow modification while only tracking a read lock on Inner.
325    accessed_key_hashes: [DashSet<u64, BuildNoHashHasher<u64>>; FAMILIES],
326}
327
328pub struct CommitOptions {
329    new_meta_files: Vec<NewFile>,
330    new_sst_files: Vec<NewFile>,
331    new_blob_files: Vec<NewFile>,
332    sst_files_to_delete: Vec<DeletedFile>,
333    blob_seq_numbers_to_delete: Vec<u32>,
334    sequence_number: u32,
335    keys_written: u64,
336}
337
338/// An SST file superseded by a commit, carrying its on-disk size (known when the deletion is
339/// decided) so `commit` can sum deleted bytes without scanning meta entries or stat'ing the file.
340#[derive(Clone, Copy)]
341struct DeletedFile {
342    seq: u32,
343    /// On-disk size in bytes
344    size: u64,
345}
346
347/// Physical byte volume of a single commit/compaction cycle, measured from on-disk file sizes
348/// (post-compression, including `.sst`, `.blob`, and `.meta` files).
349#[derive(Clone, Copy, Debug, Default)]
350pub struct CommitStats {
351    /// Total bytes of new files created by this commit.
352    pub bytes_written: u64,
353    /// Total bytes of files removed/superseded by this commit.
354    pub bytes_deleted: u64,
355}
356
357impl Display for CommitStats {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        let CommitStats {
360            bytes_written,
361            bytes_deleted,
362        } = self;
363        write!(
364            f,
365            "bytes_written={bytes_written} bytes_deleted={bytes_deleted}"
366        )
367    }
368}
369
370struct OpenOpts<S: ParallelScheduler, const FAMILIES: usize> {
371    path: PathBuf,
372    read_only: bool,
373    parallel_scheduler: S,
374    config: DbConfig<FAMILIES>,
375}
376
377impl<S: ParallelScheduler + Default, const FAMILIES: usize> TurboPersistence<S, FAMILIES> {
378    /// Open a TurboPersistence database at the given path.
379    /// This will read the directory and might performance cleanup when the database was not closed
380    /// properly. Cleanup only requires to read a few bytes from a few files and to delete
381    /// files, so it's fast.
382    pub fn open(path: PathBuf) -> Result<Self> {
383        Self::open_with_parallel_scheduler(path, Default::default())
384    }
385
386    /// Open a TurboPersistence database at the given path with custom per-family configuration.
387    pub fn open_with_config(path: PathBuf, config: DbConfig<FAMILIES>) -> Result<Self> {
388        Self::open_with_config_and_parallel_scheduler(path, config, Default::default())
389    }
390
391    /// Open a TurboPersistence database at the given path in read only mode.
392    /// This will read the directory. No Cleanup is performed.
393    pub fn open_read_only_with_config(path: PathBuf, config: DbConfig<FAMILIES>) -> Result<Self> {
394        Self::open_read_only_with_parallel_scheduler(path, config, Default::default())
395    }
396
397    /// Construct an empty, read-only `TurboPersistence` that owns no on-disk state and never
398    /// touches the filesystem. Reads return None; writes bail via the existing `read_only` guard.
399    /// Used to provide a "noop" backing storage with the same concrete type as the real one.
400    pub fn empty_in_memory_with_config(config: DbConfig<FAMILIES>) -> Self {
401        // `path` is `PathBuf::new()` but never read because `meta_files` is empty and
402        // `read_only` is true (so no write/compaction path is reachable).
403        Self::new(OpenOpts {
404            path: PathBuf::new(),
405            read_only: true,
406            parallel_scheduler: Default::default(),
407            config,
408        })
409    }
410}
411
412impl<S: ParallelScheduler, const FAMILIES: usize> TurboPersistence<S, FAMILIES> {
413    fn new(
414        OpenOpts {
415            path,
416            read_only,
417            parallel_scheduler,
418            config,
419        }: OpenOpts<S, FAMILIES>,
420    ) -> Self {
421        Self {
422            parallel_scheduler,
423            path,
424            read_only,
425            inner: RwLock::new(Inner {
426                meta_files: Vec::new(),
427                current_sequence_number: 0,
428                accessed_key_hashes: [(); FAMILIES]
429                    .map(|_| DashSet::with_hasher(BuildNoHashHasher::default())),
430            }),
431            is_empty: AtomicBool::new(true),
432            active_write_operation: Mutex::new(None),
433            deferred_deletions: Mutex::new(Vec::new()),
434            key_block_cache: OnceLock::new(),
435            value_block_cache: OnceLock::new(),
436            config,
437            #[cfg(feature = "stats")]
438            stats: TrackedStats::default(),
439        }
440    }
441
442    /// Open a TurboPersistence database at the given path.
443    /// This will read the directory and might performance cleanup when the database was not closed
444    /// properly. Cleanup only requires to read a few bytes from a few files and to delete
445    /// files, so it's fast.
446    pub fn open_with_parallel_scheduler(path: PathBuf, parallel_scheduler: S) -> Result<Self> {
447        Self::open_with_config_and_parallel_scheduler(path, DbConfig::default(), parallel_scheduler)
448    }
449
450    /// Open a TurboPersistence database at the given path with custom per-family configuration.
451    pub fn open_with_config_and_parallel_scheduler(
452        path: PathBuf,
453        config: DbConfig<FAMILIES>,
454        parallel_scheduler: S,
455    ) -> Result<Self> {
456        let mut db = Self::new(OpenOpts {
457            path,
458            read_only: false,
459            parallel_scheduler,
460            config,
461        });
462        db.open_directory(false)?;
463        Ok(db)
464    }
465
466    /// Open a TurboPersistence database at the given path in read only mode.
467    /// This will read the directory. No Cleanup is performed.
468    fn open_read_only_with_parallel_scheduler(
469        path: PathBuf,
470        config: DbConfig<FAMILIES>,
471        parallel_scheduler: S,
472    ) -> Result<Self> {
473        let mut db = Self::new(OpenOpts {
474            path,
475            read_only: true,
476            parallel_scheduler,
477            config,
478        });
479        db.open_directory(false)?;
480        Ok(db)
481    }
482
483    /// Performs the initial check on the database directory.
484    fn open_directory(&mut self, read_only: bool) -> Result<()> {
485        match fs::read_dir(&self.path) {
486            Ok(entries) => {
487                if !self
488                    .load_directory(entries, read_only)
489                    .context("Loading persistence directory failed")?
490                {
491                    if read_only {
492                        bail!("Failed to open database");
493                    }
494                    commit_current(&self.path, 0)
495                        .context("Initializing persistence directory failed")?;
496                }
497                Ok(())
498            }
499            Err(e) => {
500                if !read_only && e.kind() == ErrorKind::NotFound {
501                    self.create_and_init_directory()
502                        .context("Creating and initializing persistence directory failed")?;
503                    Ok(())
504                } else {
505                    Err(e).context("Failed to open database")
506                }
507            }
508        }
509    }
510
511    /// Creates the directory and initializes it.
512    fn create_and_init_directory(&mut self) -> Result<()> {
513        fs::create_dir_all(&self.path)?;
514        commit_current(&self.path, 0)
515    }
516
517    /// Loads an existing database directory and performs cleanup if necessary.
518    fn load_directory(&mut self, entries: ReadDir, read_only: bool) -> Result<bool> {
519        let mut meta_files = Vec::new();
520        let current = match read_current_version(&self.path)? {
521            Some(version) => version.max_sequence_number,
522            None if !read_only => return Ok(false),
523            None => bail!("Failed to open database: CURRENT file is missing"),
524        };
525
526        let mut deleted_files = HashSet::new();
527        for entry in entries {
528            let entry = entry?;
529            let path = entry.path();
530            if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
531                // A leftover `CURRENT.next` means a crash interrupted a `commit_current` before
532                // the rename onto `CURRENT` completed. The current `CURRENT` (already read above)
533                // is authoritative, so the stale temp file is just deleted.
534                if path.file_stem().and_then(|s| s.to_str()) == Some("CURRENT") {
535                    if !read_only {
536                        fs::remove_file(&path)?;
537                    }
538                    continue;
539                }
540                let seq: u32 = path
541                    .file_stem()
542                    .context("File has no file stem")?
543                    .to_str()
544                    .context("File stem is not valid utf-8")?
545                    .parse()?;
546                if deleted_files.contains(&seq) {
547                    continue;
548                }
549                if seq > current {
550                    if !read_only {
551                        fs::remove_file(&path)?;
552                    }
553                } else {
554                    match ext {
555                        "meta" => {
556                            meta_files.push(seq);
557                        }
558                        "del" => {
559                            let mut content = &*fs::read(&path)?;
560                            let mut no_existing_files = true;
561                            while !content.is_empty() {
562                                let seq = content.read_u32::<BE>()?;
563                                deleted_files.insert(seq);
564                                if !read_only {
565                                    // Remove the files that are marked for deletion
566                                    let sst_file = self.path.join(format!("{seq:08}.sst"));
567                                    let meta_file = self.path.join(format!("{seq:08}.meta"));
568                                    let blob_file = self.path.join(format!("{seq:08}.blob"));
569                                    for path in [sst_file, meta_file, blob_file] {
570                                        if fs::exists(&path)? {
571                                            fs::remove_file(path)?;
572                                            no_existing_files = false;
573                                        }
574                                    }
575                                }
576                            }
577                            if !read_only && no_existing_files {
578                                fs::remove_file(&path)?;
579                            }
580                        }
581                        "blob" | "sst" => {
582                            // ignore blobs and sst, they are read when needed
583                        }
584                        _ => {
585                            if !path
586                                .file_name()
587                                .is_some_and(|s| s.as_encoded_bytes().starts_with(b"."))
588                            {
589                                bail!("Unexpected file in persistence directory: {:?}", path);
590                            }
591                        }
592                    }
593                }
594            } else {
595                match path.file_stem().and_then(|s| s.to_str()) {
596                    Some("CURRENT") => {
597                        // Already read
598                    }
599                    Some("LOG") => {
600                        // Ignored, write-only
601                    }
602                    _ => {
603                        if !path
604                            .file_name()
605                            .is_some_and(|s| s.as_encoded_bytes().starts_with(b"."))
606                        {
607                            bail!("Unexpected file in persistence directory: {:?}", path);
608                        }
609                    }
610                }
611            }
612        }
613
614        meta_files.retain(|seq| !deleted_files.contains(seq));
615        meta_files.sort_unstable();
616        let mut meta_files = self
617            .parallel_scheduler
618            .parallel_map_collect::<_, _, Result<Vec<MetaFile>>>(&meta_files, |&seq| {
619                let meta_file = MetaFile::open(&self.path, seq)?;
620                Ok(meta_file)
621            })?;
622
623        let mut sst_filter = SstFilter::new();
624        for meta_file in meta_files.iter_mut().rev() {
625            sst_filter.apply_filter(meta_file);
626        }
627
628        let inner = self.inner.get_mut();
629        self.is_empty
630            .store(meta_files.is_empty(), Ordering::Relaxed);
631        inner.meta_files = meta_files;
632        inner.current_sequence_number = current;
633        Ok(true)
634    }
635
636    /// Reads and decompresses a blob file. This is not backed by any cache.
637    #[tracing::instrument(level = "info", name = "reading database blob", skip_all)]
638    fn read_blob(&self, seq: u32) -> Result<ArcBytes> {
639        let path = self.path.join(format!("{seq:08}.blob"));
640        let file = File::open(&path)?;
641        let mmap = unsafe { Mmap::map(file.file()) }.with_context(|| {
642            format!(
643                "Failed to mmap blob file {} ({} bytes)",
644                path.display(),
645                file.metadata().map(|m| m.len()).unwrap_or(0)
646            )
647        })?;
648        #[cfg(unix)]
649        mmap.advise(memmap2::Advice::Sequential)?;
650        #[cfg(unix)]
651        mmap.advise(memmap2::Advice::WillNeed)?;
652        advise_mmap_for_persistence(&mmap)?;
653        let mut reader = &mmap[..];
654        let uncompressed_length = reader
655            .read_u32::<BE>()
656            .context("Failed to read uncompressed length from blob file")?;
657        let expected_checksum = reader.read_u32::<BE>()?;
658
659        // Verify checksum on the compressed on-disk data before decompression.
660        let actual_checksum = checksum_block(reader);
661        if actual_checksum != expected_checksum {
662            bail!(
663                "Cache corruption detected: checksum mismatch in blob file {:08}.blob (expected \
664                 {:08x}, got {:08x})",
665                seq,
666                expected_checksum,
667                actual_checksum
668            );
669        }
670
671        let buffer = decompress_into_arc(uncompressed_length, reader)?;
672        Ok(ArcBytes::from(buffer))
673    }
674
675    /// Returns true if the database is empty.
676    pub fn is_empty(&self) -> bool {
677        self.is_empty.load(Ordering::Relaxed)
678    }
679
680    /// Returns `true` if a previous write or compaction left the database in an unrecoverable error
681    /// state, permanently disabling further writes.
682    pub fn has_unrecoverable_write_error(&self) -> bool {
683        matches!(
684            *self.active_write_operation.lock(),
685            Some(ActiveWriteState::Error)
686        )
687    }
688
689    /// Acquires the write-operation slot, returning an RAII guard that rolls back and releases it
690    /// on drop. Only one write operation (write batch or compaction) is allowed at a time.
691    /// `name` is a short human-readable label used in error messages (e.g. `"write batch"`).
692    fn acquire_write_operation(&self, name: &'static str) -> Result<WriteOperationGuard<'_>> {
693        if self.read_only {
694            bail!("Cannot perform write operations on a read-only database");
695        }
696        let mut slot = self.active_write_operation.lock();
697        match &*slot {
698            Some(ActiveWriteState::Active(active_name)) => {
699                bail!(
700                    "Another {active_name} is already active (only a single write operation is \
701                     allowed at a time)"
702                );
703            }
704            Some(ActiveWriteState::Error) => {
705                bail!(
706                    "A previous write operation failed with an unrecoverable error; no further \
707                     writes are possible"
708                );
709            }
710            None => {}
711        }
712        *slot = Some(ActiveWriteState::Active(name));
713        drop(slot); // release before acquiring inner read lock
714        let seq_before = self.inner.read().current_sequence_number;
715        Ok(WriteOperationGuard {
716            active: &self.active_write_operation,
717            path: &self.path,
718            seq_before,
719            succeeded: false,
720        })
721    }
722
723    /// Starts a new WriteBatch for the database. Only a single write operation is allowed at a
724    /// time. The WriteBatch need to be committed with [`TurboPersistence::commit_write_batch`].
725    /// Note that the WriteBatch might start writing data to disk while it's filled up with data.
726    /// This data will only become visible after the WriteBatch is committed.
727    pub fn write_batch<K: StoreKey + Send + Sync>(&self) -> Result<WriteBatch<'_, K, S, FAMILIES>> {
728        let guard = self.acquire_write_operation("write batch")?;
729        // seq_before is already the current sequence number, no second read needed.
730        let current = guard.seq_before;
731        Ok(WriteBatch::new(
732            guard,
733            self.path.clone(),
734            current,
735            self.parallel_scheduler.clone(),
736            self.config.family_configs,
737        ))
738    }
739
740    fn key_block_cache(&self) -> &BlockCache {
741        self.key_block_cache.get_or_init(|| {
742            BlockCache::with(
743                KEY_BLOCK_CACHE_SIZE as usize / KEY_BLOCK_AVG_SIZE,
744                KEY_BLOCK_CACHE_SIZE,
745                Default::default(),
746                Default::default(),
747                Default::default(),
748            )
749        })
750    }
751
752    fn value_block_cache(&self) -> &BlockCache {
753        self.value_block_cache.get_or_init(|| {
754            BlockCache::with(
755                VALUE_BLOCK_CACHE_SIZE as usize / VALUE_BLOCK_AVG_SIZE,
756                VALUE_BLOCK_CACHE_SIZE,
757                Default::default(),
758                Default::default(),
759                Default::default(),
760            )
761        })
762    }
763
764    /// Clears all caches of the database.
765    pub fn clear_cache(&self) {
766        self.clear_block_caches();
767        for meta in self.inner.write().meta_files.iter_mut() {
768            meta.clear_cache();
769        }
770    }
771
772    /// Clears block caches of the database. Caches that have not been allocated yet are left
773    /// uninitialized, so clearing never forces allocation.
774    pub fn clear_block_caches(&self) {
775        if let Some(cache) = self.key_block_cache.get() {
776            cache.clear();
777        }
778        if let Some(cache) = self.value_block_cache.get() {
779            cache.clear();
780        }
781    }
782
783    /// Prefetches all SST files which are usually lazy loaded. This can be used to reduce latency
784    /// for the first queries after opening the database.
785    pub fn prepare_all_sst_caches(&self) {
786        for meta in self.inner.write().meta_files.iter_mut() {
787            meta.prepare_sst_cache();
788        }
789    }
790
791    fn open_log(&self) -> Result<BufWriter<File>> {
792        if self.read_only {
793            unreachable!("Only write operations can open the log file");
794        }
795        let log_path = self.path.join("LOG");
796        let log_file = OpenOptions::new()
797            .create(true)
798            .append(true)
799            .open(log_path)?;
800        Ok(BufWriter::new(log_file))
801    }
802
803    /// Commits a WriteBatch to the database. This will finish writing the data to disk and make it
804    /// visible to readers.
805    pub fn commit_write_batch<K: StoreKey + Send + Sync>(
806        &self,
807        mut write_batch: WriteBatch<'_, K, S, FAMILIES>,
808    ) -> Result<CommitStats> {
809        if self.read_only {
810            unreachable!("It's not possible to create a write batch for a read-only database");
811        }
812        let FinishResult {
813            sequence_number,
814            new_meta_files,
815            new_sst_files,
816            new_blob_files,
817            keys_written,
818        } = write_batch.finish(|family| {
819            let inner = self.inner.read();
820            let set = &inner.accessed_key_hashes[family as usize];
821            // len is only a snapshot at that time and it can change while we create the filter.
822            // So we give it 5% more space to make resizes less likely.
823            let initial_capacity = set.len() * 20 / 19;
824            // TODO: Using u64::BITS as fingerprint size is wasteful for a
825            // probabilistic membership filter. A smaller fingerprint (e.g. via
826            // Filter::new with a target fp_rate) would significantly reduce size,
827            // but would make merging slower since mismatched fingerprint sizes
828            // fall back to one-by-one insertion instead of sorted merge.
829            let mut amqf =
830                qfilter::Filter::with_fingerprint_size(initial_capacity as u64, u64::BITS as u8)
831                    .unwrap();
832            // This drains items from the set. But due to concurrency it might not be empty
833            // afterwards, but that's fine. It will be part of the next commit.
834            set.retain(|hash| {
835                // Performance-wise it would usually be better to insert sorted fingerprints, but we
836                // assume that hashes are equally distributed, which makes it unnecessary.
837                // Good for cache locality is that we insert in the order of the dashset's buckets.
838                amqf.insert_fingerprint(false, *hash)
839                    .expect("Failed to insert fingerprint");
840                false
841            });
842            amqf
843        })?;
844        let stats = self.commit(CommitOptions {
845            new_meta_files,
846            new_sst_files,
847            new_blob_files,
848            sst_files_to_delete: vec![],
849            blob_seq_numbers_to_delete: vec![],
850            sequence_number,
851            keys_written,
852        })?;
853        // Mark the guard inside the write batch as succeeded so it skips the rollback on drop.
854        write_batch.mark_succeeded();
855        Ok(stats)
856    }
857
858    /// fsyncs the new files and updates the CURRENT file. Updates the database state to include the
859    /// new files.
860    fn commit(
861        &self,
862        CommitOptions {
863            mut new_meta_files,
864            new_sst_files,
865            new_blob_files,
866            sst_files_to_delete,
867            mut blob_seq_numbers_to_delete,
868            sequence_number: mut seq,
869            keys_written,
870        }: CommitOptions,
871    ) -> Result<CommitStats, anyhow::Error> {
872        let time = Timestamp::now();
873
874        new_meta_files.sort_unstable_by_key(|f| f.seq);
875
876        let mut stats = CommitStats::default();
877
878        let sync_span = tracing::trace_span!("sync new files").entered();
879
880        enum SyncItem {
881            Meta(u32, File),
882            Sst(File),
883            Blob(u32, File),
884        }
885        enum SyncResult {
886            Meta(MetaFile),
887            Sst,
888            Blob(u32, File),
889        }
890
891        let mut sync_items: Vec<SyncItem> =
892            Vec::with_capacity(new_meta_files.len() + new_sst_files.len() + new_blob_files.len());
893        for NewFile { seq, file, size } in new_meta_files {
894            stats.bytes_written += size;
895            sync_items.push(SyncItem::Meta(seq, file));
896        }
897        for NewFile { file, size, .. } in new_sst_files {
898            stats.bytes_written += size;
899            sync_items.push(SyncItem::Sst(file));
900        }
901        for NewFile { seq, file, size } in new_blob_files {
902            stats.bytes_written += size;
903            sync_items.push(SyncItem::Blob(seq, file));
904        }
905
906        let results: Vec<SyncResult> = self
907            .parallel_scheduler
908            .parallel_map_collect_owned::<_, _, Result<Vec<_>>>(sync_items, |item| match item {
909                SyncItem::Meta(seq, file) => {
910                    file.sync_data()?;
911                    let meta_file = MetaFile::open(&self.path, seq)?;
912                    Ok(SyncResult::Meta(meta_file))
913                }
914                SyncItem::Sst(file) => {
915                    file.sync_data()?;
916                    Ok(SyncResult::Sst)
917                }
918                SyncItem::Blob(seq, file) => {
919                    file.sync_data()?;
920                    Ok(SyncResult::Blob(seq, file))
921                }
922            })?;
923
924        let mut new_meta_files: Vec<MetaFile> = Vec::new();
925        let mut new_blob_files: Vec<(u32, File)> = Vec::new();
926        for result in results {
927            match result {
928                SyncResult::Meta(mf) => new_meta_files.push(mf),
929                SyncResult::Sst => {}
930                SyncResult::Blob(seq, file) => new_blob_files.push((seq, file)),
931            }
932        }
933
934        let mut sst_filter = SstFilter::new();
935        for meta_file in new_meta_files.iter_mut().rev() {
936            sst_filter.apply_filter(meta_file);
937        }
938
939        // Note: the file *contents* were made durable by the `sync_data()` calls above. The
940        // directory entries (file name → inode mappings) are made durable by the single directory
941        // fsync inside `commit_current` below, which also commits the CURRENT rename. See
942        // `commit_current` for why one trailing fsync is sufficient.
943        drop(sync_span);
944
945        let new_meta_info = new_meta_files
946            .iter()
947            .map(|meta| {
948                let ssts = meta
949                    .entries()
950                    .iter()
951                    .map(|entry| {
952                        let seq = entry.sequence_number();
953                        let range = entry.range();
954                        let size = entry.size();
955                        let flags = entry.flags();
956                        (seq, range.min_hash, range.max_hash, size, flags)
957                    })
958                    .collect::<Vec<_>>();
959                (
960                    meta.sequence_number(),
961                    meta.family(),
962                    ssts,
963                    meta.obsolete_sst_files().to_vec(),
964                )
965            })
966            .collect::<Vec<_>>();
967
968        // ── Phase A: compute what will change without modifying inner. ──
969        //
970        // We need `meta_seq_numbers_to_delete` and `has_delete_file` to write
971        // the .del file BEFORE writing CURRENT. We must not modify `inner` at
972        // all — if a disk error occurs before CURRENT is durable, the
973        // WriteOperationGuard rollback can only clean up orphan files, not undo
974        // in-memory mutations. The MetaFile in-memory optimization
975        // (retain_entries) is deferred to Phase C.
976        let has_delete_file;
977        let mut meta_seq_numbers_to_delete = Vec::new();
978        let entries_to_remove;
979        // Deleted SST bytes: the caller knows each deleted SST's size when it decides to delete it,
980        // so it's carried on `DeletedFile` and summed here (no scan, no stat).
981        stats.bytes_deleted += sst_files_to_delete.iter().map(|f| f.size).sum::<u64>();
982        // The rest of the commit only needs the sequence numbers of the deleted SSTs.
983        let mut sst_seq_numbers_to_delete = sst_files_to_delete
984            .iter()
985            .map(|f| f.seq)
986            .collect::<Vec<_>>();
987
988        {
989            let inner = self.inner.read();
990
991            // (A1) Run the SST filter on existing meta files. This only
992            // updates the SstFilter state — the MetaFile in-memory layout is
993            // not modified yet (that happens in Phase C via retain_entries).
994            // Collects the set of SST entry sequence numbers to remove from
995            // each meta file, keyed by position in `inner.meta_files`.
996            entries_to_remove = inner
997                .meta_files
998                .iter()
999                .rev()
1000                .map(|meta_file| sst_filter.apply_filter_collect(meta_file))
1001                .collect::<Vec<_>>();
1002
1003            // (A2) Determine which meta files are fully obsolete by running
1004            // `apply_and_get_remove` in newest-first order. Process new metas
1005            // first (they are newer than existing ones) to advance the filter
1006            // state, then existing ones. New metas are never candidates for
1007            // removal (just created), so only their filter-state side-effects
1008            // matter.
1009            for meta_file in new_meta_files.iter().rev() {
1010                let should_remove = sst_filter.apply_and_get_remove(meta_file);
1011                debug_assert!(
1012                    !should_remove,
1013                    "newly created meta file should never be a candidate for removal"
1014                );
1015            }
1016            for i in (0..inner.meta_files.len()).rev() {
1017                if sst_filter.apply_and_get_remove(&inner.meta_files[i]) {
1018                    meta_seq_numbers_to_delete.push(inner.meta_files[i].sequence_number());
1019                    // Deleted meta bytes, read from the `MetaFile`'s mmap length (no stat).
1020                    stats.bytes_deleted += inner.meta_files[i].byte_size();
1021                }
1022            }
1023
1024            // (A3) Compute the final sequence number that will be written to
1025            // CURRENT. A .del file is created only when there are files to
1026            // delete, which consumes one extra sequence number.
1027            has_delete_file = !sst_files_to_delete.is_empty()
1028                || !blob_seq_numbers_to_delete.is_empty()
1029                || !meta_seq_numbers_to_delete.is_empty();
1030        }
1031
1032        // Deleted blob bytes. Unlike SST/meta sizes (both already in memory), blob sizes aren't
1033        // tracked, so we stat them by sequence number before Phase C unlinks them. Best-effort: a
1034        // file already gone reports 0 rather than failing the commit (these stats are reported, not
1035        // load-bearing). Left serial rather than dispatched to the scheduler: blob deletions are
1036        // rare and few, so the fan-out overhead would outweigh a handful of `stat` calls.
1037        stats.bytes_deleted += blob_seq_numbers_to_delete
1038            .iter()
1039            .map(|seq| {
1040                fs::metadata(self.path.join(format!("{seq:08}.blob")))
1041                    .map(|m| m.len())
1042                    .unwrap_or(0)
1043            })
1044            .sum::<u64>();
1045
1046        if has_delete_file {
1047            seq += 1;
1048        }
1049
1050        self.parallel_scheduler.block_in_place(|| {
1051            if has_delete_file {
1052                sst_seq_numbers_to_delete.sort_unstable();
1053                meta_seq_numbers_to_delete.sort_unstable();
1054                blob_seq_numbers_to_delete.sort_unstable();
1055                // Write *.del file, marking the selected files as to delete
1056                let mut buf = Vec::with_capacity(
1057                    (sst_seq_numbers_to_delete.len()
1058                        + meta_seq_numbers_to_delete.len()
1059                        + blob_seq_numbers_to_delete.len())
1060                        * size_of::<u32>(),
1061                );
1062                for seq in sst_seq_numbers_to_delete.iter() {
1063                    buf.write_u32::<BE>(*seq)?;
1064                }
1065                for seq in meta_seq_numbers_to_delete.iter() {
1066                    buf.write_u32::<BE>(*seq)?;
1067                }
1068                for seq in blob_seq_numbers_to_delete.iter() {
1069                    buf.write_u32::<BE>(*seq)?;
1070                }
1071                let del_path = self.path.join(format!("{seq:08}.del"));
1072                let mut file = File::create(&del_path)?;
1073                file.write_all(&buf)?;
1074                file.sync_data()?;
1075            }
1076
1077            commit_current(&self.path, seq).context("Committing CURRENT file failed")?;
1078
1079            // ── Point of no return ──────────────────────────────────────────
1080            //
1081            // CURRENT has been durably updated. The commit is now visible to
1082            // future readers (including after a crash/restart via
1083            // `load_directory`). Everything below is best-effort cleanup:
1084            //
1085            // • Writing the LOG is purely informational.
1086            //
1087            // • Superseded files are NOT deleted here — Phase C handles that
1088            //   after `inner` is updated. On Linux/macOS they are deleted
1089            //   immediately; on Windows (where open memory maps prevent
1090            //   deletion) they are retried on the next commit or shutdown.
1091            //
1092            // Errors here must NOT propagate, because the WriteOperationGuard
1093            // would then run its rollback and delete the *newly committed*
1094            // files, corrupting the database.
1095
1096            if let Err(e) = (|| {
1097                let mut log = self.open_log()?;
1098                writeln!(log, "Time {time}")?;
1099                let span = time.until(Timestamp::now())?;
1100                writeln!(log, "Commit {seq:08} {keys_written} keys in {span:#}")?;
1101                writeln!(log, "FAM | META SEQ | SST SEQ         | RANGE")?;
1102                for (meta_seq, family, ssts, obsolete) in new_meta_info {
1103                    for (seq, min, max, size, flags) in ssts {
1104                        writeln!(
1105                            log,
1106                            "{family:3} | {meta_seq:08} | {seq:08} SST    | {} ({} MiB, {})",
1107                            range_to_str(min, max),
1108                            size / 1024 / 1024,
1109                            flags
1110                        )?;
1111                    }
1112                    for obsolete in obsolete.chunks(15) {
1113                        write!(log, "{family:3} | {meta_seq:08} |")?;
1114                        for seq in obsolete {
1115                            write!(log, " {seq:08}")?;
1116                        }
1117                        writeln!(log, " OBSOLETE SST")?;
1118                    }
1119                }
1120
1121                fn write_seq_numbers<W: std::io::Write, T>(
1122                    log: &mut W,
1123                    items: &[T],
1124                    label: &str,
1125                    extract_seq: fn(&T) -> u32,
1126                ) -> std::io::Result<()> {
1127                    for chunk in items.chunks(15) {
1128                        write!(log, "    |          |")?;
1129                        for item in chunk {
1130                            write!(log, " {:08}", extract_seq(item))?;
1131                        }
1132                        writeln!(log, " {}", label)?;
1133                    }
1134                    Ok(())
1135                }
1136
1137                new_blob_files.sort_unstable_by_key(|(seq, _)| *seq);
1138                write_seq_numbers(&mut log, &new_blob_files, "NEW BLOB", |&(seq, _)| seq)?;
1139                write_seq_numbers(
1140                    &mut log,
1141                    &blob_seq_numbers_to_delete,
1142                    "BLOB DELETED",
1143                    |&seq| seq,
1144                )?;
1145                write_seq_numbers(
1146                    &mut log,
1147                    &sst_seq_numbers_to_delete,
1148                    "SST DELETED",
1149                    |&seq| seq,
1150                )?;
1151                write_seq_numbers(
1152                    &mut log,
1153                    &meta_seq_numbers_to_delete,
1154                    "META DELETED",
1155                    |&seq| seq,
1156                )?;
1157                anyhow::Ok(())
1158            })() {
1159                eprintln!("turbo-persistence: failed to write LOG after commit {seq:08}: {e:#}");
1160            }
1161
1162            anyhow::Ok(())
1163        })?;
1164
1165        // ── Phase C: structurally update inner (CURRENT is already durable). ──
1166        //
1167        // Between Phase A's read-lock drop and this point no other writer can
1168        // run (WriteOperationGuard ensures exclusivity) and readers never mutate
1169        // inner, so the snapshot from Phase A is still valid.
1170        {
1171            let mut inner = self.inner.write();
1172
1173            // Apply the deferred MetaFile mutations from Phase A1. apply_filter
1174            // was called read-only earlier; now we actually move superseded
1175            // entries from active to obsolete inside each MetaFile.
1176            // entries_to_remove was collected in reverse order, so iterate it
1177            // in reverse to match the forward order of inner.meta_files.
1178            for (meta_file, to_remove) in inner
1179                .meta_files
1180                .iter_mut()
1181                .zip(entries_to_remove.into_iter().rev())
1182            {
1183                if !to_remove.is_empty() {
1184                    meta_file.retain_entries(|seq| !to_remove.contains(&seq));
1185                }
1186            }
1187
1188            inner.meta_files.append(&mut new_meta_files);
1189            if !meta_seq_numbers_to_delete.is_empty() {
1190                let to_delete: HashSet<u32> = meta_seq_numbers_to_delete.iter().copied().collect();
1191                inner
1192                    .meta_files
1193                    .retain(|meta| !to_delete.contains(&meta.sequence_number()));
1194            }
1195            inner.current_sequence_number = seq;
1196            self.is_empty
1197                .store(inner.meta_files.is_empty(), Ordering::Relaxed);
1198        }
1199
1200        // Try to delete superseded files immediately. On Linux/macOS this always
1201        // works even if readers have the files memory-mapped. On Windows, open
1202        // memory maps prevent deletion; any file that fails is kept in
1203        // `deferred_deletions` and retried on the next commit or at shutdown.
1204        self.deferred_deletions.lock().extend(
1205            Self::try_delete_files(&self.path, &sst_seq_numbers_to_delete, "sst")
1206                .map(DeferredDeletion::Sst)
1207                .chain(
1208                    Self::try_delete_files(&self.path, &meta_seq_numbers_to_delete, "meta")
1209                        .map(DeferredDeletion::Meta),
1210                )
1211                .chain(
1212                    Self::try_delete_files(&self.path, &blob_seq_numbers_to_delete, "blob")
1213                        .map(DeferredDeletion::Blob),
1214                ),
1215        );
1216
1217        // Retry any deletions that failed in earlier commits.
1218        self.retry_deferred_deletions();
1219
1220        // Best-effort verbose log of the new database state after Phase C.
1221        #[cfg(feature = "verbose_log")]
1222        {
1223            let _: Result<(), _> = (|| -> anyhow::Result<()> {
1224                let mut log = self.open_log()?;
1225                writeln!(log, "New database state:")?;
1226                writeln!(log, "FAM | META SEQ | SST SEQ  FLAGS | RANGE")?;
1227                let inner = self.inner.read();
1228                let families = inner.meta_files.iter().map(|meta| meta.family()).filter({
1229                    let mut set = HashSet::new();
1230                    move |family| set.insert(*family)
1231                });
1232                for family in families {
1233                    for meta in inner.meta_files.iter() {
1234                        if meta.family() != family {
1235                            continue;
1236                        }
1237                        let meta_seq = meta.sequence_number();
1238                        for entry in meta.entries().iter() {
1239                            let seq = entry.sequence_number();
1240                            let range = entry.range();
1241                            writeln!(
1242                                log,
1243                                "{family:3} | {meta_seq:08} | {seq:08} {:>6} | {}",
1244                                entry.flags(),
1245                                range_to_str(range.min_hash, range.max_hash)
1246                            )?;
1247                        }
1248                    }
1249                }
1250                Ok(())
1251            })();
1252        }
1253
1254        Ok(stats)
1255    }
1256
1257    /// Runs a full compaction on the database. This will rewrite all SST files, removing all
1258    /// duplicate keys and separating all key ranges into unique files.
1259    pub fn full_compact(&self) -> Result<()> {
1260        self.compact(&CompactConfig {
1261            min_merge_count: 2,
1262            optimal_merge_count: usize::MAX,
1263            max_merge_count: usize::MAX,
1264            max_merge_bytes: u64::MAX,
1265            min_merge_duplication_bytes: 0,
1266            optimal_merge_duplication_bytes: u64::MAX,
1267            max_merge_segment_count: usize::MAX,
1268        })?;
1269        Ok(())
1270    }
1271
1272    /// Runs a (partial) compaction. Compaction will only be performed if the coverage of the SST
1273    /// files is above the given threshold. The coverage is the average number of SST files that
1274    /// need to be read to find a key. It also limits the maximum number of SST files that are
1275    /// merged at once, which is the main factor for the runtime of the compaction.
1276    ///
1277    /// Returns `Some(stats)` describing the bytes written/deleted if a compaction commit happened,
1278    /// or `None` if there was nothing to compact.
1279    pub fn compact(&self, compact_config: &CompactConfig) -> Result<Option<CommitStats>> {
1280        let mut guard = self.acquire_write_operation("compaction")?;
1281
1282        // Free block caches and SST mmaps before compaction. The block caches
1283        // are not used during compaction (we iterate uncached), and any cached
1284        // SST mmaps would use MADV_RANDOM which is wrong for sequential scans.
1285        // Clearing them upfront frees memory for the merge work.
1286        self.clear_cache();
1287
1288        let mut sequence_number;
1289        let mut new_meta_files = Vec::new();
1290        let mut new_sst_files = Vec::new();
1291        let mut sst_files_to_delete = Vec::new();
1292        let mut blob_seq_numbers_to_delete = Vec::new();
1293        let mut keys_written = 0;
1294
1295        {
1296            let inner = self.inner.read();
1297            sequence_number = AtomicU32::new(inner.current_sequence_number);
1298            self.compact_internal(
1299                &inner.meta_files,
1300                &sequence_number,
1301                &mut new_meta_files,
1302                &mut new_sst_files,
1303                &mut sst_files_to_delete,
1304                &mut blob_seq_numbers_to_delete,
1305                &mut keys_written,
1306                compact_config,
1307            )
1308            .context("Failed to compact database")?;
1309        }
1310
1311        let has_changes = !new_meta_files.is_empty();
1312        let stats = if has_changes {
1313            let stats = self
1314                .commit(CommitOptions {
1315                    new_meta_files,
1316                    new_sst_files,
1317                    new_blob_files: Vec::new(),
1318                    sst_files_to_delete,
1319                    blob_seq_numbers_to_delete,
1320                    sequence_number: *sequence_number.get_mut(),
1321                    keys_written,
1322                })
1323                .context("Failed to commit the database compaction")?;
1324            Some(stats)
1325        } else {
1326            None
1327        };
1328
1329        guard.success();
1330        Ok(stats)
1331    }
1332
1333    /// Internal function to perform a compaction.
1334    fn compact_internal(
1335        &self,
1336        meta_files: &[MetaFile],
1337        sequence_number: &AtomicU32,
1338        new_meta_files: &mut Vec<NewFile>,
1339        new_sst_files: &mut Vec<NewFile>,
1340        sst_files_to_delete: &mut Vec<DeletedFile>,
1341        blob_seq_numbers_to_delete: &mut Vec<u32>,
1342        keys_written: &mut u64,
1343        compact_config: &CompactConfig,
1344    ) -> Result<()> {
1345        if meta_files.is_empty() {
1346            return Ok(());
1347        }
1348
1349        struct SstWithRange {
1350            meta_index: usize,
1351            index_in_meta: u32,
1352            seq: u32,
1353            range: StaticSortedFileRange,
1354            size: u64,
1355            flags: MetaEntryFlags,
1356        }
1357
1358        impl Compactable for SstWithRange {
1359            fn range(&self) -> RangeInclusive<u64> {
1360                self.range.min_hash..=self.range.max_hash
1361            }
1362
1363            fn size(&self) -> u64 {
1364                self.size
1365            }
1366
1367            fn category(&self) -> u8 {
1368                // Cold and non-cold files are placed separately so we pass different category
1369                // values to ensure they are not merged together.
1370                if self.flags.cold() { 1 } else { 0 }
1371            }
1372        }
1373
1374        let ssts_with_ranges = meta_files
1375            .iter()
1376            .enumerate()
1377            .flat_map(|(meta_index, meta)| {
1378                meta.entries()
1379                    .iter()
1380                    .enumerate()
1381                    .map(move |(index_in_meta, entry)| SstWithRange {
1382                        meta_index,
1383                        index_in_meta: index_in_meta as u32,
1384                        seq: entry.sequence_number(),
1385                        range: entry.range(),
1386                        size: entry.size(),
1387                        flags: entry.flags(),
1388                    })
1389            })
1390            .collect::<Vec<_>>();
1391
1392        let mut sst_by_family = [(); FAMILIES].map(|_| Vec::new());
1393
1394        for sst in ssts_with_ranges {
1395            sst_by_family[sst.range.family as usize].push(sst);
1396        }
1397
1398        let path = &self.path;
1399
1400        let log_mutex = Mutex::new(());
1401
1402        struct PartialResultPerFamily {
1403            new_meta_file: Option<NewFile>,
1404            new_sst_files: Vec<NewFile>,
1405            sst_files_to_delete: Vec<DeletedFile>,
1406            blob_seq_numbers_to_delete: Vec<u32>,
1407            keys_written: u64,
1408        }
1409
1410        let mut compact_config = compact_config.clone();
1411        let merge_jobs = sst_by_family
1412            .into_iter()
1413            .enumerate()
1414            .filter_map(|(family, ssts_with_ranges)| {
1415                if compact_config.max_merge_segment_count == 0 {
1416                    return None;
1417                }
1418                let (merge_jobs, real_merge_job_size) =
1419                    get_merge_segments(&ssts_with_ranges, &compact_config);
1420                compact_config.max_merge_segment_count -= real_merge_job_size;
1421                Some((family, ssts_with_ranges, merge_jobs))
1422            })
1423            .collect::<Vec<_>>();
1424
1425        let result = self
1426            .parallel_scheduler
1427            .parallel_map_collect_owned::<_, _, Result<Vec<_>>>(
1428                merge_jobs,
1429                |(family, ssts_with_ranges, merge_jobs)| {
1430                    let family = family as u32;
1431
1432                    if merge_jobs.is_empty() {
1433                        return Ok(PartialResultPerFamily {
1434                            new_meta_file: None,
1435                            new_sst_files: Vec::new(),
1436                            sst_files_to_delete: Vec::new(),
1437                            blob_seq_numbers_to_delete: Vec::new(),
1438                            keys_written: 0,
1439                        });
1440                    }
1441
1442                    // Deserialize and merge used key hash filters per-family into
1443                    // a single filter. This avoids O(entries × N) filter probes
1444                    // during the merge loop. Empty filters (from commits with no
1445                    // reads) are discarded.
1446                    let used_key_hashes: Option<qfilter::Filter> = {
1447                        let filters: Vec<qfilter::FilterRef<'_>> = meta_files
1448                            .iter()
1449                            .filter(|m| m.family() == family)
1450                            .filter_map(|meta_file| {
1451                                meta_file.deserialize_used_key_hashes_amqf().transpose()
1452                            })
1453                            .collect::<Result<Vec<_>>>()?
1454                            .into_iter()
1455                            .filter(|amqf| !amqf.is_empty())
1456                            .collect();
1457                        if filters.is_empty() {
1458                            None
1459                        } else if filters.len() == 1 {
1460                            // Just directly use the single item
1461                            Some(filters[0].to_owned())
1462                        } else {
1463                            let total_len: u64 = filters.iter().map(|f| f.len()).sum();
1464                            // Fingerprint size must match the source filters to
1465                            // enable the efficient sorted merge path in qfilter.
1466                            let mut merged =
1467                                qfilter::Filter::with_fingerprint_size(total_len, u64::BITS as u8)
1468                                    .expect("Failed to create merged AMQF filter");
1469                            for filter in &filters {
1470                                merged
1471                                    .merge(false, filter)
1472                                    .expect("Failed to merge AMQF filters");
1473                            }
1474                            merged.shrink_to_fit();
1475                            Some(merged)
1476                        }
1477                    };
1478
1479                    // Later we will remove the merged files. Capture each one's size now (we know
1480                    // exactly which SST it is) so `commit` can report deleted bytes without a scan.
1481                    let sst_files_to_delete = merge_jobs
1482                        .iter()
1483                        .filter(|l| l.len() > 1)
1484                        .flat_map(|l| l.iter().copied())
1485                        .map(|index| DeletedFile {
1486                            seq: ssts_with_ranges[index].seq,
1487                            size: ssts_with_ranges[index].size,
1488                        })
1489                        .collect::<Vec<_>>();
1490
1491                    // Merge SST files
1492                    let span = tracing::trace_span!(
1493                        "merge files",
1494                        family = self.config.family_configs[family as usize].name
1495                    );
1496                    enum PartialMergeResult<'l> {
1497                        Merged {
1498                            new_sst_files: Vec<(u32, File, StaticSortedFileBuilderMeta<'static>)>,
1499                            blob_seq_numbers_to_delete: Vec<u32>,
1500                            keys_written: u64,
1501                            indices: SmallVec<[usize; 1]>,
1502                        },
1503                        Move {
1504                            seq: u32,
1505                            meta: StaticSortedFileBuilderMeta<'l>,
1506                        },
1507                    }
1508                    let merge_result = self
1509                        .parallel_scheduler
1510                        .parallel_map_collect_owned::<_, _, Result<Vec<_>>>(merge_jobs, |indices| {
1511                            let _span = span.clone().entered();
1512                            if indices.len() == 1 {
1513                                // If we only have one file, we can just move it
1514                                let index = indices[0];
1515                                let meta_index = ssts_with_ranges[index].meta_index;
1516                                let index_in_meta = ssts_with_ranges[index].index_in_meta;
1517                                let meta_file = &meta_files[meta_index];
1518                                let entry = meta_file.entry(index_in_meta);
1519                                let amqf = Cow::Borrowed(entry.raw_amqf(meta_file.amqf_data()));
1520                                let meta = StaticSortedFileBuilderMeta {
1521                                    min_hash: entry.min_hash(),
1522                                    max_hash: entry.max_hash(),
1523                                    amqf,
1524                                    block_count: entry.block_count(),
1525                                    size: entry.size(),
1526                                    flags: entry.flags(),
1527                                    entries: 0,
1528                                };
1529                                return Ok(PartialMergeResult::Move {
1530                                    seq: entry.sequence_number(),
1531                                    meta,
1532                                });
1533                            }
1534
1535                            // Open SST files independently for compaction.
1536                            // Uses MADV_SEQUENTIAL for better OS page management
1537                            // and avoids caching mmaps on MetaEntry's OnceLock.
1538                            let iters = indices
1539                                .iter()
1540                                .map(|&index| {
1541                                    let meta_index = ssts_with_ranges[index].meta_index;
1542                                    let index_in_meta = ssts_with_ranges[index].index_in_meta;
1543                                    let entry = meta_files[meta_index].entry(index_in_meta);
1544                                    StaticSortedFileIter::open(path, entry.sst_metadata())
1545                                })
1546                                .collect::<Result<Vec<_>>>()?;
1547
1548                            let iter = MergeIter::new(iters.into_iter())?;
1549
1550                            let mut blob_seq_numbers_to_delete: Vec<u32> = Vec::new();
1551
1552                            struct Collector {
1553                                /// The active writer and its sequence number. `None` if no
1554                                /// entries have been added since the last flush. We defer
1555                                /// allocation to avoid creating empty SST files for collectors
1556                                /// that receive no entries (e.g., the unused_collector when
1557                                /// all keys are in the
1558                                /// used set).
1559                                writer: Option<(u32, StreamingSstWriter<LookupEntry>)>,
1560                                flags: MetaEntryFlags,
1561                                new_sst_files:
1562                                    Vec<(u32, File, StaticSortedFileBuilderMeta<'static>)>,
1563                                /// Hash of the last key added. Used to ensure we only split
1564                                /// SST files at key boundaries (not mid-key-group for MultiValue).
1565                                last_hash: Option<u64>,
1566                            }
1567                            impl Collector {
1568                                fn new(flags: MetaEntryFlags) -> Self {
1569                                    Self {
1570                                        writer: None,
1571                                        flags,
1572                                        new_sst_files: Vec::new(),
1573                                        last_hash: None,
1574                                    }
1575                                }
1576
1577                                /// Ensures a writer is open, creating one if needed.
1578                                fn ensure_writer(
1579                                    &mut self,
1580                                    path: &Path,
1581                                    sequence_number: &AtomicU32,
1582                                ) -> Result<&mut StreamingSstWriter<LookupEntry>>
1583                                {
1584                                    if self.writer.is_none() {
1585                                        let seq =
1586                                            sequence_number.fetch_add(1, Ordering::SeqCst) + 1;
1587                                        let sst_path = path.join(format!("{seq:08}.sst"));
1588                                        let writer = StreamingSstWriter::new(
1589                                            &sst_path,
1590                                            self.flags,
1591                                            MAX_ENTRIES_PER_COMPACTED_FILE as u64,
1592                                        )?;
1593                                        self.writer = Some((seq, writer));
1594                                    }
1595                                    Ok(&mut self.writer.as_mut().unwrap().1)
1596                                }
1597
1598                                /// Closes the current SST file (flushing remaining blocks and
1599                                /// writing the index) and records it in the completed files
1600                                /// list.
1601                                fn close_sst_file(&mut self, keys_written: &mut u64) -> Result<()> {
1602                                    if let Some((seq, writer)) = self.writer.take() {
1603                                        let _span =
1604                                            tracing::trace_span!("close merged sst file").entered();
1605                                        let (meta, file) = writer.close()?;
1606                                        *keys_written += meta.entries;
1607                                        self.new_sst_files.push((seq, file, meta));
1608                                    }
1609                                    Ok(())
1610                                }
1611
1612                                /// Adds an entry to the collector. Only splits the SST file at
1613                                /// key boundaries to avoid breaking key groups for MultiValue
1614                                /// families.
1615                                fn add_entry(
1616                                    &mut self,
1617                                    entry: LookupEntry,
1618                                    path: &Path,
1619                                    sequence_number: &AtomicU32,
1620                                    keys_written: &mut u64,
1621                                ) -> Result<()> {
1622                                    let key_changed = self.last_hash != Some(entry.hash);
1623                                    // Only check fullness at key boundaries to avoid splitting
1624                                    // a key group across two SST files.
1625                                    if key_changed
1626                                        && let Some((_, ref writer)) = self.writer
1627                                        && writer.is_full(
1628                                            MAX_ENTRIES_PER_COMPACTED_FILE,
1629                                            DATA_THRESHOLD_PER_COMPACTED_FILE,
1630                                        )
1631                                    {
1632                                        self.close_sst_file(keys_written)?;
1633                                    }
1634                                    self.last_hash = Some(entry.hash);
1635                                    let writer = self.ensure_writer(path, sequence_number)?;
1636                                    writer.add(entry)?;
1637                                    Ok(())
1638                                }
1639                            }
1640                            #[cfg(debug_assertions)]
1641                            impl Drop for Collector {
1642                                fn drop(&mut self) {
1643                                    if !std::thread::panicking() {
1644                                        assert!(
1645                                            self.writer.is_none(),
1646                                            "Collector dropped with an open writer"
1647                                        );
1648                                    }
1649                                }
1650                            }
1651                            let mut used_collector = Collector::new(MetaEntryFlags::WARM);
1652                            let mut unused_collector = Collector::new(MetaEntryFlags::COLD);
1653                            let mut current_key: Option<RcBytes> = None;
1654                            let mut keys_written = 0;
1655
1656                            // MergeIter yields entries from newer SSTs first (by SST sequence
1657                            // number). Within each SST, tombstones sort last within key groups.
1658                            // Use a skip flag to handle:
1659                            // - SingleValue: skip all older entries after writing the first
1660                            // - MultiValue: skip all older entries after encountering a tombstone
1661                            //   (which signals deletion of all prior values for this key)
1662                            let mut skip_remaining_for_this_key = false;
1663                            let family_config = &self.config.family_configs[family as usize];
1664
1665                            for entry in iter {
1666                                let entry = entry?;
1667                                if current_key.as_ref() != Some(&entry.key) {
1668                                    // we changed keys so undo this flag
1669                                    skip_remaining_for_this_key = false;
1670                                    current_key = Some(entry.key.clone());
1671                                }
1672                                if !skip_remaining_for_this_key {
1673                                    let is_used = used_key_hashes
1674                                        .as_ref()
1675                                        .is_some_and(|amqf| amqf.contains_fingerprint(entry.hash));
1676                                    let collector = if is_used {
1677                                        &mut used_collector
1678                                    } else {
1679                                        &mut unused_collector
1680                                    };
1681                                    match family_config.kind {
1682                                        FamilyKind::MultiValue => {
1683                                            // For MultiValue families we only skip remaining if we
1684                                            // see a tombstone
1685                                            if matches!(entry.value, IterValue::Deleted) {
1686                                                skip_remaining_for_this_key = true;
1687                                            }
1688                                        }
1689                                        FamilyKind::SingleValue => {
1690                                            // Since MergeItr is in newest to oldest order anything
1691                                            // else that comes out must be skipped
1692                                            skip_remaining_for_this_key = true;
1693                                        }
1694                                    }
1695                                    collector.add_entry(
1696                                        entry,
1697                                        path,
1698                                        sequence_number,
1699                                        &mut keys_written,
1700                                    )?;
1701                                } else {
1702                                    // Entry is being dropped (superseded by newer entry or
1703                                    // pruned by tombstone). If it references a blob file,
1704                                    // mark that blob for deletion.
1705                                    if let IterValue::Blob { sequence_number } = &entry.value {
1706                                        blob_seq_numbers_to_delete.push(*sequence_number);
1707                                    }
1708                                }
1709                            }
1710
1711                            // Close remaining writers
1712                            used_collector.close_sst_file(&mut keys_written)?;
1713                            unused_collector.close_sst_file(&mut keys_written)?;
1714
1715                            let mut new_sst_files = take(&mut unused_collector.new_sst_files);
1716                            new_sst_files.append(&mut used_collector.new_sst_files);
1717                            Ok(PartialMergeResult::Merged {
1718                                new_sst_files,
1719                                blob_seq_numbers_to_delete,
1720                                keys_written,
1721                                indices,
1722                            })
1723                        })
1724                        .with_context(|| {
1725                            format!("Failed to merge database files for family {family}")
1726                        })?;
1727
1728                    let Some((sst_files_len, blob_delete_len)) = merge_result
1729                        .iter()
1730                        .map(|r| {
1731                            if let PartialMergeResult::Merged {
1732                                new_sst_files,
1733                                blob_seq_numbers_to_delete,
1734                                indices: _,
1735                                keys_written: _,
1736                            } = r
1737                            {
1738                                (new_sst_files.len(), blob_seq_numbers_to_delete.len())
1739                            } else {
1740                                (0, 0)
1741                            }
1742                        })
1743                        .reduce(|(a1, a2), (b1, b2)| (a1 + b1, a2 + b2))
1744                    else {
1745                        unreachable!()
1746                    };
1747
1748                    let mut new_sst_files = Vec::with_capacity(sst_files_len);
1749                    let mut blob_seq_numbers_to_delete = Vec::with_capacity(blob_delete_len);
1750
1751                    let meta_seq = sequence_number.fetch_add(1, Ordering::SeqCst) + 1;
1752                    let mut meta_file_builder = MetaFileBuilder::new(family);
1753
1754                    let mut keys_written = 0;
1755                    self.parallel_scheduler.block_in_place(|| {
1756                        let guard = log_mutex.lock();
1757                        let mut log = self.open_log()?;
1758                        writeln!(log, "{family:3} | {meta_seq:08} | Compaction:",)?;
1759                        for result in merge_result {
1760                            match result {
1761                                PartialMergeResult::Merged {
1762                                    new_sst_files: merged_new_sst_files,
1763                                    blob_seq_numbers_to_delete: merged_blob_seq_numbers_to_delete,
1764                                    keys_written: merged_keys_written,
1765                                    indices,
1766                                } => {
1767                                    writeln!(
1768                                        log,
1769                                        "{family:3} | {meta_seq:08} | MERGE \
1770                                         ({merged_keys_written} keys):"
1771                                    )?;
1772                                    for i in indices.iter() {
1773                                        let seq = ssts_with_ranges[*i].seq;
1774                                        let (min, max) = ssts_with_ranges[*i].range().into_inner();
1775                                        writeln!(
1776                                            log,
1777                                            "{family:3} | {meta_seq:08} | {seq:08} INPUT  | {}",
1778                                            range_to_str(min, max)
1779                                        )?;
1780                                    }
1781                                    for (seq, file, meta) in merged_new_sst_files {
1782                                        let min = meta.min_hash;
1783                                        let max = meta.max_hash;
1784                                        writeln!(
1785                                            log,
1786                                            "{family:3} | {meta_seq:08} | {seq:08} OUTPUT | {} \
1787                                             ({})",
1788                                            range_to_str(min, max),
1789                                            meta.flags
1790                                        )?;
1791
1792                                        let size = meta.size;
1793                                        meta_file_builder.add(seq, meta);
1794                                        new_sst_files.push(NewFile { seq, file, size });
1795                                    }
1796                                    blob_seq_numbers_to_delete
1797                                        .extend(merged_blob_seq_numbers_to_delete);
1798                                    keys_written += merged_keys_written;
1799                                }
1800                                PartialMergeResult::Move { seq, meta } => {
1801                                    let min = meta.min_hash;
1802                                    let max = meta.max_hash;
1803                                    writeln!(
1804                                        log,
1805                                        "{family:3} | {meta_seq:08} | {seq:08} MOVED  | {}",
1806                                        range_to_str(min, max)
1807                                    )?;
1808
1809                                    meta_file_builder.add(seq, meta);
1810                                }
1811                            }
1812                        }
1813                        drop(log);
1814                        drop(guard);
1815
1816                        anyhow::Ok(())
1817                    })?;
1818
1819                    for f in sst_files_to_delete.iter() {
1820                        meta_file_builder.add_obsolete_sst_file(f.seq);
1821                    }
1822
1823                    let new_meta_file = {
1824                        let _span = tracing::trace_span!("write meta file").entered();
1825                        let (file, size) = self
1826                            .parallel_scheduler
1827                            .block_in_place(|| meta_file_builder.write(&self.path, meta_seq))?;
1828                        NewFile {
1829                            seq: meta_seq,
1830                            file,
1831                            size,
1832                        }
1833                    };
1834
1835                    Ok(PartialResultPerFamily {
1836                        new_meta_file: Some(new_meta_file),
1837                        new_sst_files,
1838                        sst_files_to_delete,
1839                        blob_seq_numbers_to_delete,
1840                        keys_written,
1841                    })
1842                },
1843            )?;
1844
1845        for PartialResultPerFamily {
1846            new_meta_file: inner_new_meta_file,
1847            new_sst_files: mut inner_new_sst_files,
1848            sst_files_to_delete: mut inner_sst_files_to_delete,
1849            blob_seq_numbers_to_delete: mut inner_blob_seq_numbers_to_delete,
1850            keys_written: inner_keys_written,
1851        } in result
1852        {
1853            new_meta_files.extend(inner_new_meta_file);
1854            new_sst_files.append(&mut inner_new_sst_files);
1855            sst_files_to_delete.append(&mut inner_sst_files_to_delete);
1856            blob_seq_numbers_to_delete.append(&mut inner_blob_seq_numbers_to_delete);
1857            *keys_written += inner_keys_written;
1858        }
1859
1860        Ok(())
1861    }
1862
1863    /// Get a value from the database. Returns None if the key is not found. The returned value
1864    /// might hold onto a block of the database and it should not be hold long-term.
1865    pub fn get<K: QueryKey>(&self, family: usize, key: &K) -> Result<Option<ArcBytes>> {
1866        debug_assert!(family < FAMILIES, "Family index out of bounds");
1867        if self.config.family_configs[family].kind != FamilyKind::SingleValue {
1868            // This is an error in our caller so just panic
1869            panic!(
1870                "only single valued tables can be queried with `get', call `get_multiple` instead"
1871            )
1872        }
1873        let span = tracing::trace_span!(
1874            "database read",
1875            name = self.config.family_configs[family].name,
1876            result_size = tracing::field::Empty
1877        )
1878        .entered();
1879        let results = self.get_impl::<K, false>(family, key, &span)?;
1880        debug_assert!(results.len() <= 1, "get() should return at most one result");
1881        Ok(results.into_iter().next())
1882    }
1883
1884    /// Looks up a key and returns all matching values.
1885    ///
1886    /// This is useful for keyspaces where keys are not unique and multiple mappings are possible.
1887    /// Unlike `get`, which returns only the first match, this method returns all
1888    /// entries with the same key from all SST files.  By default however we assume these
1889    /// collections are small and thus optimize for there being exactly 0 or 1 results.
1890    ///
1891    /// The order of returned values is undefined and duplicates are preserved. Callers must not
1892    /// rely on any particular ordering (neither insertion order nor byte order).
1893    pub fn get_multiple<K: QueryKey>(
1894        &self,
1895        family: usize,
1896        key: &K,
1897    ) -> Result<SmallVec<[ArcBytes; 1]>> {
1898        debug_assert!(family < FAMILIES, "Family index out of bounds");
1899        if self.config.family_configs[family].kind != FamilyKind::MultiValue {
1900            // This is an error in our caller so just panic
1901            panic!("only multi-valued tables can be queried with `get_multiple`")
1902        }
1903        let span = tracing::trace_span!(
1904            "database read multiple",
1905            name = self.config.family_configs[family].name,
1906            result_count = tracing::field::Empty,
1907            result_size = tracing::field::Empty
1908        )
1909        .entered();
1910        let results = self.get_impl::<K, true>(family, key, &span)?;
1911        Ok(results)
1912    }
1913
1914    /// Shared implementation for `get` and `get_multiple`.
1915    ///
1916    /// If `FIND_ALL` is false, stops after finding the first match.
1917    /// If `FIND_ALL` is true, continues to find all matches across all meta files.
1918    fn get_impl<K: QueryKey, const FIND_ALL: bool>(
1919        &self,
1920        family: usize,
1921        key: &K,
1922        span: &EnteredSpan,
1923    ) -> Result<SmallVec<[ArcBytes; 1]>> {
1924        let hash = hash_key(key);
1925        let inner = self.inner.read();
1926        let mut output: SmallVec<[ArcBytes; 1]> = SmallVec::new();
1927        // Track whether we found the key in any SST (even if deleted).
1928        // Used for miss_global stat: only fires if key was never found anywhere.
1929        #[cfg(feature = "stats")]
1930        let mut found_in_sst = false;
1931
1932        let mut size = 0;
1933
1934        for meta in inner.meta_files.iter().rev() {
1935            match meta.lookup::<K, FIND_ALL>(
1936                family as u32,
1937                hash,
1938                key,
1939                self.key_block_cache(),
1940                self.value_block_cache(),
1941            )? {
1942                MetaLookupResult::FamilyMiss => {
1943                    #[cfg(feature = "stats")]
1944                    self.stats.miss_family.fetch_add(1, Ordering::Relaxed);
1945                }
1946                MetaLookupResult::RangeMiss => {
1947                    #[cfg(feature = "stats")]
1948                    self.stats.miss_range.fetch_add(1, Ordering::Relaxed);
1949                }
1950                MetaLookupResult::QuickFilterMiss => {
1951                    #[cfg(feature = "stats")]
1952                    self.stats.miss_amqf.fetch_add(1, Ordering::Relaxed);
1953                }
1954                MetaLookupResult::SstLookup(result) => match result {
1955                    SstLookupResult::Found(values) => {
1956                        #[cfg(feature = "stats")]
1957                        {
1958                            found_in_sst = true;
1959                        }
1960                        inner.accessed_key_hashes[family].insert(hash);
1961                        // Process values. Tombstones sort last within a key group,
1962                        // so when we see a tombstone, we can return immediately.
1963                        for value in values {
1964                            match value {
1965                                LookupValue::Deleted => {
1966                                    #[cfg(feature = "stats")]
1967                                    self.stats.hits_deleted.fetch_add(1, Ordering::Relaxed);
1968                                    if !FIND_ALL {
1969                                        span.record("result_size", "deleted");
1970                                        return Ok(SmallVec::new());
1971                                    }
1972                                    // Tombstone is last in key group. Return accumulated
1973                                    // values (from this SST and newer layers). Stop
1974                                    // searching older SSTs.
1975                                    if output.is_empty() {
1976                                        span.record("result_size", "deleted");
1977                                    } else {
1978                                        span.record("result_size", size);
1979                                    }
1980                                    return Ok(output);
1981                                }
1982                                LookupValue::Slice { value } => {
1983                                    #[cfg(feature = "stats")]
1984                                    self.stats.hits_small.fetch_add(1, Ordering::Relaxed);
1985                                    if !FIND_ALL {
1986                                        span.record("result_size", value.len());
1987                                        return Ok(SmallVec::from_buf([value]));
1988                                    }
1989                                    size += value.len();
1990                                    output.push(value);
1991                                }
1992                                LookupValue::Blob { sequence_number } => {
1993                                    #[cfg(feature = "stats")]
1994                                    self.stats.hits_blob.fetch_add(1, Ordering::Relaxed);
1995                                    let blob = self.read_blob(sequence_number)?;
1996                                    if !FIND_ALL {
1997                                        span.record("result_size", blob.len());
1998                                        return Ok(SmallVec::from_buf([blob]));
1999                                    }
2000                                    size += blob.len();
2001                                    output.push(blob);
2002                                }
2003                            }
2004                        }
2005                    }
2006                    SstLookupResult::NotFound => {
2007                        #[cfg(feature = "stats")]
2008                        self.stats.miss_key.fetch_add(1, Ordering::Relaxed);
2009                    }
2010                },
2011            }
2012        }
2013
2014        #[cfg(feature = "stats")]
2015        if !found_in_sst {
2016            self.stats.miss_global.fetch_add(1, Ordering::Relaxed);
2017        }
2018
2019        if FIND_ALL {
2020            span.record("result_count", output.len());
2021        }
2022        if output.is_empty() {
2023            span.record("result_size", "not_found");
2024        } else {
2025            span.record("result_size", size);
2026        }
2027        Ok(output)
2028    }
2029
2030    pub fn batch_get<K: QueryKey>(
2031        &self,
2032        family: usize,
2033        keys: &[K],
2034    ) -> Result<Vec<Option<ArcBytes>>> {
2035        debug_assert!(family < FAMILIES, "Family index out of bounds");
2036        if self.config.family_configs[family].kind != FamilyKind::SingleValue {
2037            // This is an error in our caller so just panic
2038            panic!("only single valued tables can be queried with `batch_get'")
2039        }
2040        let span = tracing::trace_span!(
2041            "database batch read",
2042            name = self.config.family_configs[family].name,
2043            keys = keys.len(),
2044            not_found = tracing::field::Empty,
2045            deleted = tracing::field::Empty,
2046            result_size = tracing::field::Empty
2047        )
2048        .entered();
2049        let mut cells: Vec<(u64, usize, Option<LookupValue>)> = Vec::with_capacity(keys.len());
2050        let mut empty_cells = keys.len();
2051        for (index, key) in keys.iter().enumerate() {
2052            let hash = hash_key(key);
2053            cells.push((hash, index, None));
2054        }
2055        cells.sort_by_key(|(hash, _, _)| *hash);
2056        let inner = self.inner.read();
2057        for meta in inner.meta_files.iter().rev() {
2058            let _result = meta.batch_lookup(
2059                family as u32,
2060                keys,
2061                &mut cells,
2062                &mut empty_cells,
2063                self.key_block_cache(),
2064                self.value_block_cache(),
2065            )?;
2066
2067            #[cfg(feature = "stats")]
2068            {
2069                let crate::meta_file::MetaBatchLookupResult {
2070                    family_miss,
2071                    range_misses,
2072                    quick_filter_misses,
2073                    sst_misses,
2074                    hits: _,
2075                } = _result;
2076                if family_miss {
2077                    self.stats.miss_family.fetch_add(1, Ordering::Relaxed);
2078                }
2079                if range_misses > 0 {
2080                    self.stats
2081                        .miss_range
2082                        .fetch_add(range_misses as u64, Ordering::Relaxed);
2083                }
2084                if quick_filter_misses > 0 {
2085                    self.stats
2086                        .miss_amqf
2087                        .fetch_add(quick_filter_misses as u64, Ordering::Relaxed);
2088                }
2089                if sst_misses > 0 {
2090                    self.stats
2091                        .miss_key
2092                        .fetch_add(sst_misses as u64, Ordering::Relaxed);
2093                }
2094            }
2095
2096            if empty_cells == 0 {
2097                break;
2098            }
2099        }
2100        let mut deleted = 0;
2101        let mut not_found = 0;
2102        let mut result_size = 0;
2103        let mut results = vec![None; keys.len()];
2104        for (hash, index, result) in cells {
2105            if let Some(result) = result {
2106                inner.accessed_key_hashes[family].insert(hash);
2107                let result = match result {
2108                    LookupValue::Deleted => {
2109                        #[cfg(feature = "stats")]
2110                        self.stats.hits_deleted.fetch_add(1, Ordering::Relaxed);
2111                        deleted += 1;
2112                        None
2113                    }
2114                    LookupValue::Slice { value } => {
2115                        #[cfg(feature = "stats")]
2116                        self.stats.hits_small.fetch_add(1, Ordering::Relaxed);
2117                        result_size += value.len();
2118                        Some(value)
2119                    }
2120                    LookupValue::Blob { sequence_number } => {
2121                        #[cfg(feature = "stats")]
2122                        self.stats.hits_blob.fetch_add(1, Ordering::Relaxed);
2123                        let blob = self.read_blob(sequence_number)?;
2124                        result_size += blob.len();
2125                        Some(blob)
2126                    }
2127                };
2128                results[index] = result;
2129            } else {
2130                #[cfg(feature = "stats")]
2131                self.stats.miss_global.fetch_add(1, Ordering::Relaxed);
2132                not_found += 1;
2133            }
2134        }
2135        span.record("not_found", not_found);
2136        span.record("deleted", deleted);
2137        span.record("result_size", result_size);
2138        Ok(results)
2139    }
2140
2141    /// Returns database statistics.
2142    #[cfg(feature = "stats")]
2143    pub fn statistics(&self) -> Statistics {
2144        let inner = self.inner.read();
2145        Statistics {
2146            meta_files: inner.meta_files.len(),
2147            sst_files: inner.meta_files.iter().map(|m| m.entries().len()).sum(),
2148            key_block_cache: CacheStatistics::new(self.key_block_cache()),
2149            value_block_cache: CacheStatistics::new(self.value_block_cache()),
2150            hits: self.stats.hits_deleted.load(Ordering::Relaxed)
2151                + self.stats.hits_small.load(Ordering::Relaxed)
2152                + self.stats.hits_blob.load(Ordering::Relaxed),
2153            misses: self.stats.miss_global.load(Ordering::Relaxed),
2154            miss_family: self.stats.miss_family.load(Ordering::Relaxed),
2155            miss_range: self.stats.miss_range.load(Ordering::Relaxed),
2156            miss_amqf: self.stats.miss_amqf.load(Ordering::Relaxed),
2157            miss_key: self.stats.miss_key.load(Ordering::Relaxed),
2158        }
2159    }
2160
2161    pub fn meta_info(&self) -> Result<Vec<MetaFileInfo>> {
2162        Ok(self
2163            .inner
2164            .read()
2165            .meta_files
2166            .iter()
2167            .rev()
2168            .map(|meta_file| {
2169                let entries = meta_file
2170                    .entries()
2171                    .iter()
2172                    .map(|entry| {
2173                        let amqf = entry.raw_amqf(meta_file.amqf_data());
2174                        MetaFileEntryInfo {
2175                            sequence_number: entry.sequence_number(),
2176                            min_hash: entry.min_hash(),
2177                            max_hash: entry.max_hash(),
2178                            sst_size: entry.size(),
2179                            flags: entry.flags(),
2180                            amqf_size: entry.amqf_size(),
2181                            amqf_entries: amqf.len(),
2182                            block_count: entry.block_count(),
2183                        }
2184                    })
2185                    .collect();
2186                MetaFileInfo {
2187                    sequence_number: meta_file.sequence_number(),
2188                    family: meta_file.family(),
2189                    obsolete_sst_files: meta_file.obsolete_sst_files().to_vec(),
2190                    entries,
2191                }
2192            })
2193            .collect())
2194    }
2195
2196    /// Shuts down the database. This will print statistics if the `print_stats` feature is enabled.
2197    /// Retries deletion of all previously-deferred files and clears successfully deleted batches.
2198    pub fn shutdown(&self) -> Result<()> {
2199        #[cfg(feature = "print_stats")]
2200        println!("{:#?}", self.statistics());
2201        self.retry_deferred_deletions();
2202        Ok(())
2203    }
2204
2205    /// Attempts to delete files with the given extension, returning an iterator of sequence
2206    /// numbers for files that could not be deleted (e.g. due to open memory maps on Windows).
2207    fn try_delete_files<'a>(
2208        dir: &'a Path,
2209        seqs: &'a [u32],
2210        ext: &'a str,
2211    ) -> impl Iterator<Item = u32> + 'a {
2212        seqs.iter()
2213            .copied()
2214            .filter(move |&seq| fs::remove_file(dir.join(format!("{seq:08}.{ext}"))).is_err())
2215    }
2216
2217    /// Retries deletion of files that previously failed (typically due to open memory maps on
2218    /// Windows). Any file that still fails is kept for the next retry.
2219    /// Best-effort: persistent failures are acceptable because `load_directory` cleans up
2220    /// any leftover files on the next open via the `.del` file.
2221    fn retry_deferred_deletions(&self) {
2222        let mut deferred = self.deferred_deletions.lock();
2223        deferred.retain(|entry| {
2224            let (seq, ext) = match *entry {
2225                DeferredDeletion::Sst(seq) => (seq, "sst"),
2226                DeferredDeletion::Meta(seq) => (seq, "meta"),
2227                DeferredDeletion::Blob(seq) => (seq, "blob"),
2228            };
2229            // Keep the entry only if deletion still fails.
2230            fs::remove_file(self.path.join(format!("{seq:08}.{ext}"))).is_err()
2231        });
2232    }
2233}
2234
2235fn range_to_str(min: u64, max: u64) -> String {
2236    use std::fmt::Write;
2237    const DISPLAY_SIZE: usize = 100;
2238    const TOTAL_SIZE: u64 = u64::MAX;
2239    let start_pos = (min as u128 * DISPLAY_SIZE as u128 / TOTAL_SIZE as u128) as usize;
2240    let end_pos = (max as u128 * DISPLAY_SIZE as u128 / TOTAL_SIZE as u128) as usize;
2241    let mut range_str = String::new();
2242    for i in 0..DISPLAY_SIZE {
2243        if i == start_pos && i == end_pos {
2244            range_str.push('O');
2245        } else if i == start_pos {
2246            range_str.push('[');
2247        } else if i == end_pos {
2248            range_str.push(']');
2249        } else if i > start_pos && i < end_pos {
2250            range_str.push('=');
2251        } else {
2252            range_str.push(' ');
2253        }
2254    }
2255    write!(range_str, " | {min:016x}-{max:016x}").unwrap();
2256    range_str
2257}
2258
2259pub struct MetaFileInfo {
2260    pub sequence_number: u32,
2261    pub family: u32,
2262    pub obsolete_sst_files: Vec<u32>,
2263    pub entries: Vec<MetaFileEntryInfo>,
2264}
2265
2266pub struct MetaFileEntryInfo {
2267    pub sequence_number: u32,
2268    pub min_hash: u64,
2269    pub max_hash: u64,
2270    pub amqf_size: u32,
2271    pub amqf_entries: usize,
2272    pub sst_size: u64,
2273    pub flags: MetaEntryFlags,
2274    pub block_count: u16,
2275}