Skip to content

Commit 9b5d072

Browse files
committed
PR 3627 nits
1 parent 6b7fab8 commit 9b5d072

2 files changed

Lines changed: 189 additions & 14 deletions

File tree

storage/src/qmdb/current/batch.rs

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ where
634634
let bitmap_batch = BitmapBatch::Layer(Arc::new(BitmapBatchLayer {
635635
parent: bitmap_parent.clone(),
636636
overlay: Arc::new(overlay),
637+
shared: Arc::clone(bitmap_parent.shared()),
637638
}));
638639

639640
// Compute canonical root. The grafted batch alone cannot resolve committed nodes,
@@ -763,19 +764,19 @@ pub(crate) struct BitmapBatchLayer<const N: usize> {
763764
pub(crate) parent: BitmapBatch<N>,
764765
/// Chunk-level overlay: materialized bytes for every chunk that differs from parent.
765766
pub(crate) overlay: Arc<ChunkOverlay<N>>,
767+
/// Cached terminal [`SharedBitmap`] so [`BitmapBatch::shared`] and
768+
/// [`BitmapBatch::pruned_chunks`] answer in O(1) instead of walking the chain.
769+
pub(crate) shared: Arc<SharedBitmap<N>>,
766770
}
767771

768772
impl<const N: usize> BitmapBatch<N> {
769773
const CHUNK_SIZE_BITS: u64 = BitMap::<N>::CHUNK_SIZE_BITS;
770774

771-
/// Walk to the terminal [`SharedBitmap`] at the bottom of the chain.
775+
/// Return the terminal [`SharedBitmap`] at the bottom of the chain.
772776
fn shared(&self) -> &Arc<SharedBitmap<N>> {
773-
let mut current = self;
774-
loop {
775-
match current {
776-
Self::Base(s) => return s,
777-
Self::Layer(layer) => current = &layer.parent,
778-
}
777+
match self {
778+
Self::Base(s) => s,
779+
Self::Layer(layer) => &layer.shared,
779780
}
780781
}
781782

@@ -799,6 +800,7 @@ impl<const N: usize> BitmapBatch<N> {
799800
result = Self::Layer(Arc::new(BitmapBatchLayer {
800801
parent: result,
801802
overlay,
803+
shared: Arc::clone(shared),
802804
}));
803805
}
804806
result
@@ -843,13 +845,7 @@ impl<const N: usize> BitmapReadable<N> for BitmapBatch<N> {
843845
}
844846

845847
fn pruned_chunks(&self) -> usize {
846-
let mut current = self;
847-
loop {
848-
match current {
849-
Self::Base(shared) => return shared.pruned_chunks(),
850-
Self::Layer(layer) => current = &layer.parent,
851-
}
852-
}
848+
self.shared().pruned_chunks()
853849
}
854850

855851
fn len(&self) -> u64 {
@@ -1380,4 +1376,124 @@ mod tests {
13801376
// Empty bitmap, tip = 0: no candidates.
13811377
assert_eq!(scan.next_candidate(Location::new(0), 0), None);
13821378
}
1379+
1380+
// ---- trim_committed tests ----
1381+
//
1382+
// `trim_committed` is called from `MerkleizedBatch::new_batch` to strip any `Layer`s whose
1383+
// overlays have already been absorbed into the shared committed bitmap by a prior apply.
1384+
// The implementation is a single loop that collects uncommitted overlays top-down and
1385+
// rebuilds a fresh chain rooted at `Base`. These tests cover distinct input shapes directly,
1386+
// without going through the full Db/batch machinery, so the function's structural output
1387+
// can be asserted.
1388+
1389+
/// Build a chain `Base(shared) -> Layer(len=L1) -> Layer(len=L2) -> ...` from a list of
1390+
/// overlay lengths (bottom to top). Each constructed `Layer` caches `shared` per the
1391+
/// struct's invariant.
1392+
fn make_chain(shared: &Arc<SharedBitmap<N>>, overlay_lens: &[u64]) -> BitmapBatch<N> {
1393+
let mut chain = BitmapBatch::Base(Arc::clone(shared));
1394+
for &len in overlay_lens {
1395+
chain = BitmapBatch::Layer(Arc::new(BitmapBatchLayer {
1396+
parent: chain,
1397+
overlay: Arc::new(ChunkOverlay::new(len)),
1398+
shared: Arc::clone(shared),
1399+
}));
1400+
}
1401+
chain
1402+
}
1403+
1404+
/// Walk a chain and return its overlay lengths in bottom-to-top order. Used to assert the
1405+
/// structural output of `trim_committed` without touching private fields. Panics if the
1406+
/// chain isn't terminated by a single `Base` at the bottom.
1407+
fn chain_overlays(batch: &BitmapBatch<N>) -> Vec<u64> {
1408+
let mut lens = Vec::new();
1409+
let mut current = batch;
1410+
while let BitmapBatch::Layer(layer) = current {
1411+
lens.push(layer.overlay.len);
1412+
current = &layer.parent;
1413+
}
1414+
assert!(matches!(current, BitmapBatch::Base(_)));
1415+
lens.reverse();
1416+
lens
1417+
}
1418+
1419+
/// Input is already a bare `Base` with no speculative layers on top — the loop body never
1420+
/// runs, `kept` stays empty, and the result is a freshly constructed `Base` pointing at the
1421+
/// same `SharedBitmap`. Real-world trigger: `MerkleizedBatch::new_batch` on a batch whose
1422+
/// chain was previously trimmed flat (e.g., immediately after an apply collapsed everything).
1423+
#[test]
1424+
fn trim_committed_already_base() {
1425+
let shared = Arc::new(SharedBitmap::<N>::new(make_bitmap(&[true; 64])));
1426+
let base = BitmapBatch::Base(Arc::clone(&shared));
1427+
let result = base.trim_committed();
1428+
// Still `Base`, pointing at the same shared terminal.
1429+
match result {
1430+
BitmapBatch::Base(s) => assert!(Arc::ptr_eq(&s, &shared)),
1431+
BitmapBatch::Layer(_) => panic!("expected Base"),
1432+
}
1433+
}
1434+
1435+
/// Every layer has been absorbed by prior applies — the loop breaks on the first iteration
1436+
/// and `kept` stays empty, so the result is a bare `Base`. This is the steady-state
1437+
/// "extend a just-applied batch" flow: after `apply_batch(A)`, `A`'s own layer has
1438+
/// `overlay.len == committed` and the next `new_batch` call should start from a clean
1439+
/// terminal.
1440+
#[test]
1441+
fn trim_committed_all_committed() {
1442+
// `shared.len() == 64`; the single layer's `overlay.len == 32 (<= 64)`, so it's committed.
1443+
let shared = Arc::new(SharedBitmap::<N>::new(make_bitmap(&[true; 64])));
1444+
let chain = make_chain(&shared, &[32]);
1445+
let result = chain.trim_committed();
1446+
// Collapsed to a bare Base, pointing at the original shared.
1447+
match result {
1448+
BitmapBatch::Base(s) => assert!(Arc::ptr_eq(&s, &shared)),
1449+
BitmapBatch::Layer(_) => panic!("expected Base after full trim"),
1450+
}
1451+
}
1452+
1453+
/// Every layer is still speculative — the loop walks all the way to `Base` without
1454+
/// breaking, and `kept` holds every overlay. The rebuilt chain is structurally equivalent
1455+
/// to the input (same overlay lens, same shared terminal). Real-world trigger: speculating
1456+
/// multiple batches deep (A, then B off A, then C off B) without `apply_batch` in between.
1457+
#[test]
1458+
fn trim_committed_none_committed() {
1459+
// `shared.len() == 32`; both overlays have `len > 32`, so neither is committed.
1460+
let shared = Arc::new(SharedBitmap::<N>::new(make_bitmap(&[true; 32])));
1461+
let chain = make_chain(&shared, &[64, 96]);
1462+
let result = chain.trim_committed();
1463+
// Structure must be preserved in bottom-to-top order.
1464+
assert_eq!(chain_overlays(&result), vec![64, 96]);
1465+
}
1466+
1467+
/// Exactly one layer is uncommitted (the newest) on top of a committed prefix — the
1468+
/// dominant pattern in chained growth. The loop collects the one uncommitted overlay, and
1469+
/// the rebuild produces `Layer(Base, overlay_B)`. Also verifies the rebuilt layer carries
1470+
/// the cached `shared` reference correctly. Real-world trigger: apply parent A, then B
1471+
/// held alive off A, then `B.new_batch()` to build C.
1472+
#[test]
1473+
fn trim_committed_exactly_one_uncommitted() {
1474+
// `shared.len() == 64`; committed layer (`overlay.len == 64`) + uncommitted (`96`).
1475+
let shared = Arc::new(SharedBitmap::<N>::new(make_bitmap(&[true; 64])));
1476+
let chain = make_chain(&shared, &[64, 96]);
1477+
let result = chain.trim_committed();
1478+
// The committed layer is gone; only the uncommitted overlay remains.
1479+
assert_eq!(chain_overlays(&result), vec![96]);
1480+
// And the rebuilt layer's `shared` field still points at the original terminal.
1481+
assert!(Arc::ptr_eq(result.shared(), &shared));
1482+
}
1483+
1484+
/// Two or more uncommitted layers on top of a committed prefix — exercises the loop's
1485+
/// iterated `kept.push` and the rebuild's iterated `Arc::new(BitmapBatchLayer)`, including
1486+
/// the cached `shared` wire-through on every reconstructed layer. Real-world trigger:
1487+
/// build A, then B off A, then C off B; apply only A; then call `C.new_batch()`.
1488+
#[test]
1489+
fn trim_committed_multiple_uncommitted() {
1490+
// `shared.len() == 64`; committed layer (64), then two uncommitted (96, 128).
1491+
let shared = Arc::new(SharedBitmap::<N>::new(make_bitmap(&[true; 64])));
1492+
let chain = make_chain(&shared, &[64, 96, 128]);
1493+
let result = chain.trim_committed();
1494+
// Committed layer dropped; uncommitted pair preserved in order.
1495+
assert_eq!(chain_overlays(&result), vec![96, 128]);
1496+
// Every reconstructed layer must still cache the original shared terminal.
1497+
assert!(Arc::ptr_eq(result.shared(), &shared));
1498+
}
13831499
}

storage/src/qmdb/current/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,6 +2969,65 @@ pub mod tests {
29692969
});
29702970
}
29712971

2972+
/// Build a child batch from a still-live parent whose apply was followed by a prune, then
2973+
/// merkleize and apply the child. The parent's `BitmapBatch` chain terminates in the shared
2974+
/// committed bitmap, and `prune` mutates that bitmap's pruning boundary in place. When the
2975+
/// child is constructed via `parent.new_batch()`, the internal `trim_committed` call must
2976+
/// observe the advanced boundary and produce a correct child chain; merkleize and apply must
2977+
/// then produce correct state for keys at and beyond the advanced floor.
2978+
#[test_traced("INFO")]
2979+
fn test_current_live_batch_child_after_prune() {
2980+
let executor = deterministic::Runner::default();
2981+
executor.start(|context| async move {
2982+
let ctx = context.with_label("db");
2983+
let mut db: UnorderedVariableDb = UnorderedVariableDb::init(
2984+
ctx.clone(),
2985+
variable_config::<OneCap>("child-after-prune", &ctx),
2986+
)
2987+
.await
2988+
.unwrap();
2989+
2990+
// Seed enough ops to span multiple bitmap chunks.
2991+
let mut seed = db.new_batch();
2992+
for i in 0u64..300 {
2993+
seed = seed.write(key(i), Some(val(i)));
2994+
}
2995+
let seed_m = seed.merkleize(&db, None).await.unwrap();
2996+
db.apply_batch(seed_m).await.unwrap();
2997+
db.commit().await.unwrap();
2998+
2999+
// Overwrite keys 0..250 so the inactivity floor advances past chunk 0.
3000+
let mut a_batch = db.new_batch();
3001+
for i in 0u64..250 {
3002+
a_batch = a_batch.write(key(i), Some(val(i + 10_000)));
3003+
}
3004+
let a = a_batch.merkleize(&db, None).await.unwrap();
3005+
db.apply_batch(Arc::clone(&a)).await.unwrap();
3006+
db.commit().await.unwrap();
3007+
3008+
// Prune while `a` is still live. Mutates the shared bitmap's pruning boundary in place.
3009+
let floor = db.inactivity_floor_loc();
3010+
db.prune(floor).await.unwrap();
3011+
3012+
// Extend `a` into `b` AFTER the prune. Building `b` off `a` triggers
3013+
// `trim_committed` on `a`'s chain, which must correctly see the advanced pruning
3014+
// boundary on the shared bitmap.
3015+
let b = a
3016+
.new_batch::<Sha256>()
3017+
.write(key(300), Some(val(300)))
3018+
.merkleize(&db, None)
3019+
.await
3020+
.unwrap();
3021+
3022+
db.apply_batch(b).await.unwrap();
3023+
assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(10_000)));
3024+
assert_eq!(db.get(&key(249)).await.unwrap(), Some(val(10_249)));
3025+
assert_eq!(db.get(&key(300)).await.unwrap(), Some(val(300)));
3026+
3027+
db.destroy().await.unwrap();
3028+
});
3029+
}
3030+
29723031
/// Regression: applying a batch after its ancestor Arc is dropped (without
29733032
/// committing) must still apply the ancestor's bitmap pushes/clears and
29743033
/// snapshot diffs.

0 commit comments

Comments
 (0)