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 (uncommitted ancestors are applied automatically).
25//!
26//! The specific mutation methods vary by variant.
27//! See each variant's module documentation for the concrete API and usage examples.
28//!
29//! # Durability
30//!
31//! `commit()` makes applied state durable. `start_sync()` is its pipelined form, which also
32//! tries to advance the recovery watermark to bound startup recovery. `sync()` makes applied state
33//! durable and guarantees no recovery is needed on startup after a crash.
34//!
35//! # Ownership
36//!
37//! Mutating methods take the database by value and return it on success. If a mutating
38//! method returns an error, or its future is dropped before it finishes, the database is
39//! gone: state that was not yet durable is discarded, but everything already on disk stays
40//! recoverable. This applies to validation errors too (e.g. rejecting a stale batch);
41//! use each database's `validate_batch` to pre-check a batch without risking the handle.
42//!
43//! # Traits
44//!
45//! Keyed mutable variants ([any] and [current]) implement `any::traits::DbAny`.
46//!
47//! # Acknowledgments
48//!
49//! The following resources were used as references when implementing this crate:
50//!
51//! * [QMDB: Quick Merkle Database](https://arxiv.org/abs/2501.05262)
52//! * [Merkle Mountain
53//!   Ranges](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md)
54
55use crate::{
56    index::{
57        Cursor, Unordered as Index,
58        partitioned::{PartitionRange, Partitioned},
59    },
60    journal::{
61        Error as JournalError,
62        contiguous::{Contiguous, Mutable},
63    },
64    merkle::{
65        Bagging, Family, Location,
66        hasher::{Hasher as MerkleHasher, Standard as StandardHasher},
67    },
68    qmdb::operation::{Floored, Operation},
69    translator::Translator,
70};
71use commonware_codec::Encode;
72use commonware_cryptography::Hasher;
73use commonware_runtime::{ReadOptions, Spawner};
74use commonware_utils::{
75    bitmap::{Atomic, BitMap},
76    cache::Clock,
77    channel::mpsc,
78};
79use core::{num::NonZeroUsize, ops::Range};
80use futures::{StreamExt as _, future::join_all, pin_mut};
81use std::sync::Arc;
82use thiserror::Error;
83
84pub mod any;
85pub mod batch_chain;
86pub(crate) mod bitmap;
87pub(crate) mod compact;
88#[cfg(test)]
89mod conformance;
90pub mod current;
91pub mod immutable;
92pub mod keyless;
93mod metrics;
94pub mod operation;
95pub mod store;
96pub mod sync;
97pub mod verify;
98
99pub use verify::{
100    create_multi_proof, create_proof_store, verify_multi_proof, verify_proof,
101    verify_proof_and_extract_digests, verify_proof_and_pinned_nodes,
102};
103
104/// Merkle peak bagging policy used by QMDB operation roots.
105pub(crate) const ROOT_BAGGING: Bagging = Bagging::BackwardFold;
106
107/// Return the Merkle hasher configuration used by QMDB operation roots and proofs.
108pub const fn hasher<H: Hasher>() -> StandardHasher<H> {
109    StandardHasher::new(ROOT_BAGGING)
110}
111
112/// Return the root of an operation log containing only `operation`.
113///
114/// This lets database variants derive their initial root from the bootstrap commit without
115/// opening a database.
116fn single_operation_root<F: Family, H: Hasher>(operation: &impl Encode) -> H::Digest {
117    let hasher = hasher::<H>();
118    let leaf = MerkleHasher::<F>::leaf_digest(
119        &hasher,
120        F::location_to_position(Location::new(0)),
121        &operation.encode(),
122    );
123    MerkleHasher::<F>::root(&hasher, Location::new(1), 0, [&leaf])
124        .expect("a single-leaf Merkle root is always valid")
125}
126
127/// Look up the inactivity floor declared at the commit immediately preceding `op_count`.
128///
129/// `op_count` must be a non-zero commit-boundary historical size: the operation at `op_count - 1`
130/// must itself be a commit op (one for which `floor_of` returns `Some`).
131///
132/// # Errors
133///
134/// - [`Error::HistoricalFloorPruned`] if `op_count` is zero (no preceding commit exists), or if
135///   `op_count - 1` is retained but is not a commit op (either because the caller passed a
136///   non-commit-boundary size, or because pruning removed the commit that would have governed this
137///   size).
138/// - [`JournalError::ItemPruned`] if `op_count - 1` precedes the oldest retained location.
139pub(crate) async fn find_inactivity_floor_at<F, R>(
140    reader: &R,
141    op_count: Location<F>,
142) -> Result<Location<F>, Error<F>>
143where
144    F: Family,
145    R: Contiguous<Item: Floored<F>>,
146{
147    let Some(last_op) = op_count.checked_sub(1) else {
148        return Err(Error::HistoricalFloorPruned(op_count));
149    };
150    let last_op = *last_op;
151    let bounds = reader.bounds();
152    if last_op < bounds.start {
153        return Err(JournalError::ItemPruned(last_op).into());
154    }
155
156    let op = reader.read(last_op).await?;
157    let floor = op
158        .has_floor()
159        .ok_or(Error::HistoricalFloorPruned(op_count))?;
160    if floor > Location::new(last_op) {
161        return Err(Error::DataCorrupted(
162            "inactivity floor exceeds commit location",
163        ));
164    }
165    Ok(floor)
166}
167
168/// Compute the inactive peak count for a historical operation count.
169pub(crate) async fn inactive_peaks_at<F, R>(
170    reader: &R,
171    op_count: Location<F>,
172) -> Result<usize, Error<F>>
173where
174    F: Family,
175    R: Contiguous<Item: Floored<F>>,
176{
177    if op_count == Location::new(0) {
178        return Ok(0);
179    }
180
181    let floor = find_inactivity_floor_at::<F, _>(reader, op_count).await?;
182    Ok(F::inactive_peaks(op_count, floor))
183}
184
185/// Errors that can occur when interacting with an authenticated database.
186#[derive(Error, Debug)]
187pub enum Error<F: Family> {
188    #[error("data corrupted: {0}")]
189    DataCorrupted(&'static str),
190
191    #[error("merkle error: {0}")]
192    Merkle(#[from] crate::merkle::Error<F>),
193
194    #[error("metadata error: {0}")]
195    Metadata(#[from] crate::metadata::Error),
196
197    #[error("journal error: {0}")]
198    Journal(#[from] crate::journal::Error),
199
200    #[error("runtime error: {0}")]
201    Runtime(#[from] commonware_runtime::Error),
202
203    #[error("operation pruned: {0}")]
204    OperationPruned(Location<F>),
205
206    /// The requested key was not found in the snapshot.
207    #[error("key not found")]
208    KeyNotFound,
209
210    /// The key exists in the db, so we cannot prove its exclusion.
211    #[error("key exists")]
212    KeyExists,
213
214    #[error("unexpected data at location: {0}")]
215    UnexpectedData(Location<F>),
216
217    #[error("location out of bounds: {0} >= {1}")]
218    LocationOutOfBounds(Location<F>, Location<F>),
219
220    #[error("prune location {0} beyond minimum required location {1}")]
221    PruneBeyondMinRequired(Location<F>, Location<F>),
222
223    /// The batch was created from a different database state than the current one.
224    ///
225    /// See [`batch_chain`] for more details on staleness detection.
226    #[error("stale batch: current database state does not match the batch")]
227    StaleBatch,
228
229    /// The batch's inactivity floor is lower than the database's current floor.
230    #[error("floor regressed: batch floor {0} < current floor {1}")]
231    FloorRegressed(Location<F>, Location<F>),
232
233    /// The batch's inactivity floor exceeds its own commit operation's location. The floor
234    /// must not sit past the commit, since a subsequent `prune(floor)` would then remove the
235    /// last readable commit from the journal.
236    #[error("floor beyond commit location: floor {0} > commit loc {1}")]
237    FloorBeyondSize(Location<F>, Location<F>),
238
239    /// The inactivity floor that governed the requested `historical_size` is not retrievable from
240    /// the journal, so the wrapper cannot derive the `inactive_peaks` count needed to construct a
241    /// proof matching the historical root.
242    ///
243    /// Historical proofs require `historical_size` to be a commit-boundary: the operation at
244    /// `historical_size - 1` must itself be a commit op declaring the governing floor. This error
245    /// fires when the caller passes a non-commit-boundary size, or when pruning has removed the
246    /// commit that would have governed the size.
247    #[error("historical floor pruned for size: {0}")]
248    HistoricalFloorPruned(Location<F>),
249}
250
251impl<F: Family> From<crate::journal::authenticated::Error<F>> for Error<F> {
252    fn from(e: crate::journal::authenticated::Error<F>) -> Self {
253        match e {
254            crate::journal::authenticated::Error::Journal(j) => Self::Journal(j),
255            crate::journal::authenticated::Error::Merkle(m) => Self::Merkle(m),
256        }
257    }
258}
259
260/// Builds the database's snapshot by replaying the log starting at the inactivity floor. Assumes
261/// the log is not pruned beyond the inactivity floor. The callback is invoked for each replayed
262/// operation, indicating activity status updates. The first argument of the callback is the
263/// activity status of the operation, and the second argument is the location of the operation it
264/// inactivates (if any). Returns the number of active keys in the db.
265///
266/// `init_buffer` sizes the replay read buffer (in bytes). `cache_size` bounds a
267/// `(location -> key)` cache that lets collision resolution resolve candidates from memory
268/// instead of re-reading the log; `None` disables it.
269pub(super) async fn build_snapshot_from_log<F, C, I, Fn>(
270    inactivity_floor_loc: crate::merkle::Location<F>,
271    reader: &C,
272    snapshot: &mut I,
273    init_buffer: NonZeroUsize,
274    cache_size: Option<NonZeroUsize>,
275    mut callback: Fn,
276) -> Result<usize, Error<F>>
277where
278    F: crate::merkle::Family,
279    C: Contiguous<Item: Operation<F>>,
280    I: Index<Value = crate::merkle::Location<F>>,
281    Fn: FnMut(bool, Option<crate::merkle::Location<F>>),
282{
283    let bounds = reader.bounds();
284    let stream = reader
285        .replay(*inactivity_floor_loc, init_buffer, ReadOptions::default())
286        .await?;
287    pin_mut!(stream);
288    let last_commit_loc = bounds.end.saturating_sub(1);
289
290    // Memoize `(location -> key)` for replayed update ops so collision resolution in
291    // `find_update_op` resolves candidates from memory instead of re-reading (and re-decoding) the
292    // log.
293    let mut cache = cache_size.map(Clock::<u64, <C::Item as Operation<F>>::Key>::new);
294
295    let mut active_keys: usize = 0;
296    while let Some(result) = stream.next().await {
297        let (loc, op) = result?;
298        if let Some(key) = op.key() {
299            if op.is_delete() {
300                let old_loc = delete_key(snapshot, reader, key, cache.as_mut()).await?;
301                callback(false, old_loc);
302                if old_loc.is_some() {
303                    active_keys -= 1;
304                }
305            } else if op.is_update() {
306                let new_loc = crate::merkle::Location::new(loc);
307                let old_loc = update_key(snapshot, reader, key, new_loc, cache.as_mut()).await?;
308                callback(true, old_loc);
309                if old_loc.is_none() {
310                    active_keys += 1;
311                }
312
313                // This update op is now a `find_update_op` candidate for later ops of its key.
314                if let Some(cache) = cache.as_mut() {
315                    cache.put(loc, key.clone());
316                }
317            }
318        } else if op.has_floor().is_some() {
319            callback(loc == last_commit_loc, None);
320        }
321    }
322
323    Ok(active_keys)
324}
325
326/// Delete `key` from the snapshot if it exists, using a stable log reader, and return the
327/// previously associated location.
328async fn delete_key<F, I, R>(
329    snapshot: &mut I,
330    reader: &R,
331    key: &<R::Item as Operation<F>>::Key,
332    cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
333) -> Result<Option<Location<F>>, Error<F>>
334where
335    F: Family,
336    I: Index<Value = Location<F>>,
337    R: Contiguous,
338    R::Item: Operation<F>,
339{
340    // If the translated key is in the snapshot, get a cursor to look for the key.
341    let Some(cursor) = snapshot.get_mut(key) else {
342        return Ok(None);
343    };
344    delete_at_cursor::<F, _, _>(cursor, reader, key, cache).await
345}
346
347/// Delete `key` at `cursor` (obtained from a `get_mut` lookup of `key`), returning its location if
348/// it was present among the cursor's conflicts. When supplied, the matched location is removed
349/// from `cache` with the snapshot deletion.
350async fn delete_at_cursor<F, C, R>(
351    mut cursor: C,
352    reader: &R,
353    key: &<R::Item as Operation<F>>::Key,
354    mut cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
355) -> Result<Option<Location<F>>, Error<F>>
356where
357    F: Family,
358    C: Cursor<Value = Location<F>>,
359    R: Contiguous,
360    R::Item: Operation<F>,
361{
362    // Find the matching key among all conflicts, then delete it.
363    let Some(loc) = find_update_op::<F, _>(reader, &mut cursor, key, cache.as_deref_mut()).await?
364    else {
365        return Ok(None);
366    };
367
368    // Cache entries mirror current snapshot locations, so invalidate the matched location with
369    // the authoritative deletion.
370    cursor.delete();
371    if let Some(cache) = cache {
372        cache.remove(&*loc);
373    }
374
375    Ok(Some(loc))
376}
377
378/// Update `key` in the snapshot using a stable log reader, returning its old location if present.
379async fn update_key<F, I, R>(
380    snapshot: &mut I,
381    reader: &R,
382    key: &<R::Item as Operation<F>>::Key,
383    new_loc: Location<F>,
384    cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
385) -> Result<Option<Location<F>>, Error<F>>
386where
387    F: Family,
388    I: Index<Value = Location<F>>,
389    R: Contiguous,
390    R::Item: Operation<F>,
391{
392    // If the translated key is not in the snapshot, insert the new location. Otherwise, get a
393    // cursor to look for the key.
394    let Some(cursor) = snapshot.get_mut_or_insert(key, new_loc) else {
395        return Ok(None);
396    };
397    update_at_cursor::<F, _, _>(cursor, reader, key, new_loc, cache).await
398}
399
400/// Update `key` to `new_loc` at `cursor` (obtained from a `get_mut_or_insert` lookup of `key`),
401/// returning its old location if it was present among the cursor's conflicts; otherwise `new_loc`
402/// is inserted at the cursor. When supplied, the matched old location is removed from `cache` with
403/// the snapshot update.
404async fn update_at_cursor<F, C, R>(
405    mut cursor: C,
406    reader: &R,
407    key: &<R::Item as Operation<F>>::Key,
408    new_loc: Location<F>,
409    mut cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
410) -> Result<Option<Location<F>>, Error<F>>
411where
412    F: Family,
413    C: Cursor<Value = Location<F>>,
414    R: Contiguous,
415    R::Item: Operation<F>,
416{
417    // Find the matching key among all conflicts, then update its location.
418    if let Some(loc) =
419        find_update_op::<F, _>(reader, &mut cursor, key, cache.as_deref_mut()).await?
420    {
421        // Removing the superseded cache entry with the snapshot update lets the caller reuse its
422        // slot for `new_loc` instead of evicting another live entry.
423        assert!(new_loc > loc);
424        cursor.update(new_loc);
425        if let Some(cache) = cache {
426            cache.remove(&*loc);
427        }
428        return Ok(Some(loc));
429    }
430
431    // The key wasn't in the snapshot, so add it to the cursor.
432    cursor.insert(new_loc);
433
434    Ok(None)
435}
436
437/// Find and return the location of the update operation for `key`, if it exists. The cursor is
438/// positioned at the matching location, and can be used to update or delete the key.
439async fn find_update_op<F, R>(
440    reader: &R,
441    cursor: &mut impl Cursor<Value = Location<F>>,
442    key: &<R::Item as Operation<F>>::Key,
443    mut cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
444) -> Result<Option<Location<F>>, Error<F>>
445where
446    F: Family,
447    R: Contiguous,
448    R::Item: Operation<F>,
449{
450    while let Some(&loc) = cursor.next() {
451        // Consult the cache first; on a miss, read the log and populate.
452        let matches = if let Some(k) = cache.as_deref().and_then(|c| c.get(&*loc)) {
453            *k == *key
454        } else {
455            let op = reader.read(*loc).await?;
456            let k = op.key().expect("operation without key");
457            let matches = *k == *key;
458
459            // Every caller immediately mutates a match. Admitting it here could evict a live
460            // candidate before the caller invalidates this location.
461            if !matches && let Some(cache) = cache.as_deref_mut() {
462                cache.put(*loc, k.clone());
463            }
464            matches
465        };
466        if matches {
467            return Ok(Some(loc));
468        }
469    }
470
471    Ok(None)
472}
473
474/// Number of operations the snapshot replay batches per worker-channel send during a parallel build.
475const SNAPSHOT_ROUTE_BATCH: usize = 4096;
476
477/// Bounded depth (in batches) of each per-worker channel during a parallel build. Backpressure keeps
478/// the replay from running arbitrarily far ahead of a slow worker.
479const SNAPSHOT_CHANNEL_DEPTH: usize = 4;
480
481/// A batch of keyed operations routed to a snapshot-build worker: each entry is the op's key, its
482/// location, and whether it is a delete.
483type RoutedBatch<K> = Vec<(K, u64, bool)>;
484
485/// Build one parallel-init worker's partial snapshot: apply the routed operations (streamed in log
486/// order over `rx`) to `index`, resolving translated-key collisions with the worker's own log
487/// `reader` and `(location -> key)` cache. Sets the bits of the range's active locations in the
488/// shared `active` bitmap (indexed over `activity`, the replayed region) and returns the populated
489/// worker index along with the range's active-key count.
490async fn build_snapshot_worker<F, C, R>(
491    log: Arc<C>,
492    mut rx: mpsc::Receiver<RoutedBatch<<C::Item as Operation<F>>::Key>>,
493    mut index: R,
494    activity: Range<u64>,
495    active: Arc<Atomic>,
496    cache_size: Option<NonZeroUsize>,
497) -> Result<(R, usize), Error<F>>
498where
499    F: Family,
500    C: Contiguous<Item: Operation<F>>,
501    R: PartitionRange<Value = Location<F>>,
502{
503    let mut cache = cache_size.map(Clock::<u64, <C::Item as Operation<F>>::Key>::new);
504    while let Some(batch) = rx.recv().await {
505        for (key, loc, is_delete) in batch {
506            if is_delete {
507                if let Some(cursor) = index.get_mut(&key) {
508                    delete_at_cursor::<F, _, _>(cursor, &*log, &key, cache.as_mut()).await?;
509                }
510            } else {
511                let new_loc = Location::new(loc);
512                if let Some(cursor) = index.get_mut_or_insert(&key, new_loc) {
513                    update_at_cursor::<F, _, _>(cursor, &*log, &key, new_loc, cache.as_mut())
514                        .await?;
515                }
516
517                // This update op is now a `find_update_op` candidate for later ops of its key.
518                // `key` is owned by this batch and unused after the update, so move it in.
519                if let Some(cache) = cache.as_mut() {
520                    cache.put(loc, key);
521                }
522            }
523        }
524    }
525
526    // Reconstruct this range's share of the activity bitmap (in parallel with the other workers)
527    // and count the active keys. Locations are partitioned across workers, so every bit has a
528    // single writer.
529    let mut active_keys = 0;
530    index.for_each_value(|loc| {
531        active.set(**loc - activity.start);
532        active_keys += 1;
533    });
534    Ok((index, active_keys))
535}
536
537/// Build a snapshot serially on the calling task via [build_snapshot_from_log], collecting each
538/// replayed location's activity status into a [BitMap]. Returns the number of active keys and the
539/// activity bitmap (see [SnapshotBuild::build_snapshot]).
540async fn build_snapshot_serial<F, C, I>(
541    inactivity_floor_loc: Location<F>,
542    reader: &C,
543    snapshot: &mut I,
544    init_buffer: NonZeroUsize,
545    cache_size: Option<NonZeroUsize>,
546) -> Result<(usize, BitMap), Error<F>>
547where
548    F: Family,
549    C: Contiguous<Item: Operation<F>>,
550    I: Index<Value = Location<F>>,
551{
552    // Track per-op transitions locally: push each op's status and clear the bit of any location it
553    // supersedes. The state after the last op is each location's final status.
554    let mut activity = BitMap::new();
555    let floor = *inactivity_floor_loc;
556    let active_keys = build_snapshot_from_log(
557        inactivity_floor_loc,
558        reader,
559        snapshot,
560        init_buffer,
561        cache_size,
562        |is_active, old_loc| {
563            activity.push(is_active);
564            if let Some(loc) = old_loc {
565                activity.set(*loc - floor, false);
566            }
567        },
568    )
569    .await?;
570    Ok((active_keys, activity))
571}
572
573/// Build a snapshot by splitting the log replay across parallel workers, each owning a contiguous
574/// range of the index's partitions (see [Partitioned]). Returns the number of active keys and
575/// the activity bitmap (see [SnapshotBuild::build_snapshot]).
576async fn build_snapshot_parallel<F, E, C, I>(
577    snapshot: &mut I,
578    context: E,
579    inactivity_floor_loc: Location<F>,
580    log: &Arc<C>,
581    init_concurrency: NonZeroUsize,
582    init_buffer: NonZeroUsize,
583    cache_size: Option<NonZeroUsize>,
584) -> Result<(usize, BitMap), Error<F>>
585where
586    F: Family,
587    E: Spawner,
588    C: Contiguous<Item: Operation<F>> + 'static,
589    I: Partitioned + Index<Value = Location<F>>,
590{
591    let count = snapshot.partition_count();
592    let workers = (init_concurrency.get() - 1).min(count);
593
594    // No workers: build on this task.
595    if workers == 0 {
596        return build_snapshot_serial(
597            inactivity_floor_loc,
598            &**log,
599            snapshot,
600            init_buffer,
601            cache_size,
602        )
603        .await;
604    }
605
606    let floor = *inactivity_floor_loc;
607    let range_size = count.div_ceil(workers);
608
609    // `range_size` rounds up, so `range_size * workers` can exceed `count`, leaving trailing
610    // ranges empty (and a naive `count - lo` would underflow). Reduce to the number of
611    // non-empty ranges so routing (`p / range_size`) stays in `[0, workers)`.
612    let workers = count.div_ceil(range_size);
613    let per_worker_cache = cache_size.and_then(|n| NonZeroUsize::new(n.get() / workers));
614    let end = log.bounds().end;
615
616    // All workers share one atomic bitmap to track the activity bits.
617    let active = Arc::new(Atomic::zeroes(end - floor));
618
619    // Spawn one worker per contiguous partition range, each owning its own reader and cache.
620    let mut senders = Vec::with_capacity(workers);
621    let mut handles = Vec::with_capacity(workers);
622    for w in 0..workers {
623        let (tx, rx) = mpsc::channel(SNAPSHOT_CHANNEL_DEPTH);
624        senders.push(tx);
625        let log = log.clone();
626
627        // This worker owns the contiguous partition range [lo, lo + range_len). It allocates
628        // only that many slots, so per-worker memory is the range, not the full partition set.
629        let lo = w * range_size;
630        let range_len = range_size.min(count - lo);
631        let worker_index = snapshot.new_range(lo, range_len);
632        let active = active.clone();
633        let handle = context
634            .child("snapshot_worker")
635            .with_attribute("worker", w)
636            .dedicated()
637            .spawn(move |_| {
638                build_snapshot_worker::<F, C, I::Range>(
639                    log,
640                    rx,
641                    worker_index,
642                    floor..end,
643                    active,
644                    per_worker_cache,
645                )
646            });
647        handles.push(handle);
648    }
649
650    // Replay the log once and route each keyed op to the worker owning its partition.
651    // Routing runs in an inner future so any replay failure is captured rather than
652    // returned immediately: returning while the worker handles are merely dropped would
653    // leave the workers running detached, retaining the log and their range allocations
654    // after init has already failed. The stream is also released before the join.
655    let routing_result: Result<(), Error<F>> = async {
656        let stream = log
657            .replay(floor, init_buffer, ReadOptions::default())
658            .await?;
659        pin_mut!(stream);
660        let mut batches: Vec<RoutedBatch<_>> = (0..workers)
661            .map(|_| Vec::with_capacity(SNAPSHOT_ROUTE_BATCH))
662            .collect();
663
664        // A closed channel means a worker terminated early (e.g. returned an `Error<F>`
665        // while resolving a collision). Stop routing on the first such send failure and
666        // let the join below surface that worker's error, rather than panicking on the
667        // send.
668        while let Some(result) = stream.next().await {
669            let (loc, op) = result?;
670            let is_delete = op.is_delete();
671            let Some(key) = op.into_key() else { continue };
672            let w = I::partition_of(key.as_ref()) / range_size;
673            batches[w].push((key, loc, is_delete));
674            if batches[w].len() >= SNAPSHOT_ROUTE_BATCH {
675                let batch =
676                    std::mem::replace(&mut batches[w], Vec::with_capacity(SNAPSHOT_ROUTE_BATCH));
677                if senders[w].send(batch).await.is_err() {
678                    return Ok(());
679                }
680            }
681        }
682
683        // Flush remaining batches before the channels close.
684        for (w, batch) in batches.into_iter().enumerate() {
685            if !batch.is_empty() && senders[w].send(batch).await.is_err() {
686                break;
687            }
688        }
689        Ok(())
690    }
691    .await;
692
693    // Close the channels so each worker's stream terminates and it returns its index.
694    drop(senders);
695
696    // Join workers before surfacing any replay failure, so none outlive a failed init.
697    let joined = join_all(handles).await;
698    routing_result?;
699
700    // Install each worker's partition range into the snapshot and fold its active-key count in.
701    let mut total_items = 0;
702    for handle in joined {
703        let (worker_index, worker_keys) = handle??;
704        snapshot.install_range(worker_index);
705        total_items += worker_keys;
706    }
707
708    // The join reclaimed exclusive ownership of the shared bitmap, so it can be read back.
709    let mut active = Arc::into_inner(active)
710        .expect("workers were joined")
711        .into_bitmap();
712
713    // The last operation is the final commit (a log always ends with one), which stays active.
714    // An empty log has none.
715    if let Some(last_commit) = end.checked_sub(1)
716        && last_commit >= floor
717    {
718        active.set(last_commit - floor, true);
719    }
720
721    Ok((total_items, active))
722}
723
724/// Builds a database's snapshot index from the operations log.
725///
726/// Generic over the `Index` type so each index controls how it builds: serially with the default
727/// method body, or split across parallel workers with an override.
728///
729/// Sealed: only in-crate index types implement this, so internal invariants (e.g. builds must
730/// drop every clone of the shared log before returning) are enforced by the implementations
731/// rather than the public contract.
732pub trait SnapshotBuild<F: Family>:
733    sealed::SnapshotBuildSealed + Index<Value = Location<F>> + Sized + 'static
734{
735    /// The concurrency configuration the build consumes. Index types that always build serially
736    /// declare `()`, so a setting they cannot use is unrepresentable.
737    type Concurrency: Copy + Send + 'static;
738
739    /// Replay `log` from `inactivity_floor_loc`, populating `self`. Returns the number of active
740    /// keys and the activity status of every replayed location, in location order: a location's
741    /// bit is set iff it holds the current operation of an active key or is the last commit.
742    ///
743    /// `init_buffer` sizes the replay read buffer (in bytes), and `cache_size` bounds each
744    /// build's `(location -> key)` cache (`None` disables it).
745    // In-crate callers await this future at concrete index types, so the flexibility an explicit
746    // `Send` bound on the returned future would add is unused.
747    #[allow(async_fn_in_trait)]
748    async fn build_snapshot<E, C>(
749        &mut self,
750        _context: E,
751        inactivity_floor_loc: Location<F>,
752        log: &Arc<C>,
753        _init_concurrency: Self::Concurrency,
754        init_buffer: NonZeroUsize,
755        cache_size: Option<NonZeroUsize>,
756    ) -> Result<(usize, BitMap), Error<F>>
757    where
758        E: Spawner,
759        C: Contiguous<Item: Operation<F>> + 'static,
760    {
761        build_snapshot_serial(inactivity_floor_loc, &**log, self, init_buffer, cache_size).await
762    }
763}
764
765mod sealed {
766    use crate::translator::Translator;
767
768    pub trait SnapshotBuildSealed {}
769    impl<T: Translator, V: Send + Sync> SnapshotBuildSealed for crate::index::unordered::Index<T, V> {}
770    impl<T: Translator, V: Send + Sync> SnapshotBuildSealed for crate::index::ordered::Index<T, V> {}
771    impl<T: Translator, V: Send + Sync, const P: usize> SnapshotBuildSealed
772        for crate::index::partitioned::unordered::Index<T, V, P>
773    {
774    }
775    impl<T: Translator, V: Send + Sync, const P: usize> SnapshotBuildSealed
776        for crate::index::partitioned::ordered::Index<T, V, P>
777    {
778    }
779}
780
781impl<F: Family, T: Translator> SnapshotBuild<F> for crate::index::unordered::Index<T, Location<F>> {
782    type Concurrency = ();
783}
784impl<F: Family, T: Translator> SnapshotBuild<F> for crate::index::ordered::Index<T, Location<F>> {
785    type Concurrency = ();
786}
787
788impl<F: Family, T: Translator, const P: usize> SnapshotBuild<F>
789    for crate::index::partitioned::unordered::Index<T, Location<F>, P>
790{
791    type Concurrency = NonZeroUsize;
792
793    async fn build_snapshot<E, C>(
794        &mut self,
795        context: E,
796        inactivity_floor_loc: Location<F>,
797        log: &Arc<C>,
798        init_concurrency: NonZeroUsize,
799        init_buffer: NonZeroUsize,
800        cache_size: Option<NonZeroUsize>,
801    ) -> Result<(usize, BitMap), Error<F>>
802    where
803        E: Spawner,
804        C: Contiguous<Item: Operation<F>> + 'static,
805    {
806        build_snapshot_parallel(
807            self,
808            context,
809            inactivity_floor_loc,
810            log,
811            init_concurrency,
812            init_buffer,
813            cache_size,
814        )
815        .await
816    }
817}
818
819impl<F: Family, T: Translator, const P: usize> SnapshotBuild<F>
820    for crate::index::partitioned::ordered::Index<T, Location<F>, P>
821{
822    type Concurrency = NonZeroUsize;
823
824    async fn build_snapshot<E, C>(
825        &mut self,
826        context: E,
827        inactivity_floor_loc: Location<F>,
828        log: &Arc<C>,
829        init_concurrency: NonZeroUsize,
830        init_buffer: NonZeroUsize,
831        cache_size: Option<NonZeroUsize>,
832    ) -> Result<(usize, BitMap), Error<F>>
833    where
834        E: Spawner,
835        C: Contiguous<Item: Operation<F>> + 'static,
836    {
837        build_snapshot_parallel(
838            self,
839            context,
840            inactivity_floor_loc,
841            log,
842            init_concurrency,
843            init_buffer,
844            cache_size,
845        )
846        .await
847    }
848}
849
850/// For the given `key` which is known to exist in the snapshot with location `old_loc`, update
851/// its location to `new_loc`.
852///
853/// # Panics
854///
855/// Panics if `key` is not found in the snapshot or if `old_loc` is not found in the cursor.
856fn update_known_loc<F: Family, I: Index<Value = Location<F>>>(
857    snapshot: &mut I,
858    key: &[u8],
859    old_loc: Location<F>,
860    new_loc: Location<F>,
861) {
862    let mut cursor = snapshot.get_mut(key).expect("key should be known to exist");
863    assert!(
864        cursor.find(|&loc| *loc == old_loc),
865        "known key with given old_loc should have been found"
866    );
867    cursor.update(new_loc);
868}
869
870/// For the given `key` which is known to exist in the snapshot with location `old_loc`, delete
871/// it from the snapshot.
872///
873/// # Panics
874///
875/// Panics if `key` is not found in the snapshot or if `old_loc` is not found in the cursor.
876fn delete_known_loc<F: Family, I: Index<Value = Location<F>>>(
877    snapshot: &mut I,
878    key: &[u8],
879    old_loc: Location<F>,
880) {
881    let mut cursor = snapshot.get_mut(key).expect("key should be known to exist");
882    assert!(
883        cursor.find(|&loc| *loc == old_loc),
884        "known key with given old_loc should have been found"
885    );
886    cursor.delete();
887}
888
889/// A wrapper of DB state required for implementing inactivity floor management.
890pub(crate) struct FloorHelper<
891    'a,
892    F: Family,
893    I: Index<Value = Location<F>>,
894    C: Mutable<Item: Operation<F>>,
895> {
896    pub snapshot: &'a mut I,
897    pub log: C,
898}
899
900impl<F, I, C> FloorHelper<'_, F, I, C>
901where
902    F: Family,
903    I: Index<Value = Location<F>>,
904    C: Mutable<Item: Operation<F>>,
905{
906    /// Moves the given operation to the tip of the log if it is active, rendering its old location
907    /// inactive. If the operation was not active, then this is a no-op. Returns the helper and
908    /// whether the operation was moved.
909    async fn move_op_if_active(
910        mut self,
911        op: C::Item,
912        old_loc: Location<F>,
913    ) -> Result<(Self, bool), Error<F>> {
914        let Some(key) = op.key() else {
915            return Ok((self, false)); // operations without keys cannot be active
916        };
917
918        // If we find a snapshot entry corresponding to the operation, we know it's active.
919        let active = {
920            let Some(mut cursor) = self.snapshot.get_mut(key) else {
921                return Ok((self, false));
922            };
923            if cursor.find(|&loc| loc == old_loc) {
924                // Update the operation's snapshot location to point to tip.
925                cursor.update(Location::<F>::new(self.log.bounds().end));
926                true
927            } else {
928                false
929            }
930        };
931        if !active {
932            return Ok((self, false));
933        }
934
935        // Apply the operation at tip.
936        (self.log, _) = self.log.append(&op).await?;
937
938        Ok((self, true))
939    }
940
941    /// Raise the inactivity floor by taking one _step_, which involves searching for the first
942    /// active operation above the inactivity floor, moving it to tip, and then setting the
943    /// inactivity floor to the location following the moved operation. This method is therefore
944    /// guaranteed to raise the floor by at least one. Returns the helper and the new inactivity
945    /// floor location.
946    ///
947    /// # Panics
948    ///
949    /// Expects there is at least one active operation above the inactivity floor, and panics
950    /// otherwise.
951    async fn raise_floor(
952        mut self,
953        mut inactivity_floor_loc: Location<F>,
954    ) -> Result<(Self, Location<F>), Error<F>> {
955        let tip_loc: Location<F> = Location::new(self.log.bounds().end);
956        loop {
957            assert!(
958                *inactivity_floor_loc < tip_loc,
959                "no active operations above the inactivity floor"
960            );
961            let old_loc = inactivity_floor_loc;
962            inactivity_floor_loc += 1;
963            let op = self.log.read(*old_loc).await?;
964            let moved;
965            (self, moved) = self.move_op_if_active(op, old_loc).await?;
966            if moved {
967                return Ok((self, inactivity_floor_loc));
968            }
969        }
970    }
971}