Skip to main content

commonware_storage/qmdb/immutable/
batch.rs

1//! Batch mutation API for Immutable QMDBs.
2
3use super::Immutable;
4use crate::{
5    journal::{authenticated, contiguous::Mutable},
6    merkle::{Family, Location},
7    qmdb::{
8        any::{batch::lookup_sorted, ValueEncoding},
9        batch_chain::{self, Bounds},
10        immutable::operation::Operation,
11        operation::Key,
12        Error,
13    },
14    translator::Translator,
15    Context,
16};
17use commonware_codec::EncodeShared;
18use commonware_cryptography::{Digest, Hasher};
19use commonware_parallel::Strategy;
20use std::{
21    collections::BTreeMap,
22    sync::{Arc, Weak},
23};
24
25type DiffVec<K, F, V> = Vec<(K, DiffEntry<F, V>)>;
26
27/// What happened to a key in this batch.
28#[derive(Clone)]
29pub(crate) struct DiffEntry<F: Family, V> {
30    pub(crate) value: V,
31    pub(crate) loc: Location<F>,
32}
33
34/// A speculative batch of operations whose root digest has not yet been computed, in contrast
35/// to [`MerkleizedBatch`].
36///
37/// Consuming [`UnmerkleizedBatch::merkleize`] produces an `Arc<MerkleizedBatch>`.
38/// Methods that need the committed DB (e.g. [`get`](Self::get)) accept it as a parameter.
39#[allow(clippy::type_complexity)]
40pub struct UnmerkleizedBatch<F, H, K, V, S: Strategy>
41where
42    F: Family,
43    K: Key,
44    V: ValueEncoding,
45    H: Hasher,
46{
47    /// Authenticated journal batch for computing the speculative Merkle root.
48    journal_batch: authenticated::UnmerkleizedBatch<F, H, Operation<F, K, V>, S>,
49
50    /// Pending mutations.
51    mutations: BTreeMap<K, V::Value>,
52
53    /// Parent batch in the chain. `None` for batches created directly from the DB.
54    parent: Option<Arc<MerkleizedBatch<F, H::Digest, K, V, S>>>,
55
56    /// Total operation count before this batch (committed DB + prior batches).
57    /// This batch's i-th operation lands at location `base_size + i`.
58    base_size: u64,
59
60    /// The database size when this batch was created, used to detect stale batches.
61    db_size: u64,
62}
63
64/// Merkleized authenticated-journal batch wrapping an [`Operation`] payload.
65type JournalBatch<F, D, K, V, S> = Arc<authenticated::MerkleizedBatch<F, D, Operation<F, K, V>, S>>;
66
67/// A speculative batch of operations whose root digest has been computed,
68/// in contrast to [`UnmerkleizedBatch`].
69#[derive(Clone)]
70pub struct MerkleizedBatch<F: Family, D: Digest, K: Key, V: ValueEncoding, S: Strategy> {
71    /// Authenticated journal batch (Merkle state + local items).
72    pub(super) journal_batch: JournalBatch<F, D, K, V, S>,
73
74    /// Cached operations root after applying this batch.
75    pub(super) root: D,
76
77    /// This batch's local key-level changes only (not accumulated from ancestors).
78    /// Sorted by key with no duplicates; queried via `lookup_sorted` (binary search).
79    pub(super) diff: Arc<DiffVec<K, F, V::Value>>,
80
81    /// The parent batch in the chain, if any.
82    pub(super) parent: Option<Weak<Self>>,
83
84    /// Arc refs to each ancestor's diff, collected during `merkleize()` while the parent
85    /// is alive. Used by `apply_batch` to apply uncommitted ancestor snapshot diffs.
86    /// 1:1 with `bounds.ancestors` (same length, same ordering).
87    pub(super) ancestor_diffs: Vec<Arc<DiffVec<K, F, V::Value>>>,
88
89    /// Position and floor bounds for this batch chain.
90    pub(super) bounds: batch_chain::Bounds<F>,
91}
92
93impl<F, H, K, V, S: Strategy> UnmerkleizedBatch<F, H, K, V, S>
94where
95    F: Family,
96    K: Key,
97    V: ValueEncoding,
98    H: Hasher,
99    Operation<F, K, V>: EncodeShared,
100{
101    /// Create a batch from a committed DB (no parent chain).
102    pub(super) fn new<E, C, T>(
103        immutable: &Immutable<F, E, K, V, C, H, T, S>,
104        journal_size: u64,
105    ) -> Self
106    where
107        E: Context,
108        C: Mutable<Item = Operation<F, K, V>>,
109        C::Item: EncodeShared,
110        T: Translator,
111    {
112        Self {
113            journal_batch: immutable.journal.new_batch(),
114            mutations: BTreeMap::new(),
115            parent: None,
116            base_size: journal_size,
117            db_size: journal_size,
118        }
119    }
120
121    /// Set a key to a value.
122    ///
123    /// If the key already exists in the database or an ancestor batch, reads
124    /// of it may return any of its written values.
125    pub fn set(mut self, key: K, value: V::Value) -> Self {
126        self.mutations.insert(key, value);
127        self
128    }
129
130    /// Read through: mutations -> ancestor diffs -> committed DB.
131    pub async fn get<E, C, T>(
132        &self,
133        key: &K,
134        db: &Immutable<F, E, K, V, C, H, T, S>,
135    ) -> Result<Option<V::Value>, Error<F>>
136    where
137        E: Context,
138        C: Mutable<Item = Operation<F, K, V>>,
139        C::Item: EncodeShared,
140        T: Translator,
141    {
142        // Check this batch's pending mutations.
143        if let Some(value) = self.mutations.get(key) {
144            return Ok(Some(value.clone()));
145        }
146        // Walk parent chain. The first parent is a strong Arc (held by UnmerkleizedBatch),
147        // subsequent parents are Weak refs.
148        if let Some(parent) = self.parent.as_ref() {
149            if let Some(entry) = lookup_sorted(parent.diff.as_slice(), key) {
150                return Ok(Some(entry.value.clone()));
151            }
152            for batch in parent.ancestors() {
153                if let Some(entry) = lookup_sorted(batch.diff.as_slice(), key) {
154                    return Ok(Some(entry.value.clone()));
155                }
156            }
157        }
158        // Fall through to base DB.
159        db.get(key).await
160    }
161
162    /// Batch read multiple keys.
163    ///
164    /// Returns results in the same order as the input keys.
165    pub async fn get_many<E, C, T>(
166        &self,
167        keys: &[&K],
168        db: &Immutable<F, E, K, V, C, H, T, S>,
169    ) -> Result<Vec<Option<V::Value>>, Error<F>>
170    where
171        E: Context,
172        C: Mutable<Item = Operation<F, K, V>>,
173        C::Item: EncodeShared,
174        T: Translator,
175    {
176        if keys.is_empty() {
177            return Ok(Vec::new());
178        }
179
180        let mut results: Vec<Option<V::Value>> = Vec::with_capacity(keys.len());
181        let mut db_indices = Vec::new();
182        let mut db_keys = Vec::new();
183
184        for (i, key) in keys.iter().enumerate() {
185            // Check local mutations.
186            if let Some(value) = self.mutations.get(*key) {
187                results.push(Some(value.clone()));
188                continue;
189            }
190
191            // Check parent diff chain.
192            let mut found = false;
193            if let Some(parent) = self.parent.as_ref() {
194                if let Some(entry) = lookup_sorted(parent.diff.as_slice(), *key) {
195                    results.push(Some(entry.value.clone()));
196                    found = true;
197                }
198                if !found {
199                    for batch in parent.ancestors() {
200                        if let Some(entry) = lookup_sorted(batch.diff.as_slice(), *key) {
201                            results.push(Some(entry.value.clone()));
202                            found = true;
203                            break;
204                        }
205                    }
206                }
207            }
208
209            if found {
210                continue;
211            }
212
213            // Need DB fallthrough.
214            db_indices.push(i);
215            db_keys.push(*key);
216            results.push(None);
217        }
218
219        if !db_keys.is_empty() {
220            let db_results = db.get_many(&db_keys).await?;
221            for (slot, value) in db_indices.into_iter().zip(db_results) {
222                results[slot] = value;
223            }
224        }
225
226        Ok(results)
227    }
228
229    /// Resolve mutations into operations, merkleize, and return an `Arc<MerkleizedBatch>`.
230    ///
231    /// `inactivity_floor` declares that all operations before this location are inactive.
232    /// It must be >= the database's current inactivity floor (monotonically non-decreasing).
233    #[tracing::instrument(name = "qmdb.immutable.batch.merkleize", level = "info", skip_all)]
234    pub async fn merkleize<E, C, T>(
235        self,
236        db: &Immutable<F, E, K, V, C, H, T, S>,
237        metadata: Option<V::Value>,
238        inactivity_floor: Location<F>,
239    ) -> Arc<MerkleizedBatch<F, H::Digest, K, V, S>>
240    where
241        E: Context,
242        C: Mutable<Item = Operation<F, K, V>>,
243        C::Item: EncodeShared,
244        T: Translator,
245    {
246        let base = self.base_size;
247
248        // Build operations: one Set per key, then Commit. `self.mutations` is a BTreeMap, so
249        // iteration yields keys in sorted order, which `diff` relies on for binary search.
250        let mut ops: Vec<Operation<F, K, V>> = Vec::with_capacity(self.mutations.len() + 1);
251        let mut diff: DiffVec<K, F, V::Value> = Vec::with_capacity(self.mutations.len());
252
253        for (key, value) in self.mutations {
254            let loc = Location::new(base + ops.len() as u64);
255            ops.push(Operation::Set(key.clone(), value.clone()));
256            diff.push((key, DiffEntry { value, loc }));
257        }
258        assert!(diff.is_sorted_by(|a, b| a.0 < b.0));
259
260        ops.push(Operation::Commit(metadata, inactivity_floor));
261
262        let total_size = base + ops.len() as u64;
263        let inactive_peaks = F::inactive_peaks(
264            F::location_to_position(Location::new(total_size)),
265            inactivity_floor,
266        );
267
268        // Leaf and node hashing dominate merkleization, so run them as one job on the
269        // strategy instead of occupying the calling task (see `Journal::merkleize`).
270        let (journal, root) = db
271            .journal
272            .merkleize(self.journal_batch, ops, inactive_peaks)
273            .await
274            .expect("inactive_peaks computed from batch size");
275
276        // Compute the batch chain bounds.
277        let mut ancestor_diffs = Vec::new();
278        let mut ancestors = Vec::new();
279        for batch in
280            batch_chain::parent_and_ancestors(self.parent.as_ref(), |parent| parent.ancestors())
281        {
282            ancestor_diffs.push(Arc::clone(&batch.diff));
283            ancestors.push(batch_chain::AncestorBounds {
284                floor: batch.bounds.inactivity_floor,
285                end: batch.bounds.total_size,
286            });
287        }
288
289        Arc::new(MerkleizedBatch {
290            journal_batch: journal,
291            root,
292            diff: Arc::new(diff),
293            parent: self.parent.as_ref().map(Arc::downgrade),
294            ancestor_diffs,
295            bounds: batch_chain::Bounds {
296                base_size: self.base_size,
297                db_size: self.db_size,
298                total_size,
299                ancestors,
300                inactivity_floor,
301            },
302        })
303    }
304}
305
306impl<F: Family, D: Digest, K: Key, V: ValueEncoding, S: Strategy> MerkleizedBatch<F, D, K, V, S>
307where
308    Operation<F, K, V>: EncodeShared,
309{
310    /// Return the speculative root.
311    pub const fn root(&self) -> D {
312        self.root
313    }
314
315    /// Return the [`Bounds`] of the batch.
316    pub const fn bounds(&self) -> &Bounds<F> {
317        &self.bounds
318    }
319
320    /// Iterate over ancestor batches (parent first, then grandparent, etc.).
321    pub(super) fn ancestors(&self) -> impl Iterator<Item = Arc<Self>> {
322        batch_chain::ancestors(self.parent.clone(), |batch| batch.parent.as_ref())
323    }
324
325    /// Read through: local diff -> ancestor diffs -> committed DB.
326    pub async fn get<E, C, H, T>(
327        &self,
328        key: &K,
329        db: &Immutable<F, E, K, V, C, H, T, S>,
330    ) -> Result<Option<V::Value>, Error<F>>
331    where
332        E: Context,
333        C: Mutable<Item = Operation<F, K, V>>,
334        C::Item: EncodeShared,
335        H: Hasher<Digest = D>,
336        T: Translator,
337    {
338        if let Some(entry) = lookup_sorted(self.diff.as_slice(), key) {
339            return Ok(Some(entry.value.clone()));
340        }
341        for batch in self.ancestors() {
342            if let Some(entry) = lookup_sorted(batch.diff.as_slice(), key) {
343                return Ok(Some(entry.value.clone()));
344            }
345        }
346        db.get(key).await
347    }
348
349    /// Batch read multiple keys.
350    ///
351    /// Returns results in the same order as the input keys.
352    pub async fn get_many<E, C, H, T>(
353        &self,
354        keys: &[&K],
355        db: &Immutable<F, E, K, V, C, H, T, S>,
356    ) -> Result<Vec<Option<V::Value>>, Error<F>>
357    where
358        E: Context,
359        C: Mutable<Item = Operation<F, K, V>>,
360        C::Item: EncodeShared,
361        H: Hasher<Digest = D>,
362        T: Translator,
363    {
364        if keys.is_empty() {
365            return Ok(Vec::new());
366        }
367
368        let mut results: Vec<Option<V::Value>> = Vec::with_capacity(keys.len());
369        let mut db_indices = Vec::new();
370        let mut db_keys = Vec::new();
371
372        for (i, key) in keys.iter().enumerate() {
373            // Check local diff.
374            if let Some(entry) = lookup_sorted(self.diff.as_slice(), *key) {
375                results.push(Some(entry.value.clone()));
376                continue;
377            }
378
379            // Walk parent chain.
380            let mut found = false;
381            for batch in self.ancestors() {
382                if let Some(entry) = lookup_sorted(batch.diff.as_slice(), *key) {
383                    results.push(Some(entry.value.clone()));
384                    found = true;
385                    break;
386                }
387            }
388
389            if found {
390                continue;
391            }
392
393            // Need DB fallthrough.
394            db_indices.push(i);
395            db_keys.push(*key);
396            results.push(None);
397        }
398
399        if !db_keys.is_empty() {
400            let db_results = db.get_many(&db_keys).await?;
401            for (slot, value) in db_indices.into_iter().zip(db_results) {
402                results[slot] = value;
403            }
404        }
405
406        Ok(results)
407    }
408
409    /// Create a new speculative batch of operations with this batch as its parent.
410    ///
411    /// All uncommitted ancestors in the chain must be kept alive until the child (or any
412    /// descendant) is merkleized. Dropping an uncommitted ancestor causes data
413    /// loss detected at `apply_batch` time.
414    pub fn new_batch<H>(self: &Arc<Self>) -> UnmerkleizedBatch<F, H, K, V, S>
415    where
416        H: Hasher<Digest = D>,
417    {
418        UnmerkleizedBatch {
419            journal_batch: self.journal_batch.new_batch::<H>(),
420            mutations: BTreeMap::new(),
421            parent: Some(Arc::clone(self)),
422            base_size: self.bounds.total_size,
423            db_size: self.bounds.db_size,
424        }
425    }
426}
427
428impl<F, E, K, V, C, H, T, S> Immutable<F, E, K, V, C, H, T, S>
429where
430    F: Family,
431    E: Context,
432    K: Key,
433    V: ValueEncoding,
434    C: Mutable<Item = Operation<F, K, V>>,
435    C::Item: EncodeShared,
436    H: Hasher,
437    T: Translator,
438    S: Strategy,
439{
440    /// Create an initial [`MerkleizedBatch`] from the committed DB state.
441    pub fn to_batch(&self) -> Arc<MerkleizedBatch<F, H::Digest, K, V, S>> {
442        let journal_size = *self.last_commit_loc + 1;
443        Arc::new(MerkleizedBatch {
444            journal_batch: self.journal.to_merkleized_batch(),
445            root: self.root,
446            diff: Arc::new(Vec::new()),
447            parent: None,
448            ancestor_diffs: Vec::new(),
449            bounds: batch_chain::Bounds {
450                base_size: journal_size,
451                db_size: journal_size,
452                total_size: journal_size,
453                ancestors: Vec::new(),
454                inactivity_floor: self.inactivity_floor_loc,
455            },
456        })
457    }
458}