Skip to main content

commonware_storage/qmdb/
mod.rs

1//! A collection of authenticated databases inspired by QMDB (Quick Merkle Database).
2//!
3//! # Terminology
4//!
5//! A database's state is derived from an append-only log of state-changing _operations_.
6//!
7//! In a _keyed_ database, a _key_ either has a _value_ or it doesn't, and different types of
8//! operations modify the state of a specific key. A key that has a value can change to one without
9//! a value through the _delete_ operation. The _update_ operation gives a key a specific value. We
10//! sometimes call an update for a key that doesn't already have a value a _create_ operation, but
11//! its representation in the log is the same.
12//!
13//! Keys with values are called _active_. An operation is called _active_ if (1) its key is active,
14//! (2) it is an update operation, and (3) it is the most recent operation for that key.
15//!
16//! # Database Lifecycle
17//!
18//! All variants are modified through a batch API that follows a common pattern:
19//! 1. Create a batch from the database.
20//! 2. Stage mutations on the batch.
21//! 3. Merkleize the batch. This resolves mutations against the current state and computes
22//!    the Merkle root that would result from applying them.
23//! 4. Inspect the root or create child batches.
24//! 5. Apply the batch to the database (unapplied ancestors are applied automatically).
25//!
26//! [any] and [current] advance the inactivity floor with a [`floor::Policy`] when merkleizing.
27//! [store] does so when applying a batch.
28//!
29//! The specific mutation methods vary by variant.
30//! See each variant's module documentation for the concrete API and usage examples.
31//!
32//! # Batch validity
33//!
34//! A batch's chain has a set of states: its recorded database boundary, each ancestor's tip, and
35//! the batch's own tip. While the database is at one of them, value reads through the batch are
36//! exact, because the batch retains the writes of every ancestor the database has not applied. At
37//! any other state, for example after a sibling or a descendant of the batch is applied, reads
38//! return [`Error::StaleRead`], and merkleize and apply return [`Error::StaleBatch`]. Merkleize
39//! also returns [`Error::StaleBatch`] once an unapplied ancestor has been dropped, because it
40//! reads ancestor operations through the weak parent links, so every unapplied ancestor must be
41//! held until its descendant is merkleized. See [`chain`] for the details.
42//!
43//! # Durability
44//!
45//! `commit()` makes applied state durable. `start_sync()` is its pipelined form, which also
46//! tries to advance the recovery watermark to bound startup recovery. `sync()` makes applied state
47//! durable and guarantees no recovery is needed on startup after a crash.
48//!
49//! # Initialization bounds
50//!
51//! With `Some(max_size)`, `init` opens the latest retained commit with at most `max_size`
52//! operations, counting commit records and pruned operations. `None` opens the latest retained
53//! state. Zero is invalid, and a bound above the stored size does not grow the database.
54//! Initialization fails if pruning removed history needed by the selected commit.
55//!
56//! Initialization durably removes later history before returning successfully. The bound does not
57//! limit future appends. To require an exact checkpoint, also check the recovered root and range.
58//!
59//! # Ownership
60//!
61//! Mutating methods take the database by value and return it on success. If a mutating
62//! method returns an error, or its future is dropped before it finishes, the database is
63//! gone: state that was not yet durable is discarded, but everything already on disk stays
64//! recoverable. This applies to validation errors too (e.g. rejecting a stale batch);
65//! use each database's `validate_batch` to pre-check a batch without risking the handle.
66//!
67//! # Traits
68//!
69//! Keyed mutable variants ([any] and [current]) implement `any::traits::DbAny`.
70//!
71//! # Acknowledgments
72//!
73//! The following resources were used as references when implementing this crate:
74//!
75//! * [QMDB: Quick Merkle Database](https://arxiv.org/abs/2501.05262)
76//! * [Merkle Mountain
77//!   Ranges](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md)
78
79use crate::{
80    index::{
81        Cursor, Unordered as Index,
82        partitioned::{PartitionRange, Partitioned},
83    },
84    journal::{Error as JournalError, contiguous::Contiguous},
85    merkle::{
86        Bagging, Family, Location,
87        hasher::{Hasher as MerkleHasher, Standard as StandardHasher},
88    },
89    qmdb::operation::{Floored, Operation},
90    translator::Translator,
91};
92use cache::Cache;
93use commonware_codec::Encode;
94use commonware_cryptography::Hasher;
95use commonware_runtime::{AbortOnDrop, ReadOptions, Spawner};
96use commonware_utils::{
97    bitmap::{Atomic, BitMap},
98    channel::mpsc,
99    prefetch::Lookahead,
100};
101use core::{num::NonZeroUsize, ops::Range};
102use futures::{StreamExt as _, pin_mut};
103use std::{collections::VecDeque, sync::Arc};
104use thiserror::Error;
105
106pub mod any;
107pub(crate) mod bitmap;
108mod cache;
109pub mod chain;
110pub(crate) mod compact;
111#[cfg(test)]
112mod conformance;
113pub mod current;
114pub mod floor;
115pub mod immutable;
116pub mod keyless;
117mod metrics;
118pub mod operation;
119pub mod store;
120pub mod sync;
121pub mod verify;
122
123pub use verify::{
124    create_multi_proof, create_proof_store, verify_multi_proof, verify_proof,
125    verify_proof_and_extract_digests, verify_proof_and_pinned_nodes,
126};
127
128/// Reject a bound that cannot retain the bootstrap commit.
129pub(crate) fn validate_initialization_bound<F: Family>(
130    bound: Option<Location<F>>,
131) -> Result<(), Error<F>> {
132    if bound.is_some_and(|size| size == 0) {
133        return Err(Error::InvalidInitializationBound);
134    }
135    Ok(())
136}
137
138/// Validate a selected commit against its retained history and return its floor.
139fn validate_initialization_commit<F: Family>(
140    start: u64,
141    size: u64,
142    fresh: bool,
143    commit: Option<&impl Floored<F>>,
144    replay_from_floor: bool,
145) -> Result<Option<Location<F>>, Error<F>> {
146    if size == 0 {
147        return if fresh {
148            Ok(None)
149        } else {
150            Err(Error::DataCorrupted("no retained commit"))
151        };
152    }
153    let floor = commit
154        .and_then(Floored::has_floor)
155        .ok_or(Error::DataCorrupted(
156            "selected operation has no commit floor",
157        ))?;
158    if *floor >= size {
159        return Err(Error::DataCorrupted(
160            "inactivity floor exceeds commit location",
161        ));
162    }
163    if replay_from_floor && *floor < start {
164        return Err(Error::HistoricalFloorPruned(Location::new(size)));
165    }
166    Ok(Some(floor))
167}
168
169/// Check the selected commit before recovery discards history. Rebuilding a snapshot from its floor
170/// additionally requires retaining that floor. Keyless only restores commit fields.
171pub(crate) async fn validate_initialization<F, E, C, H, S>(
172    pending: &crate::journal::authenticated::Recovery<F, E, C, H, S>,
173    replay_from_floor: bool,
174) -> Result<Option<Location<F>>, Error<F>>
175where
176    F: Family,
177    E: crate::Context,
178    C: crate::journal::authenticated::Backing<E, Item: Floored<F> + commonware_codec::EncodeShared>,
179    H: Hasher,
180    S: commonware_parallel::Strategy,
181{
182    let bounds = pending.bounds();
183    let commit = if bounds.end == 0 {
184        None
185    } else {
186        Some(pending.read(bounds.end - 1).await?)
187    };
188    validate_initialization_commit(
189        bounds.start,
190        bounds.end,
191        pending.is_fresh(),
192        commit.as_ref(),
193        replay_from_floor,
194    )
195}
196
197/// Select a QMDB commit before validating variant-specific reconstruction state.
198pub(crate) async fn prepare_initialization<F, E, C, H, S>(
199    context: E,
200    merkle: crate::merkle::full::Config<S>,
201    journal: C::Config,
202    max_size: Option<Location<F>>,
203) -> Result<crate::journal::authenticated::Recovery<F, E, C, H, S>, Error<F>>
204where
205    F: Family,
206    E: crate::Context,
207    C: crate::journal::authenticated::Backing<E, Item: Floored<F> + commonware_codec::EncodeShared>,
208    H: Hasher,
209    S: commonware_parallel::Strategy,
210{
211    validate_initialization_bound(max_size)?;
212    Ok(crate::journal::authenticated::Journal::prepare(
213        context,
214        merkle,
215        journal,
216        max_size.map(|size| *size),
217        |op: &C::Item| op.has_floor().is_some(),
218        ROOT_BAGGING,
219    )
220    .await?)
221}
222
223/// Publish a standard QMDB journal after validating the retained commit.
224#[commonware_macros::boxed]
225pub(crate) async fn init_journal<F, E, C, H, S>(
226    context: E,
227    merkle: crate::merkle::full::Config<S>,
228    journal: C::Config,
229    max_size: Option<Location<F>>,
230    replay_from_floor: bool,
231) -> Result<crate::journal::authenticated::Journal<F, E, C, H, S>, Error<F>>
232where
233    F: Family,
234    E: crate::Context,
235    C: crate::journal::authenticated::Backing<E, Item: Floored<F> + commonware_codec::EncodeShared>,
236    H: Hasher,
237    S: commonware_parallel::Strategy,
238{
239    let pending = prepare_initialization(context, merkle, journal, max_size).await?;
240    validate_initialization(&pending, replay_from_floor).await?;
241    Ok(pending.finish().await?)
242}
243
244/// Merkle peak bagging policy used by QMDB operation roots.
245pub(crate) const ROOT_BAGGING: Bagging = Bagging::BackwardFold;
246
247/// Return the Merkle hasher configuration used by QMDB operation roots and proofs.
248pub const fn hasher<H: Hasher>() -> StandardHasher<H> {
249    StandardHasher::new(ROOT_BAGGING)
250}
251
252/// Return the root of an operation log containing only `operation`.
253///
254/// This lets database variants derive their initial root from the bootstrap commit without
255/// opening a database.
256fn single_operation_root<F: Family, H: Hasher>(operation: &impl Encode) -> H::Digest {
257    let hasher = hasher::<H>();
258    let leaf = MerkleHasher::<F>::leaf_digest(
259        &hasher,
260        F::location_to_position(Location::new(0)),
261        &operation.encode(),
262    );
263    MerkleHasher::<F>::root(&hasher, Location::new(1), 0, [&leaf])
264        .expect("a single-leaf Merkle root is always valid")
265}
266
267/// Look up the inactivity floor declared at the commit immediately preceding `op_count`.
268///
269/// `op_count` must be a non-zero commit-boundary historical size: the operation at `op_count - 1`
270/// must itself be a commit op (one for which `floor_of` returns `Some`).
271///
272/// # Errors
273///
274/// - [`Error::HistoricalFloorPruned`] if `op_count` is zero (no preceding commit exists), or if
275///   `op_count - 1` is retained but is not a commit op (either because the caller passed a
276///   non-commit-boundary size, or because pruning removed the commit that would have governed this
277///   size).
278/// - [`JournalError::ItemPruned`] if `op_count - 1` precedes the oldest retained location.
279pub(crate) async fn find_inactivity_floor_at<F, R>(
280    reader: &R,
281    op_count: Location<F>,
282) -> Result<Location<F>, Error<F>>
283where
284    F: Family,
285    R: Contiguous<Item: Floored<F>>,
286{
287    let Some(last_op) = op_count.checked_sub(1) else {
288        return Err(Error::HistoricalFloorPruned(op_count));
289    };
290    let last_op = *last_op;
291    let bounds = reader.bounds();
292    if last_op < bounds.start {
293        return Err(JournalError::ItemPruned(last_op).into());
294    }
295
296    let op = reader.read(last_op).await?;
297    let floor = op
298        .has_floor()
299        .ok_or(Error::HistoricalFloorPruned(op_count))?;
300    if floor > Location::new(last_op) {
301        return Err(Error::DataCorrupted(
302            "inactivity floor exceeds commit location",
303        ));
304    }
305    Ok(floor)
306}
307
308/// Compute the inactive peak count for a historical operation count.
309pub(crate) async fn inactive_peaks_at<F, R>(
310    reader: &R,
311    op_count: Location<F>,
312) -> Result<usize, Error<F>>
313where
314    F: Family,
315    R: Contiguous<Item: Floored<F>>,
316{
317    if op_count == Location::new(0) {
318        return Ok(0);
319    }
320
321    let floor = find_inactivity_floor_at::<F, _>(reader, op_count).await?;
322    Ok(F::inactive_peaks(op_count, floor))
323}
324
325/// Errors that can occur when interacting with an authenticated database.
326#[derive(Error, Debug)]
327pub enum Error<F: Family> {
328    /// A QMDB initialization bound cannot exclude its initial commit.
329    #[error("initialization bound must allow at least one operation")]
330    InvalidInitializationBound,
331
332    #[error("data corrupted: {0}")]
333    DataCorrupted(&'static str),
334
335    #[error("merkle error: {0}")]
336    Merkle(#[from] crate::merkle::Error<F>),
337
338    #[error("metadata error: {0}")]
339    Metadata(#[from] crate::metadata::Error),
340
341    #[error("journal error: {0}")]
342    Journal(#[from] crate::journal::Error),
343
344    #[error("runtime error: {0}")]
345    Runtime(#[from] commonware_runtime::Error),
346
347    #[error("operation pruned: {0}")]
348    OperationPruned(Location<F>),
349
350    /// The requested key was not found in the snapshot.
351    #[error("key not found")]
352    KeyNotFound,
353
354    /// The key exists in the db, so we cannot prove its exclusion.
355    #[error("key exists")]
356    KeyExists,
357
358    #[error("unexpected data at location: {0}")]
359    UnexpectedData(Location<F>),
360
361    #[error("location out of bounds: {0} >= {1}")]
362    LocationOutOfBounds(Location<F>, Location<F>),
363
364    /// A batched read's locations were out of order.
365    #[error("location {1} out of order after {0}")]
366    LocationOutOfOrder(Location<F>, Location<F>),
367
368    #[error("prune location {0} beyond minimum required location {1}")]
369    PruneBeyondMinRequired(Location<F>, Location<F>),
370
371    /// The batch cannot be merkleized or applied against the database's state.
372    ///
373    /// This happens when:
374    /// - The database is at neither the batch's recorded database boundary nor an ancestor's tip.
375    ///   Reads then return [`Error::StaleRead`], except at the batch's own tip, where an applied
376    ///   batch still reads but cannot be applied again.
377    /// - Merkleize only: an unapplied ancestor was dropped, so merkleize cannot read its
378    ///   operations. Value reads are unaffected, because merkleized batches retain their
379    ///   ancestors' writes.
380    /// - Current merkleize only: the batch was built against another database instance.
381    ///
382    /// See [`chain`] for more details on staleness detection, and
383    /// [batch validity](self#batch-validity) for the states at which a batch stays usable.
384    #[error("stale batch: current database state does not match the batch")]
385    StaleBatch,
386
387    /// A read through a batch found the database at no state of the batch's chain (see [`chain`]),
388    /// for example after a sibling or a descendant of the batch was applied, or after the database
389    /// was reinitialized at another state. Build a new batch from the database to read its state.
390    #[error("stale read: database is at no state of the batch's chain")]
391    StaleRead,
392
393    /// A batch read asked for a location below the batch's inactivity floor (for an unmerkleized
394    /// batch, the floor it builds on).
395    ///
396    /// Whether the journal still retains such a location depends on local pruning, so batch reads
397    /// reject it on every database at the same commitment.
398    #[error("location below inactivity floor: {0}")]
399    BelowInactivityFloor(Location<F>),
400
401    /// The batch's inactivity floor is lower than the floor it builds on: its parent's, or the
402    /// database's for a batch with no parent.
403    #[error("floor regressed: batch floor {0} < prior floor {1}")]
404    FloorRegressed(Location<F>, Location<F>),
405
406    /// The batch's inactivity floor exceeds its own commit operation's location. The floor
407    /// must not sit past the commit, since a subsequent `prune(floor)` would then remove the
408    /// last readable commit from the journal.
409    #[error("floor beyond commit location: floor {0} > commit loc {1}")]
410    FloorBeyondSize(Location<F>, Location<F>),
411
412    /// The commit at the given operation count cannot be reconstructed from retained history.
413    /// The payload is the requested or selected operation count, not its inactivity floor.
414    #[error("historical floor pruned for size: {0}")]
415    HistoricalFloorPruned(Location<F>),
416}
417
418impl<F: Family> From<crate::journal::authenticated::Error<F>> for Error<F> {
419    fn from(e: crate::journal::authenticated::Error<F>) -> Self {
420        match e {
421            crate::journal::authenticated::Error::Journal(j) => Self::Journal(j),
422            crate::journal::authenticated::Error::Merkle(m) => Self::Merkle(m),
423        }
424    }
425}
426
427/// Builds the database's snapshot by replaying the log starting at the inactivity floor. Assumes
428/// the log is not pruned beyond the inactivity floor. The callback is invoked for each replayed
429/// operation, indicating activity status updates. The first argument of the callback is the
430/// activity status of the operation, and the second argument is the location of the operation it
431/// inactivates (if any). Returns the number of active keys in the db.
432///
433/// `init_buffer` sizes the replay read buffer (in bytes). `cache_size` bounds a
434/// `(location -> key)` cache that lets collision resolution resolve candidates from memory
435/// instead of re-reading the log; `None` disables it.
436pub(super) async fn build_snapshot_from_log<F, C, I, Fn>(
437    inactivity_floor_loc: crate::merkle::Location<F>,
438    reader: &C,
439    snapshot: &mut I,
440    init_buffer: NonZeroUsize,
441    cache_size: Option<NonZeroUsize>,
442    mut callback: Fn,
443) -> Result<usize, Error<F>>
444where
445    F: crate::merkle::Family,
446    C: Contiguous<Item: Operation<F>>,
447    I: Index<Value = crate::merkle::Location<F>>,
448    Fn: FnMut(bool, Option<crate::merkle::Location<F>>),
449{
450    let bounds = reader.bounds();
451    // Init reads every operation once, so the replayed pages are not kept in the OS page cache:
452    // the init cache, not the OS cache, decides which probes hit.
453    let stream = reader
454        .replay(
455            *inactivity_floor_loc..bounds.end,
456            init_buffer,
457            ReadOptions::DONT_CACHE,
458        )
459        .await?;
460    pin_mut!(stream);
461    let last_commit_loc = bounds.end.saturating_sub(1);
462
463    // Memoize `(location -> key)` for replayed update ops so collision resolution in
464    // `find_update_op` resolves candidates from memory instead of re-reading (and re-decoding) the
465    // log.
466    let mut cache = cache_size.map(Cache::<<C::Item as Operation<F>>::Key>::new);
467
468    let mut active_keys: usize = 0;
469    while let Some(batch) = stream.next().await {
470        for (loc, op) in batch? {
471            if let Some(key) = op.key() {
472                if op.is_delete() {
473                    let old_loc = delete_key(snapshot, reader, key, cache.as_mut()).await?;
474                    callback(false, old_loc);
475                    if old_loc.is_some() {
476                        active_keys -= 1;
477                    }
478                } else if op.is_update() {
479                    let new_loc = crate::merkle::Location::new(loc);
480                    let old_loc =
481                        update_key(snapshot, reader, key, new_loc, cache.as_mut()).await?;
482                    callback(true, old_loc);
483                    if old_loc.is_none() {
484                        active_keys += 1;
485                    }
486
487                    // This update op is now a `find_update_op` candidate for later ops of its
488                    // key.
489                    if let Some(cache) = cache.as_mut() {
490                        cache.put(loc, op.into_key().expect("operation without key"));
491                    }
492                }
493            } else if op.has_floor().is_some() {
494                callback(loc == last_commit_loc, None);
495            }
496        }
497    }
498
499    Ok(active_keys)
500}
501
502/// Delete `key` from the snapshot if it exists, using a stable log reader, and return the
503/// previously associated location.
504async fn delete_key<F, I, R>(
505    snapshot: &mut I,
506    reader: &R,
507    key: &<R::Item as Operation<F>>::Key,
508    cache: Option<&mut Cache<<R::Item as Operation<F>>::Key>>,
509) -> Result<Option<Location<F>>, Error<F>>
510where
511    F: Family,
512    I: Index<Value = Location<F>>,
513    R: Contiguous,
514    R::Item: Operation<F>,
515{
516    // If the translated key is in the snapshot, get a cursor to look for the key.
517    let Some(cursor) = snapshot.get_mut(key) else {
518        return Ok(None);
519    };
520    delete_at_cursor::<F, _, _>(cursor, reader, key, cache).await
521}
522
523/// Delete `key` at `cursor` (obtained from a `get_mut` lookup of `key`), returning its location if
524/// it was present among the cursor's conflicts. When supplied, the matched location is removed
525/// from `cache` with the snapshot deletion.
526async fn delete_at_cursor<F, C, R>(
527    mut cursor: C,
528    reader: &R,
529    key: &<R::Item as Operation<F>>::Key,
530    mut cache: Option<&mut Cache<<R::Item as Operation<F>>::Key>>,
531) -> Result<Option<Location<F>>, Error<F>>
532where
533    F: Family,
534    C: Cursor<Value = Location<F>>,
535    R: Contiguous,
536    R::Item: Operation<F>,
537{
538    // Find the matching key among all conflicts, then delete it.
539    let Some(loc) = find_update_op::<F, _>(reader, &mut cursor, key, cache.as_deref_mut()).await?
540    else {
541        return Ok(None);
542    };
543
544    // Cache entries mirror current snapshot locations, so invalidate the matched location with
545    // the authoritative deletion.
546    cursor.delete();
547    if let Some(cache) = cache {
548        cache.remove(*loc);
549    }
550
551    Ok(Some(loc))
552}
553
554/// Update `key` in the snapshot using a stable log reader, returning its old location if present.
555async fn update_key<F, I, R>(
556    snapshot: &mut I,
557    reader: &R,
558    key: &<R::Item as Operation<F>>::Key,
559    new_loc: Location<F>,
560    cache: Option<&mut Cache<<R::Item as Operation<F>>::Key>>,
561) -> Result<Option<Location<F>>, Error<F>>
562where
563    F: Family,
564    I: Index<Value = Location<F>>,
565    R: Contiguous,
566    R::Item: Operation<F>,
567{
568    // If the translated key is not in the snapshot, insert the new location. Otherwise, get a
569    // cursor to look for the key.
570    let Some(cursor) = snapshot.get_mut_or_insert(key, new_loc) else {
571        return Ok(None);
572    };
573    update_at_cursor::<F, _, _>(cursor, reader, key, new_loc, cache).await
574}
575
576/// Update `key` to `new_loc` at `cursor` (obtained from a `get_mut_or_insert` lookup of `key`),
577/// returning its old location if it was present among the cursor's conflicts; otherwise `new_loc`
578/// is inserted at the cursor. When supplied, the matched old location is removed from `cache` with
579/// the snapshot update.
580async fn update_at_cursor<F, C, R>(
581    mut cursor: C,
582    reader: &R,
583    key: &<R::Item as Operation<F>>::Key,
584    new_loc: Location<F>,
585    mut cache: Option<&mut Cache<<R::Item as Operation<F>>::Key>>,
586) -> Result<Option<Location<F>>, Error<F>>
587where
588    F: Family,
589    C: Cursor<Value = Location<F>>,
590    R: Contiguous,
591    R::Item: Operation<F>,
592{
593    // Find the matching key among all conflicts, then update its location.
594    if let Some(loc) =
595        find_update_op::<F, _>(reader, &mut cursor, key, cache.as_deref_mut()).await?
596    {
597        // Removing the superseded cache entry with the snapshot update lets the caller reuse its
598        // slot for `new_loc` instead of evicting another entry.
599        assert!(new_loc > loc);
600        cursor.update(new_loc);
601        if let Some(cache) = cache {
602            cache.remove(*loc);
603        }
604        return Ok(Some(loc));
605    }
606
607    // The key wasn't in the snapshot, so add it to the cursor.
608    cursor.insert(new_loc);
609
610    Ok(None)
611}
612
613/// Find and return the location of the update operation for `key`, if it exists. The cursor is
614/// positioned at the matching location, and can be used to update or delete the key.
615async fn find_update_op<F, R>(
616    reader: &R,
617    cursor: &mut impl Cursor<Value = Location<F>>,
618    key: &<R::Item as Operation<F>>::Key,
619    mut cache: Option<&mut Cache<<R::Item as Operation<F>>::Key>>,
620) -> Result<Option<Location<F>>, Error<F>>
621where
622    F: Family,
623    R: Contiguous,
624    R::Item: Operation<F>,
625{
626    while let Some(&loc) = cursor.next() {
627        // Consult the cache first; on a miss, read the log and populate.
628        let matches = if let Some(k) = cache.as_deref().and_then(|c| c.get(*loc)) {
629            *k == *key
630        } else {
631            let op = reader.read(*loc).await?;
632            let k = op.key().expect("operation without key");
633            let matches = *k == *key;
634
635            // Every caller immediately mutates a match. Admitting it here could evict another
636            // cached location before the caller invalidates this one.
637            if !matches && let Some(cache) = cache.as_deref_mut() {
638                cache.put(*loc, op.into_key().expect("operation without key"));
639            }
640            matches
641        };
642        if matches {
643            return Ok(Some(loc));
644        }
645    }
646
647    Ok(None)
648}
649
650/// Bounded depth (in batches) of each per-worker channel during a parallel build. Backpressure keeps
651/// the replay from running arbitrarily far ahead of a slow worker.
652const SNAPSHOT_CHANNEL_DEPTH: usize = 4;
653
654/// A batch of keyed operations routed to a snapshot-build worker: each entry is the op's key, its
655/// location, and whether it is a delete.
656type RoutedBatch<K> = Vec<(K, u64, bool)>;
657
658/// Parameters shared by the inline replay and decode chunks of one parallel build.
659#[derive(Clone, Copy)]
660struct SnapshotRouting {
661    /// Number of insert workers routed to.
662    workers: usize,
663    /// Maps a key to its index partition.
664    partition_of: fn(&[u8]) -> usize,
665    /// Partitions per worker range: worker = `partition_of(key) / range_size`.
666    range_size: usize,
667    /// Replay read-buffer size in bytes.
668    init_buffer: NonZeroUsize,
669}
670
671/// Number of operations the snapshot replay batches per worker-channel send during a parallel
672/// build. Build throughput is mostly insensitive to this value, so it is a constant rather than
673/// configuration. Small in tests so ordinary logs exercise batch boundaries.
674#[cfg(not(test))]
675const SNAPSHOT_ROUTE_BATCH: usize = 4096;
676#[cfg(test)]
677const SNAPSHOT_ROUTE_BATCH: usize = 3;
678
679/// Operations ahead of its insert at which a parallel-build worker hints an operation's partition
680/// entries. The partition's header is hinted twice as far ahead. One in tests, so the hints reach
681/// their steady state within a [SNAPSHOT_ROUTE_BATCH].
682#[cfg(not(test))]
683const PREFETCH_DISTANCE: usize = 16;
684#[cfg(test)]
685const PREFETCH_DISTANCE: usize = 1;
686
687/// Operations per decode chunk in a parallel build. Decoding the replay stream is the build's
688/// serial bottleneck at large sizes, so contiguous chunks of this many locations are decoded (and
689/// partition-routed) on concurrent tasks while the coordinator forwards finished chunks in position
690/// order. Together with the decoder count, this bounds the queued operations. Each decoder also
691/// reserves one batch per worker. Small in tests so ordinary logs exercise chunk boundaries.
692#[cfg(not(test))]
693const SNAPSHOT_DECODE_CHUNK: u64 = 1 << 17;
694#[cfg(test)]
695const SNAPSHOT_DECODE_CHUNK: u64 = 64;
696
697/// Replay `range` and send each worker's keyed operations in log order, in batches of at most
698/// [SNAPSHOT_ROUTE_BATCH] operations.
699///
700/// Stop if `send` returns false. The caller owns the receiving tasks and observes their errors when
701/// joining them.
702async fn route_snapshot<F, C, Fut>(
703    log: &C,
704    range: Range<u64>,
705    routing: SnapshotRouting,
706    mut send: impl FnMut(usize, RoutedBatch<<C::Item as Operation<F>>::Key>) -> Fut + Send,
707) -> Result<(), Error<F>>
708where
709    F: Family,
710    C: Contiguous<Item: Operation<F>>,
711    Fut: Future<Output = bool> + Send,
712{
713    // Init reads every operation once, so the replayed pages are not kept in the OS page cache:
714    // the init cache, not the OS cache, decides which probes hit.
715    let stream = log
716        .replay(range, routing.init_buffer, ReadOptions::DONT_CACHE)
717        .await?;
718    pin_mut!(stream);
719    let mut batches: Vec<RoutedBatch<_>> = (0..routing.workers)
720        .map(|_| Vec::with_capacity(SNAPSHOT_ROUTE_BATCH))
721        .collect();
722    while let Some(batch) = stream.next().await {
723        for (loc, op) in batch? {
724            let is_delete = op.is_delete();
725            let Some(key) = op.into_key() else { continue };
726            let w = (routing.partition_of)(key.as_ref()) / routing.range_size;
727            batches[w].push((key, loc, is_delete));
728            if batches[w].len() >= SNAPSHOT_ROUTE_BATCH {
729                let routed =
730                    std::mem::replace(&mut batches[w], Vec::with_capacity(SNAPSHOT_ROUTE_BATCH));
731                if !send(w, routed).await {
732                    return Ok(());
733                }
734            }
735        }
736    }
737
738    // Flush remaining batches before the caller closes the channels.
739    for (w, batch) in batches.into_iter().enumerate() {
740        if !batch.is_empty() && !send(w, batch).await {
741            break;
742        }
743    }
744    Ok(())
745}
746
747/// Build one parallel-init worker's partial snapshot: apply the routed operations (streamed in log
748/// order over `rx`) to `index`, resolving translated-key collisions with the worker's own log
749/// `reader` and `(location -> key)` cache. Sets the bits of the range's active locations in the
750/// shared `active` bitmap (indexed over `activity`, the replayed region) and returns the populated
751/// worker index along with the range's active-key count.
752async fn build_snapshot_worker<F, C, R>(
753    log: Arc<C>,
754    mut rx: mpsc::Receiver<RoutedBatch<<C::Item as Operation<F>>::Key>>,
755    mut index: R,
756    activity: Range<u64>,
757    active: Arc<Atomic>,
758    cache_size: Option<NonZeroUsize>,
759) -> Result<(R, usize), Error<F>>
760where
761    F: Family,
762    C: Contiguous<Item: Operation<F>>,
763    R: PartitionRange<Value = Location<F>>,
764{
765    let mut cache = cache_size.map(Cache::<<C::Item as Operation<F>>::Key>::new);
766
767    // Ops are software pipelined so the cache misses of their partition lookups and cache inserts
768    // overlap. Before op `k` is applied, the loop hints the header of op `k + 2 * D` and the
769    // entries and cache set of op `k + D` (`D = PREFETCH_DISTANCE`), so each op's loads are
770    // issued in three steps:
771    //
772    //   applying op:        k-2D .......... k-D ........... k
773    //   header of op k:     hinted -------> read by prefetch_entries
774    //   entries of op k:                    hinted -------> read by the insert
775    //   cache set of op k:                  hinted -------> scanned by put
776    //
777    // The header holds the entries' addresses, so their hint can only be issued once it has
778    // landed.
779    while let Some(batch) = rx.recv().await {
780        let mut ops = batch.into_iter();
781        let mut headers = Lookahead::new(2 * PREFETCH_DISTANCE);
782        let mut entries = Lookahead::new(PREFETCH_DISTANCE);
783        loop {
784            for (key, _, _) in headers.advance(ops.as_slice()) {
785                index.prefetch_header(key.as_ref());
786            }
787            for (key, loc, is_delete) in entries.advance(ops.as_slice()) {
788                index.prefetch_entries(key.as_ref());
789                if !is_delete && let Some(cache) = &cache {
790                    cache.prefetch(*loc);
791                }
792            }
793            let Some((key, loc, is_delete)) = ops.next() else {
794                break;
795            };
796
797            if is_delete {
798                if let Some(cursor) = index.get_mut(&key) {
799                    delete_at_cursor::<F, _, _>(cursor, &*log, &key, cache.as_mut()).await?;
800                }
801            } else {
802                let new_loc = Location::new(loc);
803                if let Some(cursor) = index.get_mut_or_insert(&key, new_loc) {
804                    update_at_cursor::<F, _, _>(cursor, &*log, &key, new_loc, cache.as_mut())
805                        .await?;
806                }
807
808                // This update op is now a `find_update_op` candidate for later ops of its key.
809                // `key` is owned by this batch and unused after the update, so move it in.
810                if let Some(cache) = cache.as_mut() {
811                    cache.put(loc, key);
812                }
813            }
814        }
815    }
816
817    // Reconstruct this range's share of the activity bitmap (in parallel with the other workers)
818    // and count the active keys. Locations are partitioned across workers, so every bit has a
819    // single writer.
820    let mut active_keys = 0;
821    index.for_each_value(|loc| {
822        active.set(**loc - activity.start);
823        active_keys += 1;
824    });
825    Ok((index, active_keys))
826}
827
828/// Build a snapshot serially on the calling task via [build_snapshot_from_log], collecting each
829/// replayed location's activity status into a [BitMap]. Returns the number of active keys and the
830/// activity bitmap (see [SnapshotBuild::build_snapshot]).
831async fn build_snapshot_serial<F, C, I>(
832    inactivity_floor_loc: Location<F>,
833    reader: &C,
834    snapshot: &mut I,
835    init_buffer: NonZeroUsize,
836    cache_size: Option<NonZeroUsize>,
837) -> Result<(usize, BitMap), Error<F>>
838where
839    F: Family,
840    C: Contiguous<Item: Operation<F>>,
841    I: Index<Value = Location<F>>,
842{
843    // Track per-op transitions locally: push each op's status and clear the bit of any location it
844    // supersedes. The state after the last op is each location's final status.
845    let mut activity = BitMap::new();
846    let floor = *inactivity_floor_loc;
847    let active_keys = build_snapshot_from_log(
848        inactivity_floor_loc,
849        reader,
850        snapshot,
851        init_buffer,
852        cache_size,
853        |is_active, old_loc| {
854            activity.push(is_active);
855            if let Some(loc) = old_loc {
856                activity.set(*loc - floor, false);
857            }
858        },
859    )
860    .await?;
861    Ok((active_keys, activity))
862}
863
864/// Build a snapshot by decoding the log replay in contiguous chunks on concurrent decode tasks
865/// and routing each keyed operation to the parallel insert worker owning its partition range
866/// (see [Partitioned]). Returns the number of active keys and the activity bitmap (see
867/// [SnapshotBuild::build_snapshot]).
868async fn build_snapshot_parallel<F, E, C, I>(
869    snapshot: &mut I,
870    context: E,
871    inactivity_floor_loc: Location<F>,
872    log: &Arc<C>,
873    init_concurrency: NonZeroUsize,
874    init_buffer: NonZeroUsize,
875    cache_size: Option<NonZeroUsize>,
876) -> Result<(usize, BitMap), Error<F>>
877where
878    F: Family,
879    E: Spawner,
880    C: Contiguous<Item: Operation<F>> + 'static,
881    I: Partitioned + Index<Value = Location<F>>,
882{
883    let count = snapshot.partition_count();
884
885    // Split the `init_concurrency` budget between decode tasks and insert workers. When there is at
886    // least one decode task, this task only forwards batches and is mostly idle, so it does not
887    // count against the concurrency budget. At budgets of three or less, this task decodes inline
888    // and counts against the budget.
889    let concurrency = init_concurrency.get();
890
891    // Inserts cost more CPU than decoding: across widths on both journal types, throughput peaks
892    // near two decoders per five tasks. Spawned decoding always uses at least two decoders, since
893    // a lone decoder starves the workers. Below four tasks this task decodes inline instead.
894    let decoders = if concurrency <= 3 {
895        0
896    } else {
897        // Divide before multiplying to preserve the ratio without overflowing large budgets.
898        let share = concurrency / 5 * 2 + concurrency % 5 * 2 / 5;
899        share.max(2).min(concurrency / 2)
900    };
901    let workers = if decoders == 0 {
902        concurrency.saturating_sub(1).min(count)
903    } else {
904        (concurrency - decoders).min(count)
905    };
906
907    // No workers: build on this task.
908    if workers == 0 {
909        return build_snapshot_serial(
910            inactivity_floor_loc,
911            &**log,
912            snapshot,
913            init_buffer,
914            cache_size,
915        )
916        .await;
917    }
918
919    let floor = *inactivity_floor_loc;
920    let range_size = count.div_ceil(workers);
921
922    // `range_size` rounds up, so the last ranges could start at or past `count`. Reduce
923    // `workers` to the number of non-empty ranges: every spawned worker then owns at least one
924    // partition and routing (`partition / range_size`) stays in `[0, workers)`.
925    let workers = count.div_ceil(range_size);
926    let per_worker_cache = cache_size.and_then(|n| NonZeroUsize::new(n.get() / workers));
927    let end = log.bounds().end;
928
929    // All workers share one atomic bitmap to track the activity bits.
930    let active = Arc::new(Atomic::zeroes(end - floor));
931
932    // Spawn one worker per contiguous partition range, each owning its own reader and cache.
933    let mut senders = Vec::with_capacity(workers);
934    let mut handles = Vec::with_capacity(workers);
935    for w in 0..workers {
936        let (tx, rx) = mpsc::channel(SNAPSHOT_CHANNEL_DEPTH);
937        senders.push(tx);
938        let log = log.clone();
939
940        // This worker owns the contiguous partition range [lo, lo + range_len). It allocates
941        // only that many slots, so per-worker memory is the range, not the full partition set.
942        let lo = w * range_size;
943        let range_len = range_size.min(count - lo);
944        let worker_index = snapshot.new_range(lo, range_len);
945        let active = active.clone();
946        let handle = context
947            .child("snapshot_worker")
948            .with_attribute("worker", w)
949            .dedicated()
950            .spawn(move |_| {
951                build_snapshot_worker::<F, C, I::Range>(
952                    log,
953                    rx,
954                    worker_index,
955                    floor..end,
956                    active,
957                    per_worker_cache,
958                )
959            });
960        handles.push(handle.abort_on_drop());
961    }
962
963    // Route each replayed op to the worker owning its partition, forwarding decoded chunks in
964    // position order to preserve the per-worker op order the insert path relies on. Routing runs in
965    // an inner future so a failure here still drains the workers below rather than leaving them
966    // running detached.
967    //
968    // A closed worker channel means that worker terminated early (e.g. returned an `Error<F>` while
969    // resolving a collision). Routing stops on the first such send failure and the join below
970    // surfaces that worker's error, rather than panicking on the send.
971    let mut pending = VecDeque::new();
972    let routing = SnapshotRouting {
973        workers,
974        partition_of: I::partition_of,
975        range_size,
976        init_buffer,
977    };
978    let routing_result: Result<(), Error<F>> = async {
979        // With no decode tasks, decode and route on this task over one continuous replay to avoid
980        // per-chunk buffered-reader overhead (measured 265s vs 369s on a 1.66B-op log).
981        if decoders == 0 {
982            let senders = &senders;
983            return route_snapshot::<F, C, _>(&**log, floor..end, routing, |w, batch| async move {
984                senders[w].send(batch).await.is_ok()
985            })
986            .await;
987        }
988
989        // Each chunk's channel holds the whole chunk (full sub-batches plus a final partial per
990        // worker), so every decoder can finish regardless of which chunk is being forwarded.
991        let chunk_capacity = SNAPSHOT_DECODE_CHUNK as usize / SNAPSHOT_ROUTE_BATCH + workers;
992        let mut starts = (floor..end)
993            .step_by(usize::try_from(SNAPSHOT_DECODE_CHUNK).expect("chunk size fits usize"));
994        let mut spawn_next = |pending: &mut VecDeque<_>| {
995            let Some(start) = starts.next() else {
996                return false;
997            };
998            let log = log.clone();
999            let len = SNAPSHOT_DECODE_CHUNK.min(end - start);
1000            let (tx, rx) = mpsc::channel(chunk_capacity);
1001            let handle = context
1002                .child("snapshot_decoder")
1003                .dedicated()
1004                .spawn(move |_| async move {
1005                    let tx = &tx;
1006                    route_snapshot::<F, C, _>(
1007                        &*log,
1008                        start..start + len,
1009                        routing,
1010                        |w, batch| async move { tx.send((w, batch)).await.is_ok() },
1011                    )
1012                    .await
1013                });
1014            pending.push_back((rx, handle.abort_on_drop()));
1015            true
1016        };
1017
1018        // Start decoding ahead of forwarding, with at most `decoders` chunks in flight.
1019        for _ in 0..decoders {
1020            if !spawn_next(&mut pending) {
1021                break;
1022            }
1023        }
1024
1025        // Workers need operations in log order even when later chunks finish decoding first.
1026        // Reuse each decoder slot only after forwarding its chunk and joining its task.
1027        while let Some((rx, _)) = pending.front_mut() {
1028            while let Some((w, batch)) = rx.recv().await {
1029                if senders[w].send(batch).await.is_err() {
1030                    return Ok(());
1031                }
1032            }
1033            let (_, decoder) = pending.pop_front().expect("front exists");
1034            decoder.join().await??;
1035            spawn_next(&mut pending);
1036        }
1037        Ok(())
1038    }
1039    .await;
1040
1041    // Close the channels so each worker's stream terminates and it returns its index.
1042    drop(senders);
1043
1044    // Abort and join any decode chunks still in flight, so no decoder outlives a failed init.
1045    while let Some((rx, decoder)) = pending.pop_front() {
1046        drop(rx);
1047        decoder.abort().await;
1048    }
1049
1050    // Join workers before surfacing any replay failure, so none outlive a failed init.
1051    let joined = AbortOnDrop::join_all::<Error<F>>(handles).await;
1052    routing_result?;
1053    let joined = joined?;
1054
1055    // Install each worker's partition range into the snapshot and fold its active-key count in.
1056    let mut total_items = 0;
1057    for (worker_index, worker_keys) in joined {
1058        snapshot.install_range(worker_index);
1059        total_items += worker_keys;
1060    }
1061
1062    // The join reclaimed exclusive ownership of the shared bitmap, so it can be read back.
1063    let mut active = Arc::into_inner(active)
1064        .expect("workers were joined")
1065        .into_bitmap();
1066
1067    // The last operation is the final commit (a log always ends with one), which stays active.
1068    // An empty log has none.
1069    if let Some(last_commit) = end.checked_sub(1)
1070        && last_commit >= floor
1071    {
1072        active.set(last_commit - floor, true);
1073    }
1074
1075    Ok((total_items, active))
1076}
1077
1078/// Builds a database's snapshot index from the operations log.
1079///
1080/// Generic over the `Index` type so each index controls how it builds: serially with the default
1081/// method body, or split across parallel workers with an override.
1082///
1083/// Sealed: only in-crate index types implement this, so internal invariants (e.g. builds must
1084/// drop every clone of the shared log before returning) are enforced by the implementations
1085/// rather than the public contract.
1086pub trait SnapshotBuild<F: Family>:
1087    sealed::SnapshotBuildSealed + Index<Value = Location<F>> + Sized + 'static
1088{
1089    /// The concurrency configuration the build consumes. Index types that always build serially
1090    /// declare `()`, so a setting they cannot use is unrepresentable.
1091    type Concurrency: Copy + Send + Sync + 'static;
1092
1093    /// Replay `log` from `inactivity_floor_loc`, populating `self`. Returns the number of active
1094    /// keys and the activity status of every replayed location, in location order: a location's
1095    /// bit is set iff it holds the current operation of an active key or is the last commit.
1096    ///
1097    /// `init_buffer` sizes the replay read buffer (in bytes), and `cache_size` bounds each
1098    /// build's `(location -> key)` cache in entries (`None` disables it).
1099    fn build_snapshot<E, C>(
1100        &mut self,
1101        _context: E,
1102        inactivity_floor_loc: Location<F>,
1103        log: &Arc<C>,
1104        _init_concurrency: Self::Concurrency,
1105        init_buffer: NonZeroUsize,
1106        cache_size: Option<NonZeroUsize>,
1107    ) -> impl Future<Output = Result<(usize, BitMap), Error<F>>> + Send
1108    where
1109        E: Spawner,
1110        C: Contiguous<Item: Operation<F>> + 'static,
1111    {
1112        async move {
1113            build_snapshot_serial(inactivity_floor_loc, &**log, self, init_buffer, cache_size).await
1114        }
1115    }
1116}
1117
1118mod sealed {
1119    use crate::translator::Translator;
1120
1121    pub trait SnapshotBuildSealed {}
1122    impl<T: Translator, V: Send + Sync> SnapshotBuildSealed for crate::index::unordered::Index<T, V> {}
1123    impl<T: Translator, V: Send + Sync> SnapshotBuildSealed for crate::index::ordered::Index<T, V> {}
1124    impl<T: Translator, V: Send + Sync, const P: usize> SnapshotBuildSealed
1125        for crate::index::partitioned::unordered::Index<T, V, P>
1126    {
1127    }
1128    impl<T: Translator, V: Send + Sync, const P: usize> SnapshotBuildSealed
1129        for crate::index::partitioned::ordered::Index<T, V, P>
1130    {
1131    }
1132}
1133
1134impl<F: Family, T: Translator> SnapshotBuild<F> for crate::index::unordered::Index<T, Location<F>> {
1135    type Concurrency = ();
1136}
1137impl<F: Family, T: Translator> SnapshotBuild<F> for crate::index::ordered::Index<T, Location<F>> {
1138    type Concurrency = ();
1139}
1140
1141impl<F: Family, T: Translator, const P: usize> SnapshotBuild<F>
1142    for crate::index::partitioned::unordered::Index<T, Location<F>, P>
1143{
1144    type Concurrency = NonZeroUsize;
1145
1146    async fn build_snapshot<E, C>(
1147        &mut self,
1148        context: E,
1149        inactivity_floor_loc: Location<F>,
1150        log: &Arc<C>,
1151        init_concurrency: NonZeroUsize,
1152        init_buffer: NonZeroUsize,
1153        cache_size: Option<NonZeroUsize>,
1154    ) -> Result<(usize, BitMap), Error<F>>
1155    where
1156        E: Spawner,
1157        C: Contiguous<Item: Operation<F>> + 'static,
1158    {
1159        build_snapshot_parallel(
1160            self,
1161            context,
1162            inactivity_floor_loc,
1163            log,
1164            init_concurrency,
1165            init_buffer,
1166            cache_size,
1167        )
1168        .await
1169    }
1170}
1171
1172impl<F: Family, T: Translator, const P: usize> SnapshotBuild<F>
1173    for crate::index::partitioned::ordered::Index<T, Location<F>, P>
1174{
1175    type Concurrency = NonZeroUsize;
1176
1177    async fn build_snapshot<E, C>(
1178        &mut self,
1179        context: E,
1180        inactivity_floor_loc: Location<F>,
1181        log: &Arc<C>,
1182        init_concurrency: NonZeroUsize,
1183        init_buffer: NonZeroUsize,
1184        cache_size: Option<NonZeroUsize>,
1185    ) -> Result<(usize, BitMap), Error<F>>
1186    where
1187        E: Spawner,
1188        C: Contiguous<Item: Operation<F>> + 'static,
1189    {
1190        build_snapshot_parallel(
1191            self,
1192            context,
1193            inactivity_floor_loc,
1194            log,
1195            init_concurrency,
1196            init_buffer,
1197            cache_size,
1198        )
1199        .await
1200    }
1201}
1202
1203/// For the given `key` which is known to exist in the snapshot with location `old_loc`, update
1204/// its location to `new_loc`.
1205///
1206/// # Panics
1207///
1208/// Panics if `key` is not found in the snapshot or if `old_loc` is not found in the cursor.
1209fn update_known_loc<F: Family, I: Index<Value = Location<F>>>(
1210    snapshot: &mut I,
1211    key: &[u8],
1212    old_loc: Location<F>,
1213    new_loc: Location<F>,
1214) {
1215    let mut cursor = snapshot.get_mut(key).expect("key should be known to exist");
1216    assert!(
1217        cursor.find(|&loc| *loc == old_loc),
1218        "known key with given old_loc should have been found"
1219    );
1220    cursor.update(new_loc);
1221}
1222
1223/// For the given `key` which is known to exist in the snapshot with location `old_loc`, delete
1224/// it from the snapshot.
1225///
1226/// # Panics
1227///
1228/// Panics if `key` is not found in the snapshot or if `old_loc` is not found in the cursor.
1229fn delete_known_loc<F: Family, I: Index<Value = Location<F>>>(
1230    snapshot: &mut I,
1231    key: &[u8],
1232    old_loc: Location<F>,
1233) {
1234    let mut cursor = snapshot.get_mut(key).expect("key should be known to exist");
1235    assert!(
1236        cursor.find(|&loc| *loc == old_loc),
1237        "known key with given old_loc should have been found"
1238    );
1239    cursor.delete();
1240}