Skip to main content

turbo_persistence/
db.rs

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