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