Skip to content

Commit 9f1e720

Browse files
fix bitmap batch parent chain growth with rwlock
1 parent ddbaefe commit 9f1e720

4 files changed

Lines changed: 219 additions & 296 deletions

File tree

storage/src/qmdb/current/batch.rs

Lines changed: 109 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use crate::{
2727
};
2828
use commonware_codec::Codec;
2929
use commonware_cryptography::{Digest, Hasher};
30-
use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
30+
use commonware_utils::{
31+
bitmap::{Prunable as BitMap, Readable as BitmapReadable},
32+
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
33+
};
3134
use std::{
3235
collections::{BTreeMap, BTreeSet},
3336
sync::Arc,
@@ -279,6 +282,29 @@ where
279282
///
280283
/// Wraps an [`any::batch::MerkleizedBatch`] and adds the bitmap and grafted MMR state needed
281284
/// to compute the canonical root.
285+
///
286+
/// # Stale reads
287+
///
288+
/// A `MerkleizedBatch` holds a reference to the DB's committed bitmap via
289+
/// [`BitmapBatch::Base`]. That bitmap evolves in place as [`Db::apply_batch`](super::db::Db)
290+
/// commits batches. Reads through this batch's chain (via its own `get`, by constructing child
291+
/// batches, or by anything that walks the bitmap chain) are only semantically correct if this
292+
/// batch's chain is still a valid prefix of the DB's committed state — i.e., every
293+
/// [`apply_batch`](super::db::Db::apply_batch) call since this batch was merkleized applied an
294+
/// ancestor of *this* batch.
295+
///
296+
/// **The library does not guard against stale reads.** If you apply an unrelated sibling or any
297+
/// other non-ancestor batch and then read through this one, chunk reads fall through to the
298+
/// (now-advanced) committed bitmap while your overlays are still applied on top, producing
299+
/// incoherent state that doesn't correspond to any valid DB view.
300+
///
301+
/// Rules of thumb:
302+
/// - Drop any `Arc<MerkleizedBatch>` you no longer intend to apply.
303+
/// - Extending a batch after `apply_batch` has consumed it (building a child off the
304+
/// just-applied parent) is safe — the committed bitmap now equals the parent's post-apply
305+
/// state, so child reads are consistent.
306+
/// - Extending a batch after a *different* branch has been applied is not safe and will
307+
/// silently return wrong data.
282308
pub struct MerkleizedBatch<F: Graftable, D: Digest, U: update::Update + Send + Sync, const N: usize>
283309
where
284310
Operation<F, U>: Send + Sync,
@@ -590,7 +616,6 @@ where
590616
// compute_db_root sees newly completed chunks. Using bitmap_parent alone would miss chunks
591617
// that transitioned from partial to complete in this batch.
592618
let bitmap_batch = BitmapBatch::Layer(Arc::new(BitmapBatchLayer {
593-
pruned_chunks: bitmap_parent.pruned_chunks(),
594619
parent: bitmap_parent.clone(),
595620
overlay: Arc::new(overlay),
596621
}));
@@ -632,13 +657,86 @@ where
632657
}))
633658
}
634659

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

658752
impl<const N: usize> BitmapBatch<N> {
@@ -666,7 +760,7 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
666760

667761
fn get_chunk(&self, idx: usize) -> [u8; N] {
668762
match self {
669-
Self::Base(bm) => *bm.get_chunk(idx),
763+
Self::Base(shared) => *shared.read().get_chunk(idx),
670764
Self::Layer(layer) => {
671765
// Check overlay first; fall through to parent if unmodified.
672766
if let Some(&chunk) = layer.overlay.get(idx) {
@@ -695,95 +789,19 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
695789

696790
fn pruned_chunks(&self) -> usize {
697791
match self {
698-
Self::Base(bm) => bm.pruned_chunks(),
699-
Self::Layer(layer) => layer.pruned_chunks,
792+
Self::Base(shared) => shared.read().pruned_chunks(),
793+
Self::Layer(layer) => layer.parent.pruned_chunks(),
700794
}
701795
}
702796

703797
fn len(&self) -> u64 {
704798
match self {
705-
Self::Base(bm) => BitmapReadable::<N>::len(bm.as_ref()),
799+
Self::Base(shared) => BitmapReadable::<N>::len(&*shared.read()),
706800
Self::Layer(layer) => layer.overlay.len,
707801
}
708802
}
709803
}
710804

711-
impl<const N: usize> BitmapBatch<N> {
712-
/// Apply a chunk overlay to this bitmap. When `self` is `Base` with sole ownership, writes
713-
/// overlay chunks directly into the bitmap. Otherwise creates a new `Layer`.
714-
pub(super) fn apply_overlay(&mut self, overlay: Arc<ChunkOverlay<N>>) {
715-
// Fast path: write overlay chunks directly into the Base bitmap.
716-
if let Self::Base(base) = self {
717-
if let Some(bitmap) = Arc::get_mut(base) {
718-
// Extend bitmap to the overlay's length.
719-
bitmap.extend_to(overlay.len);
720-
// Overwrite dirty chunks.
721-
for (&idx, chunk_bytes) in &overlay.chunks {
722-
if idx >= bitmap.pruned_chunks() {
723-
bitmap.set_chunk_by_index(idx, chunk_bytes);
724-
}
725-
}
726-
return;
727-
}
728-
}
729-
730-
// Slow path: create a new layer.
731-
let pruned_chunks = self.pruned_chunks();
732-
let parent = self.clone();
733-
*self = Self::Layer(Arc::new(BitmapBatchLayer {
734-
parent,
735-
overlay,
736-
pruned_chunks,
737-
}));
738-
}
739-
740-
/// Flatten all layers back to a single `Base(Arc<BitMap<N>>)`.
741-
///
742-
/// After flattening, the new `Base` Arc has refcount 1 (assuming no external clones
743-
/// are held).
744-
pub(super) fn flatten(&mut self) {
745-
if matches!(self, Self::Base(_)) {
746-
return;
747-
}
748-
749-
// Take ownership of the chain so that Arc refcounts are not
750-
// artificially inflated by a clone.
751-
let mut owned = std::mem::replace(self, Self::Base(Arc::new(BitMap::default())));
752-
753-
// Collect overlays from tip to base.
754-
let mut overlays = Vec::new();
755-
let base = loop {
756-
match owned {
757-
Self::Base(bm) => break bm,
758-
Self::Layer(layer) => match Arc::try_unwrap(layer) {
759-
Ok(inner) => {
760-
overlays.push(inner.overlay);
761-
owned = inner.parent;
762-
}
763-
Err(arc) => {
764-
overlays.push(arc.overlay.clone());
765-
owned = arc.parent.clone();
766-
}
767-
},
768-
}
769-
};
770-
771-
// Apply overlays from base to tip.
772-
let mut bitmap = Arc::try_unwrap(base).unwrap_or_else(|arc| (*arc).clone());
773-
for overlay in overlays.into_iter().rev() {
774-
// Extend bitmap to the overlay's length.
775-
bitmap.extend_to(overlay.len);
776-
// Apply dirty chunks.
777-
for (&idx, chunk_bytes) in &overlay.chunks {
778-
if idx >= bitmap.pruned_chunks() {
779-
bitmap.set_chunk_by_index(idx, chunk_bytes);
780-
}
781-
}
782-
}
783-
*self = Self::Base(Arc::new(bitmap));
784-
}
785-
}
786-
787805
impl<F: Graftable, D: Digest, U: update::Update + Send + Sync, const N: usize>
788806
MerkleizedBatch<F, D, U, N>
789807
where
@@ -853,7 +871,7 @@ where
853871
Arc::new(MerkleizedBatch {
854872
inner: self.any.to_batch(),
855873
grafted,
856-
bitmap: self.status.clone(),
874+
bitmap: BitmapBatch::Base(Arc::clone(&self.status)),
857875
canonical_root: self.root,
858876
})
859877
}
@@ -1294,32 +1312,4 @@ mod tests {
12941312
assert_eq!(scan.next_candidate(Location::new(0), 0), None);
12951313
}
12961314

1297-
#[test]
1298-
fn test_apply_overlay() {
1299-
// Base: 8 bits all set, sole owner -> fast path.
1300-
let base = make_bitmap(&[true; 8]);
1301-
let mut batch = BitmapBatch::Base(Arc::new(base));
1302-
1303-
let mut overlay = ChunkOverlay::new(12);
1304-
let mut c0 = [0u8; N];
1305-
c0[0] = 0b1111_0111; // bits 0-7 set except bit 3
1306-
c0[1] = 0b0000_0100; // bit 10 set
1307-
overlay.chunks.insert(0, c0);
1308-
batch.apply_overlay(Arc::new(overlay));
1309-
1310-
// Fast path keeps Base, extends length, applies chunks.
1311-
assert!(matches!(batch, BitmapBatch::Base(_)));
1312-
assert_eq!(batch.len(), 12);
1313-
assert_eq!(batch.get_chunk(0)[0] & (1 << 3), 0);
1314-
assert_ne!(batch.get_chunk(0)[1] & (1 << 2), 0);
1315-
1316-
// Shared Arc -> slow path creates Layer.
1317-
let BitmapBatch::Base(ref base_arc) = batch else {
1318-
panic!("expected Base");
1319-
};
1320-
let _shared = Arc::clone(base_arc);
1321-
let overlay2 = ChunkOverlay::new(12);
1322-
batch.apply_overlay(Arc::new(overlay2));
1323-
assert!(matches!(batch, BitmapBatch::Layer(_)));
1324-
}
13251315
}

0 commit comments

Comments
 (0)