Skip to content

Commit 35455c3

Browse files
fix bitmap batch parent chain growth with rwlock
1 parent 5cce12d commit 35455c3

4 files changed

Lines changed: 268 additions & 285 deletions

File tree

storage/src/qmdb/current/batch.rs

Lines changed: 113 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ use crate::{
2828
use ahash::AHasher;
2929
use commonware_codec::Codec;
3030
use commonware_cryptography::{Digest, Hasher};
31-
use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
31+
use commonware_utils::{
32+
bitmap::{Prunable as BitMap, Readable as BitmapReadable},
33+
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
34+
};
3235
use std::{
3336
collections::{BTreeSet, HashMap},
3437
hash::BuildHasherDefault,
@@ -279,11 +282,34 @@ where
279282
bitmap_parent: BitmapBatch<N>,
280283
}
281284

282-
/// A speculative batch of operations whose root digest has been computed,
283-
/// in contrast to [`UnmerkleizedBatch`].
285+
/// A speculative batch of operations whose root digest has been computed, in contrast to
286+
/// [`UnmerkleizedBatch`].
287+
///
288+
/// Wraps an [`any::batch::MerkleizedBatch`] and adds the bitmap and grafted MMR state needed to
289+
/// compute the canonical root.
290+
///
291+
/// # Stale reads
292+
///
293+
/// A `MerkleizedBatch` holds a reference to the DB's committed bitmap via [`BitmapBatch::Base`].
294+
/// That bitmap evolves in place as [`Db::apply_batch`](super::db::Db::apply_batch) commits batches.
295+
/// Reads through this batch's chain (via its own `get`, by constructing child batches, or by
296+
/// anything that walks the bitmap chain) are only semantically correct if this batch's chain is
297+
/// still a valid prefix of the DB's committed state. That is, every
298+
/// [`apply_batch`](super::db::Db::apply_batch) call since this batch was merkleized has applied an
299+
/// ancestor of *this* batch.
300+
///
301+
/// **The library does not guard against stale reads.** If you apply an unrelated sibling or any
302+
/// other non-ancestor batch and then read through this one, chunk reads fall through to the
303+
/// (now-advanced) committed bitmap while your overlays are still applied on top, producing
304+
/// incoherent state that doesn't correspond to any valid DB view.
284305
///
285-
/// Wraps an [`any::batch::MerkleizedBatch`] and adds the bitmap and grafted MMR state needed
286-
/// to compute the canonical root.
306+
/// Rules of thumb:
307+
/// - Drop any `Arc<MerkleizedBatch>` you no longer intend to apply.
308+
/// - Extending a batch after `apply_batch` has consumed it (building a child off the just-applied
309+
/// parent) is safe. The committed bitmap now equals the parent's post-apply state, so child
310+
/// reads are consistent.
311+
/// - Extending a batch after a *different* branch has been applied is not safe and will silently
312+
/// return wrong data.
287313
pub struct MerkleizedBatch<F: Graftable, D: Digest, U: update::Update + Send + Sync, const N: usize>
288314
where
289315
Operation<F, U>: Send + Sync,
@@ -595,7 +621,6 @@ where
595621
// compute_db_root sees newly completed chunks. Using bitmap_parent alone would miss chunks
596622
// that transitioned from partial to complete in this batch.
597623
let bitmap_batch = BitmapBatch::Layer(Arc::new(BitmapBatchLayer {
598-
pruned_chunks: bitmap_parent.pruned_chunks(),
599624
parent: bitmap_parent.clone(),
600625
overlay: Arc::new(overlay),
601626
}));
@@ -637,13 +662,86 @@ where
637662
}))
638663
}
639664

640-
/// Immutable bitmap state at any point in a batch chain.
665+
/// The committed bitmap shared between the [`Db`](super::db::Db) and live batches.
666+
///
667+
/// Wrapped in a [`RwLock`] so that [`Db::apply_batch`](super::db::Db::apply_batch),
668+
/// [`Db::prune`](super::db::Db::prune), and [`Db::rewind`](super::db::Db::rewind) can mutate
669+
/// the bitmap in place while live batches concurrently read through it. Batches never write;
670+
/// write access is kept module-private to `current::db`.
671+
///
672+
/// # Stale reads
673+
///
674+
/// The bitmap behind this lock represents *committed* state. If a caller holds a `MerkleizedBatch`
675+
/// whose chain is no longer a valid prefix of committed state (e.g., an orphaned sibling branch
676+
/// after the other branch was applied), reads through that chain's `Base` will silently return
677+
/// inconsistent data (the chain's overlays mixed with post-divergence committed chunks). The
678+
/// library does not guard against this; callers must avoid reading through stale batches.
679+
pub(crate) struct SharedBitmap<const N: usize> {
680+
inner: RwLock<BitMap<N>>,
681+
}
682+
683+
impl<const N: usize> SharedBitmap<N> {
684+
pub(crate) const fn new(bitmap: BitMap<N>) -> Self {
685+
Self {
686+
inner: RwLock::new(bitmap),
687+
}
688+
}
689+
690+
/// Acquire a shared read guard over the committed bitmap.
691+
pub(crate) fn read(&self) -> RwLockReadGuard<'_, BitMap<N>> {
692+
self.inner.read()
693+
}
694+
695+
/// Acquire an exclusive write guard. Only [`Db::apply_batch`](super::db::Db::apply_batch),
696+
/// [`Db::prune`](super::db::Db::prune), and [`Db::rewind`](super::db::Db::rewind) mutate the
697+
/// shared bitmap.
698+
pub(super) fn write(&self) -> RwLockWriteGuard<'_, BitMap<N>> {
699+
self.inner.write()
700+
}
701+
}
702+
703+
impl<const N: usize> std::fmt::Debug for SharedBitmap<N> {
704+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705+
f.debug_struct("SharedBitmap")
706+
.field("bitmap_len", &BitmapReadable::<N>::len(&*self.read()))
707+
.finish()
708+
}
709+
}
710+
711+
/// [`BitmapReadable`] over the DB's committed bitmap. Each call acquires the read lock briefly.
712+
impl<const N: usize> BitmapReadable<N> for SharedBitmap<N> {
713+
fn complete_chunks(&self) -> usize {
714+
self.read().complete_chunks()
715+
}
716+
717+
fn get_chunk(&self, idx: usize) -> [u8; N] {
718+
*self.read().get_chunk(idx)
719+
}
720+
721+
fn last_chunk(&self) -> ([u8; N], u64) {
722+
let guard = self.read();
723+
let (chunk, bits) = guard.last_chunk();
724+
(*chunk, bits)
725+
}
726+
727+
fn pruned_chunks(&self) -> usize {
728+
self.read().pruned_chunks()
729+
}
730+
731+
fn len(&self) -> u64 {
732+
BitmapReadable::<N>::len(&*self.read())
733+
}
734+
}
735+
736+
/// A view of the committed bitmap plus zero or more speculative overlay [`Layer`]s.
641737
///
642-
/// Mirrors the [`crate::merkle::batch::MerkleizedBatch`] pattern.
738+
/// The chain terminates in a [`Base`] that references the shared committed bitmap. No staleness
739+
/// check is performed. Callers must ensure they only read through batches whose chains are
740+
/// still valid prefixes of committed state (see [`SharedBitmap`]'s docs).
643741
#[derive(Clone, Debug)]
644742
pub(crate) enum BitmapBatch<const N: usize> {
645-
/// Committed bitmap (chain terminal).
646-
Base(Arc<BitMap<N>>),
743+
/// Chain terminal: shared reference to the committed bitmap.
744+
Base(Arc<SharedBitmap<N>>),
647745
/// Speculative layer on top of a parent batch.
648746
Layer(Arc<BitmapBatchLayer<N>>),
649747
}
@@ -654,10 +752,6 @@ pub(crate) struct BitmapBatchLayer<const N: usize> {
654752
pub(crate) parent: BitmapBatch<N>,
655753
/// Chunk-level overlay: materialized bytes for every chunk that differs from parent.
656754
pub(crate) overlay: Arc<ChunkOverlay<N>>,
657-
/// Pruned-chunk count, copied from `parent` at construction. Invariant across the whole
658-
/// layer chain (pruning only happens on the committed base), so caching here lets
659-
/// `BitmapBatch::pruned_chunks` return in O(1) instead of walking to the Base.
660-
pub(crate) pruned_chunks: usize,
661755
}
662756

663757
impl<const N: usize> BitmapBatch<N> {
@@ -675,7 +769,7 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
675769
let mut current = self;
676770
loop {
677771
match current {
678-
Self::Base(bm) => return *bm.get_chunk(idx),
772+
Self::Base(shared) => return *shared.read().get_chunk(idx),
679773
Self::Layer(layer) => {
680774
if let Some(&chunk) = layer.overlay.get(idx) {
681775
return chunk;
@@ -703,95 +797,19 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
703797

704798
fn pruned_chunks(&self) -> usize {
705799
match self {
706-
Self::Base(bm) => bm.pruned_chunks(),
707-
Self::Layer(layer) => layer.pruned_chunks,
800+
Self::Base(shared) => shared.read().pruned_chunks(),
801+
Self::Layer(layer) => layer.parent.pruned_chunks(),
708802
}
709803
}
710804

711805
fn len(&self) -> u64 {
712806
match self {
713-
Self::Base(bm) => BitmapReadable::<N>::len(bm.as_ref()),
807+
Self::Base(shared) => BitmapReadable::<N>::len(&*shared.read()),
714808
Self::Layer(layer) => layer.overlay.len,
715809
}
716810
}
717811
}
718812

719-
impl<const N: usize> BitmapBatch<N> {
720-
/// Apply a chunk overlay to this bitmap. When `self` is `Base` with sole ownership, writes
721-
/// overlay chunks directly into the bitmap. Otherwise creates a new `Layer`.
722-
pub(super) fn apply_overlay(&mut self, overlay: Arc<ChunkOverlay<N>>) {
723-
// Fast path: write overlay chunks directly into the Base bitmap.
724-
if let Self::Base(base) = self {
725-
if let Some(bitmap) = Arc::get_mut(base) {
726-
// Extend bitmap to the overlay's length.
727-
bitmap.extend_to(overlay.len);
728-
// Overwrite dirty chunks.
729-
for (&idx, chunk_bytes) in &overlay.chunks {
730-
if idx >= bitmap.pruned_chunks() {
731-
bitmap.set_chunk_by_index(idx, chunk_bytes);
732-
}
733-
}
734-
return;
735-
}
736-
}
737-
738-
// Slow path: create a new layer.
739-
let pruned_chunks = self.pruned_chunks();
740-
let parent = self.clone();
741-
*self = Self::Layer(Arc::new(BitmapBatchLayer {
742-
parent,
743-
overlay,
744-
pruned_chunks,
745-
}));
746-
}
747-
748-
/// Flatten all layers back to a single `Base(Arc<BitMap<N>>)`.
749-
///
750-
/// After flattening, the new `Base` Arc has refcount 1 (assuming no external clones
751-
/// are held).
752-
pub(super) fn flatten(&mut self) {
753-
if matches!(self, Self::Base(_)) {
754-
return;
755-
}
756-
757-
// Take ownership of the chain so that Arc refcounts are not
758-
// artificially inflated by a clone.
759-
let mut owned = std::mem::replace(self, Self::Base(Arc::new(BitMap::default())));
760-
761-
// Collect overlays from tip to base.
762-
let mut overlays = Vec::new();
763-
let base = loop {
764-
match owned {
765-
Self::Base(bm) => break bm,
766-
Self::Layer(layer) => match Arc::try_unwrap(layer) {
767-
Ok(inner) => {
768-
overlays.push(inner.overlay);
769-
owned = inner.parent;
770-
}
771-
Err(arc) => {
772-
overlays.push(arc.overlay.clone());
773-
owned = arc.parent.clone();
774-
}
775-
},
776-
}
777-
};
778-
779-
// Apply overlays from base to tip.
780-
let mut bitmap = Arc::try_unwrap(base).unwrap_or_else(|arc| (*arc).clone());
781-
for overlay in overlays.into_iter().rev() {
782-
// Extend bitmap to the overlay's length.
783-
bitmap.extend_to(overlay.len);
784-
// Apply dirty chunks.
785-
for (&idx, chunk_bytes) in &overlay.chunks {
786-
if idx >= bitmap.pruned_chunks() {
787-
bitmap.set_chunk_by_index(idx, chunk_bytes);
788-
}
789-
}
790-
}
791-
*self = Self::Base(Arc::new(bitmap));
792-
}
793-
}
794-
795813
impl<F: Graftable, D: Digest, U: update::Update + Send + Sync, const N: usize>
796814
MerkleizedBatch<F, D, U, N>
797815
where
@@ -861,7 +879,7 @@ where
861879
Arc::new(MerkleizedBatch {
862880
inner: self.any.to_batch(),
863881
grafted,
864-
bitmap: self.status.clone(),
882+
bitmap: BitmapBatch::Base(Arc::clone(&self.status)),
865883
canonical_root: self.root,
866884
})
867885
}
@@ -1301,33 +1319,4 @@ mod tests {
13011319
// Empty bitmap, tip = 0: no candidates.
13021320
assert_eq!(scan.next_candidate(Location::new(0), 0), None);
13031321
}
1304-
1305-
#[test]
1306-
fn test_apply_overlay() {
1307-
// Base: 8 bits all set, sole owner -> fast path.
1308-
let base = make_bitmap(&[true; 8]);
1309-
let mut batch = BitmapBatch::Base(Arc::new(base));
1310-
1311-
let mut overlay = ChunkOverlay::new(12);
1312-
let mut c0 = [0u8; N];
1313-
c0[0] = 0b1111_0111; // bits 0-7 set except bit 3
1314-
c0[1] = 0b0000_0100; // bit 10 set
1315-
overlay.chunks.insert(0, c0);
1316-
batch.apply_overlay(Arc::new(overlay));
1317-
1318-
// Fast path keeps Base, extends length, applies chunks.
1319-
assert!(matches!(batch, BitmapBatch::Base(_)));
1320-
assert_eq!(batch.len(), 12);
1321-
assert_eq!(batch.get_chunk(0)[0] & (1 << 3), 0);
1322-
assert_ne!(batch.get_chunk(0)[1] & (1 << 2), 0);
1323-
1324-
// Shared Arc -> slow path creates Layer.
1325-
let BitmapBatch::Base(ref base_arc) = batch else {
1326-
panic!("expected Base");
1327-
};
1328-
let _shared = Arc::clone(base_arc);
1329-
let overlay2 = ChunkOverlay::new(12);
1330-
batch.apply_overlay(Arc::new(overlay2));
1331-
assert!(matches!(batch, BitmapBatch::Layer(_)));
1332-
}
13331322
}

0 commit comments

Comments
 (0)