Skip to main content

turbo_persistence/
db.rs

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