diff --git a/storage/fuzz/fuzz_targets/fixed_journal_operations.rs b/storage/fuzz/fuzz_targets/fixed_journal_operations.rs index 58c515652f3..b5a73bbb715 100644 --- a/storage/fuzz/fuzz_targets/fixed_journal_operations.rs +++ b/storage/fuzz/fuzz_targets/fixed_journal_operations.rs @@ -133,15 +133,14 @@ fn fuzz(input: FuzzInput) { } JournalOperation::Read { pos } => { - let reader = journal.reader().await; - let bounds = reader.bounds(); + let bounds = journal.bounds(); if bounds.contains(pos) { - reader.read(*pos).await.unwrap(); + journal.read(*pos).await.unwrap(); } } JournalOperation::ReadMany { positions } => { - let reader = journal.reader().await; + let reader = journal.reader(); let bounds = reader.bounds(); // Map fuzz positions into valid, sorted, deduplicated positions let mut mapped: Vec = positions @@ -168,7 +167,7 @@ fn fuzz(input: FuzzInput) { } JournalOperation::Size => { - let size = journal.size().await; + let size = journal.size(); assert_eq!(journal_size, size, "unexpected size"); } @@ -181,26 +180,25 @@ fn fuzz(input: FuzzInput) { journal.rewind(*size).await.unwrap(); journal.sync().await.unwrap(); journal_size = *size; - oldest_retained_pos = journal.reader().await.bounds().start; + oldest_retained_pos = journal.bounds().start; } } JournalOperation::Bounds => { - let _bounds = journal.reader().await.bounds(); + let _bounds = journal.bounds(); } JournalOperation::Prune { min_pos } => { if *min_pos <= journal_size { journal.prune(*min_pos).await.unwrap(); - oldest_retained_pos = journal.reader().await.bounds().start; + oldest_retained_pos = journal.bounds().start; } } JournalOperation::Replay { buffer, start_pos } => { - let reader = journal.reader().await; - let bounds = reader.bounds(); + let bounds = journal.bounds(); let start_pos = bounds.start + (*start_pos % (bounds.end - bounds.start + 1)); - let replay = reader.replay(NZUsize!(*buffer), start_pos).await; + let replay = journal.replay(NZUsize!(*buffer), start_pos).await; match replay { Ok(stream) => { @@ -231,8 +229,8 @@ fn fuzz(input: FuzzInput) { .unwrap(); restarts += 1; // Reset tracking variables to match recovered state - journal_size = journal.size().await; - oldest_retained_pos = journal.reader().await.bounds().start; + journal_size = journal.size(); + oldest_retained_pos = journal.bounds().start; } JournalOperation::Destroy => { @@ -295,12 +293,12 @@ fn fuzz(input: FuzzInput) { let new_size = journal.rewind_to(|item| *item == target).await.unwrap(); journal.sync().await.unwrap(); journal_size = new_size; - oldest_retained_pos = journal.reader().await.bounds().start; + oldest_retained_pos = journal.reader().bounds().start; } } JournalOperation::TryReadSync { pos } => { - let reader = journal.reader().await; + let reader = journal.reader(); let bounds = reader.bounds(); if bounds.contains(pos) { // Cross-check: sync result must match async result @@ -312,7 +310,7 @@ fn fuzz(input: FuzzInput) { } JournalOperation::PruningBoundary => { - let boundary = journal.pruning_boundary().await; + let boundary = journal.pruning_boundary(); assert_eq!(boundary, oldest_retained_pos); } @@ -330,8 +328,8 @@ fn fuzz(input: FuzzInput) { .await .unwrap(); restarts += 1; - journal_size = journal.size().await; - oldest_retained_pos = journal.reader().await.bounds().start; + journal_size = journal.size(); + oldest_retained_pos = journal.reader().bounds().start; } } } diff --git a/storage/fuzz/fuzz_targets/journal_crash_recovery.rs b/storage/fuzz/fuzz_targets/journal_crash_recovery.rs index eee905f8865..bc9e727d34c 100644 --- a/storage/fuzz/fuzz_targets/journal_crash_recovery.rs +++ b/storage/fuzz/fuzz_targets/journal_crash_recovery.rs @@ -244,13 +244,13 @@ impl FuzzJournal for FixedJournal { } async fn size(&self) -> u64 { - FixedJournal::size(self).await + FixedJournal::size(self) } // Cannot use `async fn` here due to RPITIT Send auto-trait limitation. #[allow(clippy::manual_async_fn)] fn bounds(&self) -> impl Future> + Send { - async { self.reader().await.bounds() } + async { Reader::bounds(self) } } async fn append(&mut self, item: Item) -> Result { @@ -263,7 +263,7 @@ impl FuzzJournal for FixedJournal { &self, pos: u64, ) -> impl Future> + Send { - async move { self.reader().await.read(pos).await } + async move { Reader::read(self, pos).await } } async fn sync(&mut self) -> Result<(), commonware_storage::journal::Error> { @@ -283,7 +283,7 @@ impl FuzzJournal for FixedJournal { buffer: NonZeroUsize, start_pos: u64, ) -> Result<(), commonware_storage::journal::Error> { - let _ = self.reader().await.replay(buffer, start_pos).await?; + let _ = Reader::replay(self, buffer, start_pos).await?; Ok(()) } @@ -326,13 +326,13 @@ impl FuzzJournal for VariableJournal { } async fn size(&self) -> u64 { - VariableJournal::size(self).await + VariableJournal::size(self) } // Cannot use `async fn` here due to RPITIT Send auto-trait limitation. #[allow(clippy::manual_async_fn)] fn bounds(&self) -> impl Future> + Send { - async { self.reader().await.bounds() } + async { self.reader().bounds() } } async fn append(&mut self, item: Item) -> Result { @@ -345,7 +345,7 @@ impl FuzzJournal for VariableJournal { &self, pos: u64, ) -> impl Future> + Send { - async move { self.reader().await.read(pos).await } + async move { self.reader().read(pos).await } } async fn sync(&mut self) -> Result<(), commonware_storage::journal::Error> { @@ -365,7 +365,7 @@ impl FuzzJournal for VariableJournal { buffer: NonZeroUsize, start_pos: u64, ) -> Result<(), commonware_storage::journal::Error> { - let _ = self.reader().await.replay(buffer, start_pos).await?; + let _ = self.reader().replay(buffer, start_pos).await?; Ok(()) } diff --git a/storage/fuzz/fuzz_targets/ordinal_operations.rs b/storage/fuzz/fuzz_targets/ordinal_operations.rs index 1d290a2501a..54f9d75cd1c 100644 --- a/storage/fuzz/fuzz_targets/ordinal_operations.rs +++ b/storage/fuzz/fuzz_targets/ordinal_operations.rs @@ -242,7 +242,7 @@ fn fuzz(input: FuzzInput) { } OrdinalOperation::ReopenAfterOperations => { - if let Some(o) = store.take() { + if let Some(mut o) = store.take() { // Sync and drop the current ordinal o.sync().await.expect("failed to sync store before reopen failed"); drop(o); diff --git a/storage/fuzz/fuzz_targets/queue_crash_recovery.rs b/storage/fuzz/fuzz_targets/queue_crash_recovery.rs index 6d64283e7d4..c35aa995dd7 100644 --- a/storage/fuzz/fuzz_targets/queue_crash_recovery.rs +++ b/storage/fuzz/fuzz_targets/queue_crash_recovery.rs @@ -264,7 +264,7 @@ async fn run_operations( QueueOperation::DequeueAndAck => { if let Ok(Some((pos, _item))) = queue.dequeue().await { - if queue.ack(pos).await.is_ok() { + if queue.ack(pos).is_ok() { state.update_ack_floor(queue.ack_floor()); } } @@ -276,12 +276,12 @@ async fn run_operations( } QueueOperation::AckOffset { offset } => { - let size = queue.size().await; + let size = queue.size(); let ack_floor = queue.ack_floor(); if size > ack_floor { let range = size - ack_floor; let pos = ack_floor + (*offset as u64 % range); - match queue.ack(pos).await { + match queue.ack(pos) { Ok(()) => { state.update_ack_floor(queue.ack_floor()); } @@ -294,9 +294,9 @@ async fn run_operations( } QueueOperation::AckUpToOffset { offset } => { - let size = queue.size().await; + let size = queue.size(); let up_to = (*offset as u64) % (size + 1); - match queue.ack_up_to(up_to).await { + match queue.ack_up_to(up_to) { Ok(()) => { state.update_ack_floor(queue.ack_floor()); } @@ -338,7 +338,7 @@ async fn run_operations( /// that the queue can be re-initialized and used again for basic operations. async fn verify_recovery_after_mutable_error(queue: &mut Queue>) { // Basic read-path sanity should not fail. - let size_before = queue.size().await; + let size_before = queue.size(); queue .dequeue() .await @@ -371,7 +371,7 @@ async fn verify_recovery( return; } - let size = queue.size().await; + let size = queue.size(); let ack_floor = queue.ack_floor(); // Size should be within expected bounds diff --git a/storage/fuzz/fuzz_targets/queue_operations.rs b/storage/fuzz/fuzz_targets/queue_operations.rs index 3ce2bac5bb5..a5b087cdccb 100644 --- a/storage/fuzz/fuzz_targets/queue_operations.rs +++ b/storage/fuzz/fuzz_targets/queue_operations.rs @@ -214,14 +214,14 @@ fn fuzz(input: FuzzInput) { } QueueOperation::Ack { pos_offset } => { - let size = queue.size().await; + let size = queue.size(); if size == 0 { continue; } // Map offset to a valid position range let pos = (*pos_offset as u64) % size; - let result = queue.ack(pos).await; + let result = queue.ack(pos); let ref_result = reference.ack(pos); assert_eq!( @@ -232,11 +232,11 @@ fn fuzz(input: FuzzInput) { } QueueOperation::AckUpTo { pos_offset } => { - let size = queue.size().await; + let size = queue.size(); // Map offset to valid range [0, size] let up_to = (*pos_offset as u64) % (size + 1); - let result = queue.ack_up_to(up_to).await; + let result = queue.ack_up_to(up_to); let ref_result = reference.ack_up_to(up_to); assert_eq!( @@ -257,11 +257,7 @@ fn fuzz(input: FuzzInput) { } // Verify invariants after each operation - assert_eq!( - queue.size().await, - reference.size(), - "size mismatch after {op:?}" - ); + assert_eq!(queue.size(), reference.size(), "size mismatch after {op:?}"); assert_eq!( queue.ack_floor(), reference.ack_floor(), @@ -273,13 +269,13 @@ fn fuzz(input: FuzzInput) { "read_position mismatch after {op:?}" ); assert_eq!( - queue.is_empty().await, + queue.is_empty(), reference.is_empty(), "is_empty mismatch after {op:?}" ); // Verify is_acked consistency for a sample of positions - for pos in 0..queue.size().await.min(20) { + for pos in 0..queue.size().min(20) { assert_eq!( queue.is_acked(pos), reference.is_acked(pos), diff --git a/storage/fuzz/fuzz_targets/store_operations.rs b/storage/fuzz/fuzz_targets/store_operations.rs index e8da0477176..b861fd108d9 100644 --- a/storage/fuzz/fuzz_targets/store_operations.rs +++ b/storage/fuzz/fuzz_targets/store_operations.rs @@ -169,7 +169,7 @@ fn fuzz(input: FuzzInput) { } Operation::OpCount => { - let _ = db.bounds().await.end; + let _ = db.bounds().end; } Operation::InactivityFloorLoc => { diff --git a/storage/src/journal/authenticated.rs b/storage/src/journal/authenticated.rs index 4fb82286f80..3e5995aeab9 100644 --- a/storage/src/journal/authenticated.rs +++ b/storage/src/journal/authenticated.rs @@ -358,7 +358,7 @@ where { /// Durably persist the journal. This is faster than `sync()` but does not persist the Merkle /// structure, meaning recovery will be required on startup if we crash before `sync()`. - pub async fn commit(&self) -> Result<(), Error> { + pub async fn commit(&mut self) -> Result<(), Error> { self.journal.commit().await.map_err(Error::Journal) } } @@ -672,12 +672,11 @@ where } /// Durably persist the journal, ensuring no recovery is required on startup. - pub async fn sync(&self) -> Result<(), Error> { + pub async fn sync(&mut self) -> Result<(), Error> { try_join!( self.journal.sync().map_err(Error::Journal), - self.merkle.sync().map_err(Error::Merkle) + self.merkle.sync().map_err(Error::Merkle), )?; - Ok(()) } } @@ -743,11 +742,11 @@ where type Item = C::Item; async fn reader(&self) -> impl Reader + '_ { - self.journal.reader().await + Contiguous::reader(&self.journal).await } async fn size(&self) -> u64 { - self.journal.size().await + Contiguous::size(&self.journal).await } } @@ -812,11 +811,11 @@ where { type Error = JournalError; - async fn commit(&self) -> Result<(), JournalError> { + async fn commit(&mut self) -> Result<(), JournalError> { self.commit().await.map_err(Self::map_error) } - async fn sync(&self) -> Result<(), JournalError> { + async fn sync(&mut self) -> Result<(), JournalError> { self.sync().await.map_err(Self::map_error) } @@ -1068,7 +1067,7 @@ mod tests { .unwrap(); assert_eq!(merkle.leaves(), Location::::new(0)); - assert_eq!(journal.size().await, 0); + assert_eq!(journal.size(), 0); } #[test_traced("INFO")] @@ -1085,7 +1084,7 @@ mod tests { /// Verify that align() pops Merkle elements when Merkle is ahead of the journal. async fn test_align_when_mmr_ahead_inner(context: Context) { - let (mut merkle, journal, hasher) = create_components::(context, "mmr-ahead").await; + let (mut merkle, mut journal, hasher) = create_components::(context, "mmr-ahead").await; // Add 20 operations to both Merkle and journal { @@ -1115,7 +1114,7 @@ mod tests { // Merkle should have been aligned to match journal assert_eq!(merkle.leaves(), Location::::new(21)); - assert_eq!(journal.size().await, 21); + assert_eq!(journal.size(), 21); } #[test_traced("WARN")] @@ -1132,7 +1131,8 @@ mod tests { /// Verify that align() replays journal operations when journal is ahead of Merkle. async fn test_align_when_journal_ahead_inner(context: Context) { - let (mut merkle, journal, hasher) = create_components::(context, "journal-ahead").await; + let (mut merkle, mut journal, hasher) = + create_components::(context, "journal-ahead").await; // Add 20 operations to journal only for i in 0..20 { @@ -1152,7 +1152,7 @@ mod tests { // Merkle should have been replayed to match journal assert_eq!(merkle.leaves(), Location::::new(21)); - assert_eq!(journal.size().await, 21); + assert_eq!(journal.size(), 21); } #[test_traced("WARN")] @@ -1237,7 +1237,7 @@ mod tests { // Rewind to last commit let final_size = journal.rewind_to(|op| op.is_commit()).await.unwrap(); assert_eq!(final_size, 4); - assert_eq!(journal.size().await, 4); + assert_eq!(journal.size(), 4); // Verify the commit operation is still there let op = journal.read(3).await.unwrap(); @@ -1295,7 +1295,7 @@ mod tests { // Rewind should go to pruning boundary (0 for unpruned) let final_size = journal.rewind_to(|op| op.is_commit()).await.unwrap(); assert_eq!(final_size, 0, "Should rewind to pruning boundary (0)"); - assert_eq!(journal.size().await, 0); + assert_eq!(journal.size(), 0); } // Test 4: Rewind with existing pruning boundary @@ -1322,7 +1322,7 @@ mod tests { // Prune up to position 8 (this will prune section 0, items 0-6, keeping 7+) journal.prune(8).await.unwrap(); - assert_eq!(journal.reader().await.bounds().start, 7); + assert_eq!(journal.reader().bounds().start, 7); // Add more uncommitted operations for i in 15..20 { @@ -1363,7 +1363,7 @@ mod tests { // Prune up to position 8 (this prunes section 0, including the commit at pos 5) // Pruning boundary will be at position 7 (start of section 1) journal.prune(8).await.unwrap(); - assert_eq!(journal.reader().await.bounds().start, 7); + assert_eq!(journal.reader().bounds().start, 7); // Add uncommitted operations with no commits (in section 1: 7-13) for i in 10..14 { @@ -1391,7 +1391,7 @@ mod tests { .await .unwrap(); assert_eq!(final_size, 0); - assert_eq!(journal.size().await, 0); + assert_eq!(journal.size(), 0); } // Test 7: Position based authenticated journal rewind. diff --git a/storage/src/journal/benches/fixed_read_random.rs b/storage/src/journal/benches/fixed_read_random.rs index 83038380b5d..6a95aada0de 100644 --- a/storage/src/journal/benches/fixed_read_random.rs +++ b/storage/src/journal/benches/fixed_read_random.rs @@ -30,11 +30,10 @@ const ITEM_SIZE: usize = 32; /// Read `items_to_read` random items from the given `journal`, awaiting each /// result before continuing. async fn bench_run_serial(journal: &Journal>, items_to_read: usize) { - let reader = journal.reader().await; let mut rng = StdRng::seed_from_u64(0); for _ in 0..items_to_read { let pos = rng.gen_range(0..ITEMS_TO_WRITE); - black_box(reader.read(pos).await.expect("failed to read data")); + black_box(journal.read(pos).await.expect("failed to read data")); } } @@ -43,12 +42,11 @@ async fn bench_run_concurrent( journal: &Journal>, items_to_read: usize, ) { - let reader = journal.reader().await; let mut rng = StdRng::seed_from_u64(0); let mut futures = Vec::with_capacity(items_to_read); for _ in 0..items_to_read { let pos = rng.gen_range(0..ITEMS_TO_WRITE); - futures.push(reader.read(pos)); + futures.push(journal.read(pos)); } try_join_all(futures).await.expect("failed to read data"); } @@ -58,7 +56,7 @@ async fn bench_run_read_many( journal: &Journal>, items_to_read: usize, ) { - let reader = journal.reader().await; + let reader = journal.reader(); let mut rng = StdRng::seed_from_u64(0); let mut positions: Vec = (0..items_to_read) .map(|_| rng.gen_range(0..ITEMS_TO_WRITE)) diff --git a/storage/src/journal/benches/fixed_read_sequential.rs b/storage/src/journal/benches/fixed_read_sequential.rs index 09f8a31d7ed..0ec28843f58 100644 --- a/storage/src/journal/benches/fixed_read_sequential.rs +++ b/storage/src/journal/benches/fixed_read_sequential.rs @@ -3,7 +3,7 @@ use commonware_runtime::{ benchmarks::{context, tokio}, tokio::Context, }; -use commonware_storage::journal::contiguous::{fixed::Journal, Reader as _}; +use commonware_storage::journal::contiguous::fixed::Journal; use commonware_utils::{sequence::FixedBytes, NZU64}; use criterion::{criterion_group, Criterion}; use std::{ @@ -23,9 +23,8 @@ const ITEM_SIZE: usize = 32; /// Sequentially read `items_to_read` items in the given `journal` starting from item 0. async fn bench_run(journal: &Journal>, items_to_read: u64) { - let reader = journal.reader().await; for pos in 0..items_to_read { - black_box(reader.read(pos).await.expect("failed to read data")); + black_box(journal.read(pos).await.expect("failed to read data")); } } @@ -43,7 +42,7 @@ fn bench_fixed_read_sequential(c: &mut Criterion) { let mut j = get_fixed_journal::(ctx, PARTITION, ITEMS_PER_BLOB).await; append_fixed_random_data::<_, ITEM_SIZE>(&mut j, items).await; - let sz = j.size().await; + let sz = j.size(); assert_eq!(sz, items); // Run the benchmark diff --git a/storage/src/journal/benches/fixed_replay.rs b/storage/src/journal/benches/fixed_replay.rs index 45919384572..f1c076132c2 100644 --- a/storage/src/journal/benches/fixed_replay.rs +++ b/storage/src/journal/benches/fixed_replay.rs @@ -4,7 +4,7 @@ use commonware_runtime::{ tokio::{Config, Context, Runner}, Runner as _, Supervisor as _, }; -use commonware_storage::journal::contiguous::{fixed::Journal, Reader as _}; +use commonware_storage::journal::contiguous::fixed::Journal; use commonware_utils::{sequence::FixedBytes, NZUsize}; use criterion::{criterion_group, Criterion}; use futures::{pin_mut, StreamExt}; @@ -18,8 +18,7 @@ const PARTITION: &str = "test-partition"; /// Replay all items in the given `journal`. async fn bench_run(journal: &Journal>, buffer: usize) { - let reader = journal.reader().await; - let stream = reader + let stream = journal .replay(NZUsize!(buffer), 0) .await .expect("failed to replay journal"); diff --git a/storage/src/journal/benches/variable_read_random.rs b/storage/src/journal/benches/variable_read_random.rs index a1f73f15701..b49eb12d104 100644 --- a/storage/src/journal/benches/variable_read_random.rs +++ b/storage/src/journal/benches/variable_read_random.rs @@ -30,7 +30,7 @@ const ITEM_SIZE: usize = 32; /// Read `items_to_read` random items from the given `journal`, awaiting each /// result before continuing. async fn bench_run_serial(journal: &Journal>, items_to_read: usize) { - let reader = journal.reader().await; + let reader = journal.reader(); let mut rng = StdRng::seed_from_u64(0); for _ in 0..items_to_read { let pos = rng.gen_range(0..ITEMS_TO_WRITE); @@ -43,7 +43,7 @@ async fn bench_run_concurrent( journal: &Journal>, items_to_read: usize, ) { - let reader = journal.reader().await; + let reader = journal.reader(); let mut rng = StdRng::seed_from_u64(0); let mut futures = Vec::with_capacity(items_to_read); for _ in 0..items_to_read { @@ -58,7 +58,7 @@ async fn bench_run_read_many( journal: &Journal>, items_to_read: usize, ) { - let reader = journal.reader().await; + let reader = journal.reader(); let mut rng = StdRng::seed_from_u64(0); let mut positions: Vec = (0..items_to_read) .map(|_| rng.gen_range(0..ITEMS_TO_WRITE)) diff --git a/storage/src/journal/benches/variable_replay.rs b/storage/src/journal/benches/variable_replay.rs index 4620172bb07..3d90b37d494 100644 --- a/storage/src/journal/benches/variable_replay.rs +++ b/storage/src/journal/benches/variable_replay.rs @@ -4,7 +4,7 @@ use commonware_runtime::{ tokio::{Config, Context, Runner}, Runner as _, Supervisor as _, }; -use commonware_storage::journal::contiguous::{variable::Journal, Reader as _}; +use commonware_storage::journal::contiguous::variable::Journal; use commonware_utils::{sequence::FixedBytes, NZUsize}; use criterion::{criterion_group, Criterion}; use futures::{pin_mut, StreamExt}; @@ -18,7 +18,7 @@ const PARTITION: &str = "variable-test-partition"; /// Replay all items in the given `journal`. async fn bench_run(journal: &Journal>, buffer: usize) { - let reader = journal.reader().await; + let reader = journal.reader(); let stream = reader .replay(NZUsize!(buffer), 0) .await diff --git a/storage/src/journal/conformance.rs b/storage/src/journal/conformance.rs index 546daa0aa24..14e855f5588 100644 --- a/storage/src/journal/conformance.rs +++ b/storage/src/journal/conformance.rs @@ -31,7 +31,7 @@ impl Conformance for ContiguousFixed { page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE), write_buffer: WRITE_BUFFER, }; - let journal = fixed::Journal::<_, u64>::init(context.child("journal"), config) + let mut journal = fixed::Journal::<_, u64>::init(context.child("journal"), config) .await .unwrap(); @@ -64,9 +64,10 @@ impl Conformance for ContiguousVariable { compression: None, codec_config: (RangeCfg::new(0..256), ()), }; - let journal = variable::Journal::<_, Vec>::init(context.child("journal"), config) - .await - .unwrap(); + let mut journal = + variable::Journal::<_, Vec>::init(context.child("journal"), config) + .await + .unwrap(); let mut data_to_write = vec![Vec::new(); context.gen_range(0..(ITEMS_PER_BLOB.get() as usize) * 4)]; diff --git a/storage/src/journal/contiguous/fixed.rs b/storage/src/journal/contiguous/fixed.rs index 47cdc4a520a..c28165f610b 100644 --- a/storage/src/journal/contiguous/fixed.rs +++ b/storage/src/journal/contiguous/fixed.rs @@ -53,8 +53,6 @@ //! //! The `replay` method supports fast reading of all unpruned items into memory. -#[cfg(test)] -use super::Reader as _; use crate::{ journal::{ contiguous::{Many, Mutable}, @@ -66,7 +64,6 @@ use crate::{ }; use commonware_codec::CodecFixedShared; use commonware_runtime::buffer::paged::CacheRef; -use commonware_utils::sync::{AsyncRwLockReadGuard, UpgradableAsyncRwLock}; use futures::{stream::Stream, StreamExt}; use std::num::{NonZeroU64, NonZeroUsize}; use tracing::warn; @@ -96,8 +93,21 @@ pub struct Config { pub write_buffer: NonZeroUsize, } -/// Inner state protected by a single RwLock. -struct Inner { +/// Implementation of `Journal` storage. +/// +/// This is implemented as a wrapper around [SegmentedJournal] that provides position-based access +/// where positions are automatically mapped to (section, position_in_section) pairs. +/// +/// # Repair +/// +/// Like +/// [sqlite](https://github.com/sqlite/sqlite/blob/8658a8df59f00ec8fcfea336a2a6a4b5ef79d2ee/src/wal.c#L1504-L1505) +/// and +/// [rocksdb](https://github.com/facebook/rocksdb/blob/0c533e61bc6d89fdf1295e8e0bcee4edb3aef401/include/rocksdb/options.h#L441-L445), +/// the first invalid data read will be considered the new end of the journal (and the +/// underlying blob will be truncated to the last valid item). Repair is performed +/// by the underlying [SegmentedJournal] during init. +pub struct Journal { /// The underlying segmented journal. journal: SegmentedJournal, @@ -115,16 +125,19 @@ struct Inner { /// The position before which all items have been pruned. pruning_boundary: u64, + + /// The maximum number of items per blob (section). + items_per_blob: u64, } -impl Inner { - /// Read the item at position `pos` in the journal. - /// - /// # Errors - /// - /// - [Error::ItemPruned] if the item at position `pos` is pruned. - /// - [Error::ItemOutOfRange] if the item at position `pos` does not exist. - async fn read(&self, pos: u64, items_per_blob: u64) -> Result { +impl super::Reader for Journal { + type Item = A; + + fn bounds(&self) -> std::ops::Range { + self.pruning_boundary..self.size + } + + async fn read(&self, pos: u64) -> Result { if pos >= self.size { return Err(Error::ItemOutOfRange(pos)); } @@ -132,8 +145,8 @@ impl Inner { return Err(Error::ItemPruned(pos)); } - let section = pos / items_per_blob; - let section_start = section * items_per_blob; + let section = pos / self.items_per_blob; + let section_start = section * self.items_per_blob; // Calculate position within the blob. // This accounts for sections that begin mid-section (pruning_boundary > section_start). @@ -156,67 +169,6 @@ impl Inner { }) } - /// Read an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise. - fn try_read_sync(&self, pos: u64, items_per_blob: u64) -> Option { - let mut buf = vec![0u8; SegmentedJournal::::CHUNK_SIZE]; - self.try_read_sync_into(pos, items_per_blob, &mut buf) - } - - /// Read an item synchronously using caller-provided buffer. - fn try_read_sync_into(&self, pos: u64, items_per_blob: u64, buf: &mut [u8]) -> Option { - if pos >= self.size || pos < self.pruning_boundary { - return None; - } - let section = pos / items_per_blob; - let section_start = section * items_per_blob; - let first_in_section = self.pruning_boundary.max(section_start); - let pos_in_section = pos - first_in_section; - self.journal.try_get_sync_into(section, pos_in_section, buf) - } -} - -/// Implementation of `Journal` storage. -/// -/// This is implemented as a wrapper around [SegmentedJournal] that provides position-based access -/// where positions are automatically mapped to (section, position_in_section) pairs. -/// -/// # Repair -/// -/// Like -/// [sqlite](https://github.com/sqlite/sqlite/blob/8658a8df59f00ec8fcfea336a2a6a4b5ef79d2ee/src/wal.c#L1504-L1505) -/// and -/// [rocksdb](https://github.com/facebook/rocksdb/blob/0c533e61bc6d89fdf1295e8e0bcee4edb3aef401/include/rocksdb/options.h#L441-L445), -/// the first invalid data read will be considered the new end of the journal (and the -/// underlying blob will be truncated to the last valid item). Repair is performed -/// by the underlying [SegmentedJournal] during init. -pub struct Journal { - /// Inner state with segmented journal and size. - /// - /// Serializes persistence and write operations (`sync`, `append`, `prune`, `rewind`) to prevent - /// race conditions while allowing concurrent reads during sync. - inner: UpgradableAsyncRwLock>, - - /// The maximum number of items per blob (section). - items_per_blob: u64, -} - -/// A reader guard that holds a consistent snapshot of the journal's bounds. -pub struct Reader<'a, E: Context, A: CodecFixedShared> { - guard: AsyncRwLockReadGuard<'a, Inner>, - items_per_blob: u64, -} - -impl super::Reader for Reader<'_, E, A> { - type Item = A; - - fn bounds(&self) -> std::ops::Range { - self.guard.pruning_boundary..self.guard.size - } - - async fn read(&self, pos: u64) -> Result { - self.guard.read(pos, self.items_per_blob).await - } - async fn read_many(&self, positions: &[u64]) -> Result, Error> { if positions.is_empty() { return Ok(Vec::new()); @@ -227,16 +179,16 @@ impl super::Reader for Reader<'_, E, A> { ); // Validate all positions. for &pos in positions { - if pos >= self.guard.size { + if pos >= self.size { return Err(Error::ItemOutOfRange(pos)); } - if pos < self.guard.pruning_boundary { + if pos < self.pruning_boundary { return Err(Error::ItemPruned(pos)); } } let items_per_blob = self.items_per_blob; - let pruning_boundary = self.guard.pruning_boundary; + let pruning_boundary = self.pruning_boundary; let chunk_size = SegmentedJournal::::CHUNK_SIZE; // Phase 1: Drain page-cache hits synchronously. @@ -246,10 +198,7 @@ impl super::Reader for Reader<'_, E, A> { let mut sync_buf = vec![0u8; chunk_size]; for (i, &pos) in positions.iter().enumerate() { - if let Some(item) = self - .guard - .try_read_sync_into(pos, items_per_blob, &mut sync_buf) - { + if let Some(item) = self.try_read_sync_into(pos, &mut sync_buf) { result.push(Some(item)); } else { result.push(None); @@ -287,7 +236,6 @@ impl super::Reader for Reader<'_, E, A> { let buf = &mut reusable_buf[..group_len * chunk_size]; let items = self - .guard .journal .get_many(section, §ion_positions, buf) .await @@ -312,7 +260,8 @@ impl super::Reader for Reader<'_, E, A> { } fn try_read_sync(&self, pos: u64) -> Option { - self.guard.try_read_sync(pos, self.items_per_blob) + let mut buf = vec![0u8; SegmentedJournal::::CHUNK_SIZE]; + self.try_read_sync_into(pos, &mut buf) } async fn replay( @@ -321,10 +270,10 @@ impl super::Reader for Reader<'_, E, A> { start_pos: u64, ) -> Result> + Send, Error> { let items_per_blob = self.items_per_blob; - let pruning_boundary = self.guard.pruning_boundary; + let pruning_boundary = self.pruning_boundary; // Validate bounds. - if start_pos > self.guard.size { + if start_pos > self.size { return Err(Error::ItemOutOfRange(start_pos)); } if start_pos < pruning_boundary { @@ -339,11 +288,12 @@ impl super::Reader for Reader<'_, E, A> { let start_pos_in_section = start_pos - first_in_section; // Check all middle sections (not oldest, not tail) in range are complete. - let journal = &self.guard.journal; - if let (Some(oldest), Some(newest)) = (journal.oldest_section(), journal.newest_section()) { + if let (Some(oldest), Some(newest)) = + (self.journal.oldest_section(), self.journal.newest_section()) + { let first_to_check = start_section.max(oldest + 1); for section in first_to_check..newest { - let len = journal.section_len(section).await?; + let len = self.journal.section_len(section).await?; if len < items_per_blob { return Err(Error::Corruption(format!( "section {section} incomplete: expected {items_per_blob} items, got {len}" @@ -352,7 +302,8 @@ impl super::Reader for Reader<'_, E, A> { } } - let inner_stream = journal + let inner_stream = self + .journal .replay(start_section, start_pos_in_section, buffer) .await?; @@ -377,6 +328,18 @@ impl Journal { /// Size of each entry in bytes (as u64). pub const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE as u64; + /// Read an item synchronously using caller-provided buffer. + fn try_read_sync_into(&self, pos: u64, buf: &mut [u8]) -> Option { + if pos >= self.size || pos < self.pruning_boundary { + return None; + } + let section = pos / self.items_per_blob; + let section_start = section * self.items_per_blob; + let first_in_section = self.pruning_boundary.max(section_start); + let pos_in_section = pos - first_in_section; + self.journal.try_get_sync_into(section, pos_in_section, buf) + } + /// Scan a partition and return blob names, treating a missing partition as empty. async fn scan_partition(context: &E, partition: &str) -> Result>, Error> { match context.scan(partition).await { @@ -468,12 +431,10 @@ impl Journal { journal.ensure_section_exists(tail_section).await?; Ok(Self { - inner: UpgradableAsyncRwLock::new(Inner { - journal, - size, - metadata, - pruning_boundary, - }), + journal, + size, + metadata, + pruning_boundary, items_per_blob, }) } @@ -677,43 +638,29 @@ impl Journal { } Ok(Self { - inner: UpgradableAsyncRwLock::new(Inner { - journal, - size, - metadata, - pruning_boundary: size, // No data exists yet - }), + journal, + size, + metadata, + pruning_boundary: size, // No data exists yet items_per_blob, }) } - /// Convert a global position to (section, position_in_section). - #[inline] - const fn position_to_section(&self, position: u64) -> (u64, u64) { - let section = position / self.items_per_blob; - let pos_in_section = position % self.items_per_blob; - (section, pos_in_section) - } - /// Sync any pending updates to disk. /// /// Only the tail section can have pending updates since historical sections are synced /// when they become full. - pub async fn sync(&self) -> Result<(), Error> { - // Serialize with append/prune/rewind to ensure section selection is stable, while still allowing - // concurrent readers. - let inner = self.inner.upgradable_read().await; - + pub async fn sync(&mut self) -> Result<(), Error> { // Sync the tail section - let tail_section = inner.size / self.items_per_blob; + let tail_section = self.size / self.items_per_blob; // The tail section may not exist yet if the previous section was just filled, but syncing a // non-existent section is safe (returns Ok). - inner.journal.sync(tail_section).await?; + self.journal.sync(tail_section).await?; // Persist metadata only when pruning_boundary is mid-section. - let pruning_boundary = inner.pruning_boundary; - let pruning_boundary_from_metadata = inner.metadata.get(&PRUNING_BOUNDARY_KEY).cloned(); + let pruning_boundary = self.pruning_boundary; + let pruning_boundary_from_metadata = self.metadata.get(&PRUNING_BOUNDARY_KEY).cloned(); let put = if !pruning_boundary.is_multiple_of(self.items_per_blob) { let needs_update = pruning_boundary_from_metadata .is_none_or(|bytes| bytes.as_slice() != pruning_boundary.to_be_bytes()); @@ -729,39 +676,28 @@ impl Journal { return Ok(()); }; - // Upgrade only for the metadata mutation/sync step; reads were allowed while syncing - // the tail section above. - let mut inner = inner.upgrade().await; if put { - inner.metadata.put( + self.metadata.put( PRUNING_BOUNDARY_KEY, pruning_boundary.to_be_bytes().to_vec(), ); } else { - inner.metadata.remove(&PRUNING_BOUNDARY_KEY); + self.metadata.remove(&PRUNING_BOUNDARY_KEY); } - inner.metadata.sync().await?; + self.metadata.sync().await?; Ok(()) } - /// Acquire a reader guard that holds a consistent view of the journal. - pub async fn reader(&self) -> Reader<'_, E, A> { - Reader { - guard: self.inner.read().await, - items_per_blob: self.items_per_blob, - } - } - /// Return the total number of items in the journal, irrespective of pruning. The next value /// appended to the journal will be at this position. - pub async fn size(&self) -> u64 { - self.inner.read().await.size + pub const fn size(&self) -> u64 { + self.size } /// Append a new item to the journal. Return the item's position in the journal, or error if the /// operation fails. - pub async fn append(&self, item: &A) -> Result { + pub async fn append(&mut self, item: &A) -> Result { self.append_many(Many::Flat(std::slice::from_ref(item))) .await } @@ -770,12 +706,12 @@ impl Journal { /// /// Acquires the write lock once for all items instead of per-item. /// Returns [Error::EmptyAppend] if items is empty. - pub async fn append_many<'a>(&'a self, items: Many<'a, A>) -> Result { + pub async fn append_many<'a>(&'a mut self, items: Many<'a, A>) -> Result { if items.is_empty() { return Err(Error::EmptyAppend); } - // Encode all items into a single contiguous buffer before taking the write guard. + // Encode all items into a single contiguous buffer. // Uses Write::write directly to avoid per-item Bytes allocations from Encode::encode. let items_count = match &items { Many::Flat(items) => items.len(), @@ -797,35 +733,29 @@ impl Journal { } } - // Mutating operations are serialized by taking the write guard. - let mut inner = self.inner.write().await; + let items_per_blob = self.items_per_blob; let mut written = 0; while written < items_count { - let (section, pos_in_section) = self.position_to_section(inner.size); - let remaining_space = (self.items_per_blob - pos_in_section) as usize; + let section = self.size / items_per_blob; + let pos_in_section = self.size % items_per_blob; + let remaining_space = (items_per_blob - pos_in_section) as usize; let batch_count = remaining_space.min(items_count - written); let start = written * A::SIZE; let end = start + batch_count * A::SIZE; - inner - .journal + self.journal .append_raw(section, &items_buf[start..end]) .await?; - inner.size += batch_count as u64; + self.size += batch_count as u64; written += batch_count; - if inner.size.is_multiple_of(self.items_per_blob) { - // The section was filled and must be synced. Downgrade so readers can continue - // during the sync, but keep mutators blocked. After sync, upgrade again to - // create the next tail section before any append can proceed. - let inner_ref = inner.downgrade_to_upgradable(); - inner_ref.journal.sync(section).await?; - inner = inner_ref.upgrade().await; - inner.journal.ensure_section_exists(section + 1).await?; + if self.size.is_multiple_of(items_per_blob) { + self.journal.sync(section).await?; + self.journal.ensure_section_exists(section + 1).await?; } } - Ok(inner.size - 1) + Ok(self.size - 1) } /// Rewind the journal to the given `size`. Returns [Error::InvalidRewind] if the rewind point @@ -836,16 +766,14 @@ impl Journal { /// * This operation is not guaranteed to survive restarts until sync is called. /// * This operation is not atomic, but it will always leave the journal in a consistent state /// in the event of failure since blobs are always removed from newest to oldest. - pub async fn rewind(&self, size: u64) -> Result<(), Error> { - let mut inner = self.inner.write().await; - - match size.cmp(&inner.size) { + pub async fn rewind(&mut self, size: u64) -> Result<(), Error> { + match size.cmp(&self.size) { std::cmp::Ordering::Greater => return Err(Error::InvalidRewind(size)), std::cmp::Ordering::Equal => return Ok(()), std::cmp::Ordering::Less => {} } - if size < inner.pruning_boundary { + if size < self.pruning_boundary { return Err(Error::InvalidRewind(size)); } @@ -853,20 +781,19 @@ impl Journal { let section_start = section * self.items_per_blob; // Calculate offset within section for rewind - let first_in_section = inner.pruning_boundary.max(section_start); + let first_in_section = self.pruning_boundary.max(section_start); let pos_in_section = size - first_in_section; let byte_offset = pos_in_section * Self::CHUNK_SIZE_U64; - inner.journal.rewind(section, byte_offset).await?; - inner.size = size; + self.journal.rewind(section, byte_offset).await?; + self.size = size; Ok(()) } /// Return the location before which all items have been pruned. - pub async fn pruning_boundary(&self) -> u64 { - let inner = self.inner.read().await; - inner.pruning_boundary + pub const fn pruning_boundary(&self) -> u64 { + self.pruning_boundary } /// Allow the journal to prune items older than `min_item_pos`. The journal may not prune all @@ -875,29 +802,27 @@ impl Journal { /// /// Note that this operation may NOT be atomic, however it's guaranteed not to leave gaps in the /// event of failure as items are always pruned in order from oldest to newest. - pub async fn prune(&self, min_item_pos: u64) -> Result { - let mut inner = self.inner.write().await; - + pub async fn prune(&mut self, min_item_pos: u64) -> Result { // Calculate the section that would contain min_item_pos let target_section = min_item_pos / self.items_per_blob; // Calculate the tail section. - let tail_section = inner.size / self.items_per_blob; + let tail_section = self.size / self.items_per_blob; // Cap to tail section. The tail section is guaranteed to exist by our invariant. let min_section = std::cmp::min(target_section, tail_section); - let pruned = inner.journal.prune(min_section).await?; + let pruned = self.journal.prune(min_section).await?; // After pruning, update pruning_boundary to the start of the oldest remaining section if pruned { - let new_oldest = inner + let new_oldest = self .journal .oldest_section() .expect("all sections pruned - violates tail section invariant"); // Pruning boundary only moves forward - assert!(inner.pruning_boundary < new_oldest * self.items_per_blob); - inner.pruning_boundary = new_oldest * self.items_per_blob; + assert!(self.pruning_boundary < new_oldest * self.items_per_blob); + self.pruning_boundary = new_oldest * self.items_per_blob; } Ok(pruned) @@ -905,13 +830,8 @@ impl Journal { /// Remove any persisted data created by the journal. pub async fn destroy(self) -> Result<(), Error> { - // Destroy inner journal - let inner = self.inner.into_inner(); - inner.journal.destroy().await?; - - // Destroy metadata - inner.metadata.destroy().await?; - + self.journal.destroy().await?; + self.metadata.destroy().await?; Ok(()) } @@ -923,57 +843,97 @@ impl Journal { /// # Crash Safety /// If a crash occurs during this operation, `init()` will recover to a consistent state /// (though possibly different from the intended `new_size`). - pub(crate) async fn clear_to_size(&self, new_size: u64) -> Result<(), Error> { + pub(crate) async fn clear_to_size(&mut self, new_size: u64) -> Result<(), Error> { // Clear blobs before updating metadata. // This ordering is critical for crash safety: // - Crash after clear: no blobs, recovery returns (0, 0), metadata ignored // - Crash after create: old metadata triggers "metadata ahead" warning, // recovery falls back to blob state - let mut inner = self.inner.write().await; - inner.journal.clear().await?; + self.journal.clear().await?; let tail_section = new_size / self.items_per_blob; - inner.journal.ensure_section_exists(tail_section).await?; + self.journal.ensure_section_exists(tail_section).await?; - inner.size = new_size; - inner.pruning_boundary = new_size; // No data exists + self.size = new_size; + self.pruning_boundary = new_size; // No data exists // Persist metadata only when pruning_boundary is mid-section. - if !inner.pruning_boundary.is_multiple_of(self.items_per_blob) { - let value = inner.pruning_boundary.to_be_bytes().to_vec(); - inner.metadata.put(PRUNING_BOUNDARY_KEY, value); - inner.metadata.sync().await?; - } else if inner.metadata.get(&PRUNING_BOUNDARY_KEY).is_some() { - inner.metadata.remove(&PRUNING_BOUNDARY_KEY); - inner.metadata.sync().await?; + if !self.pruning_boundary.is_multiple_of(self.items_per_blob) { + let value = self.pruning_boundary.to_be_bytes().to_vec(); + self.metadata.put(PRUNING_BOUNDARY_KEY, value); + self.metadata.sync().await?; + } else if self.metadata.get(&PRUNING_BOUNDARY_KEY).is_some() { + self.metadata.remove(&PRUNING_BOUNDARY_KEY); + self.metadata.sync().await?; } Ok(()) } - /// Test helper: Read the item at the given position. - #[cfg(test)] - pub(crate) async fn read(&self, pos: u64) -> Result { - self.reader().await.read(pos).await + /// Read the item at the given position. + pub async fn read(&self, pos: u64) -> Result { + super::Reader::read(self, pos).await } - /// Test helper: Return the bounds of the journal. - #[cfg(test)] - pub(crate) async fn bounds(&self) -> std::ops::Range { - self.reader().await.bounds() + /// Replay items from the given position. + pub async fn replay( + &self, + buffer: NonZeroUsize, + start_pos: u64, + ) -> Result> + Send + '_, Error> { + super::Reader::replay(self, buffer, start_pos).await + } + + /// Return the bounds of the journal. + pub fn bounds(&self) -> std::ops::Range { + super::Reader::bounds(self) + } + + /// Acquire a reader that borrows the journal. + pub const fn reader(&self) -> Reader<'_, E, A> { + Reader(self) } /// Test helper: Get the oldest section from the internal segmented journal. #[cfg(test)] - pub(crate) async fn test_oldest_section(&self) -> Option { - let inner = self.inner.read().await; - inner.journal.oldest_section() + pub(crate) fn test_oldest_section(&self) -> Option { + self.journal.oldest_section() } /// Test helper: Get the newest section from the internal segmented journal. #[cfg(test)] - pub(crate) async fn test_newest_section(&self) -> Option { - let inner = self.inner.read().await; - inner.journal.newest_section() + pub(crate) fn test_newest_section(&self) -> Option { + self.journal.newest_section() + } +} + +/// Borrowed read-only view of a [`Journal`]. +pub struct Reader<'a, E: Context, A: CodecFixedShared>(&'a Journal); + +impl super::Reader for Reader<'_, E, A> { + type Item = A; + + fn bounds(&self) -> std::ops::Range { + self.0.bounds() + } + + async fn read(&self, pos: u64) -> Result { + super::Reader::read(self.0, pos).await + } + + async fn read_many(&self, positions: &[u64]) -> Result, Error> { + super::Reader::read_many(self.0, positions).await + } + + fn try_read_sync(&self, pos: u64) -> Option { + super::Reader::try_read_sync(self.0, pos) + } + + async fn replay( + &self, + buffer: NonZeroUsize, + start_pos: u64, + ) -> Result> + Send, Error> { + super::Reader::replay(self.0, buffer, start_pos).await } } @@ -982,11 +942,11 @@ impl super::Contiguous for Journal { type Item = A; async fn reader(&self) -> impl super::Reader + '_ { - Self::reader(self).await + Reader(self) } async fn size(&self) -> u64 { - Self::size(self).await + self.size } } @@ -1011,12 +971,12 @@ impl Mutable for Journal { impl Persistable for Journal { type Error = Error; - async fn commit(&self) -> Result<(), Error> { - self.sync().await + async fn commit(&mut self) -> Result<(), Error> { + Self::sync(self).await } - async fn sync(&self) -> Result<(), Error> { - self.sync().await + async fn sync(&mut self) -> Result<(), Error> { + Self::sync(self).await } async fn destroy(self) -> Result<(), Error> { @@ -1153,7 +1113,7 @@ mod tests { .await .expect("Failed to sync legacy blob"); - let journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); journal.append(&test_digest(1)).await.unwrap(); @@ -1175,7 +1135,7 @@ mod tests { let legacy_partition = cfg.partition.clone(); let blobs_partition = blob_partition(&cfg); - let journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); journal.append(&test_digest(1)).await.unwrap(); @@ -1198,7 +1158,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 2 items per blob. let cfg = test_cfg(&context, NZU64!(2)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -1214,10 +1174,10 @@ mod tests { drop(journal); let cfg = test_cfg(&context, NZU64!(2)); - let journal = Journal::init(context.child("second"), cfg.clone()) + let mut journal = Journal::init(context.child("second"), cfg.clone()) .await .expect("failed to re-initialize journal"); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); // Append two more items to the journal to trigger a new blob creation pos = journal @@ -1249,7 +1209,7 @@ mod tests { // Pruning to 2 should allow the first blob to be pruned. journal.prune(2).await.expect("failed to prune journal 2"); - assert_eq!(journal.bounds().await.start, 2); + assert_eq!(journal.bounds().start, 2); // Reading from the first blob should fail since it's now pruned let result0 = journal.read(0).await; @@ -1272,51 +1232,48 @@ mod tests { // Check no-op pruning journal.prune(0).await.expect("no-op pruning failed"); - assert_eq!(journal.inner.read().await.journal.oldest_section(), Some(1)); - assert_eq!(journal.inner.read().await.journal.newest_section(), Some(5)); - assert_eq!(journal.bounds().await.start, 2); + assert_eq!(journal.journal.oldest_section(), Some(1)); + assert_eq!(journal.journal.newest_section(), Some(5)); + assert_eq!(journal.bounds().start, 2); // Prune first 3 blobs (6 items) journal .prune(3 * cfg.items_per_blob.get()) .await .expect("failed to prune journal 2"); - assert_eq!(journal.inner.read().await.journal.oldest_section(), Some(3)); - assert_eq!(journal.inner.read().await.journal.newest_section(), Some(5)); - assert_eq!(journal.bounds().await.start, 6); + assert_eq!(journal.journal.oldest_section(), Some(3)); + assert_eq!(journal.journal.newest_section(), Some(5)); + assert_eq!(journal.bounds().start, 6); // Try pruning (more than) everything in the journal. journal .prune(10000) .await .expect("failed to max-prune journal"); - let size = journal.size().await; + let size = journal.size(); assert_eq!(size, 10); - assert_eq!(journal.test_oldest_section().await, Some(5)); - assert_eq!(journal.test_newest_section().await, Some(5)); + assert_eq!(journal.test_oldest_section(), Some(5)); + assert_eq!(journal.test_newest_section(), Some(5)); // Since the size of the journal is currently a multiple of items_per_blob, the newest blob // will be empty, and there will be no retained items. - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert!(bounds.is_empty()); // bounds.start should equal bounds.end when empty. assert_eq!(bounds.start, size); // Replaying from 0 should fail since all items before bounds.start are pruned { - let reader = journal.reader().await; - let result = reader.replay(NZUsize!(1024), 0).await; + let result = journal.replay(NZUsize!(1024), 0).await; assert!(matches!(result, Err(Error::ItemPruned(0)))); } // Replaying from pruning_boundary should return empty stream { - let reader = journal.reader().await; - let res = reader.replay(NZUsize!(1024), 0).await; + let res = journal.replay(NZUsize!(1024), 0).await; assert!(matches!(res, Err(Error::ItemPruned(_)))); - let reader = journal.reader().await; - let stream = reader - .replay(NZUsize!(1024), journal.bounds().await.start) + let stream = journal + .replay(NZUsize!(1024), journal.bounds().start) .await .expect("failed to replay journal from pruning boundary"); pin_mut!(stream); @@ -1345,7 +1302,7 @@ mod tests { const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(10000); executor.start(|context| async move { let cfg = test_cfg(&context, ITEMS_PER_BLOB); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); // Append 2 blobs worth of items. @@ -1379,7 +1336,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 7 items per blob. let cfg = test_cfg(&context, ITEMS_PER_BLOB); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -1400,8 +1357,7 @@ mod tests { // Replay should return all items { - let reader = journal.reader().await; - let stream = reader + let stream = journal .replay(NZUsize!(1024), 0) .await .expect("failed to replay journal"); @@ -1458,8 +1414,8 @@ mod tests { // Replay all items. { let mut error_found = false; - let reader = journal.reader().await; - let stream = reader + + let stream = journal .replay(NZUsize!(1024), 0) .await .expect("failed to replay journal"); @@ -1492,7 +1448,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 7 items per blob. let cfg = test_cfg(&context, ITEMS_PER_BLOB); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -1527,11 +1483,11 @@ mod tests { // Journal size is computed from the tail section, so it's unchanged // despite the corruption in section 40. let expected_size = ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2; - assert_eq!(journal.size().await, expected_size); + assert_eq!(journal.size(), expected_size); // Replay should detect corruption (incomplete section) in section 40 - let reader = journal.reader().await; - match reader.replay(NZUsize!(1024), 0).await { + + match journal.replay(NZUsize!(1024), 0).await { Err(Error::Corruption(msg)) => { assert!( msg.contains("section 40"), @@ -1549,7 +1505,7 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(2)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); for i in 0u64..5 { @@ -1572,8 +1528,7 @@ mod tests { .expect("init shouldn't fail"); // But replay will. - let reader = result.reader().await; - match reader.replay(NZUsize!(1024), 0).await { + match result.replay(NZUsize!(1024), 0).await { Err(Error::Corruption(_)) => {} Err(err) => panic!("expected Corruption, got: {err}"), Ok(_) => panic!("expected Corruption, got ok"), @@ -1597,7 +1552,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 7 items per blob. let cfg = test_cfg(&context, ITEMS_PER_BLOB); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -1609,7 +1564,7 @@ mod tests { .await .expect("failed to append data"); } - assert_eq!(journal.size().await, item_count); + assert_eq!(journal.size(), item_count); journal.sync().await.expect("Failed to sync journal"); drop(journal); @@ -1628,7 +1583,7 @@ mod tests { // The truncation invalidates the last page (bad checksum), which is removed. // This loses one item. - assert_eq!(journal.size().await, item_count - 1); + assert_eq!(journal.size(), item_count - 1); // Cleanup. journal.destroy().await.expect("Failed to destroy journal"); @@ -1648,7 +1603,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 7 items per blob. let cfg = test_cfg(&context, ITEMS_PER_BLOB); - let journal = Journal::init(context.child("storage"), cfg.clone()) + let mut journal = Journal::init(context.child("storage"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -1663,8 +1618,7 @@ mod tests { // Replay should return all items except the first `START_POS`. { - let reader = journal.reader().await; - let stream = reader + let stream = journal .replay(NZUsize!(1024), START_POS) .await .expect("failed to replay journal"); @@ -1710,7 +1664,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 3 items per blob. let cfg = test_cfg(&context, NZU64!(3)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); for i in 0..5 { @@ -1719,7 +1673,7 @@ mod tests { .await .expect("failed to append data"); } - assert_eq!(journal.size().await, 5); + assert_eq!(journal.size(), 5); journal.sync().await.expect("Failed to sync journal"); drop(journal); @@ -1737,8 +1691,8 @@ mod tests { .await .expect("Failed to re-initialize journal"); // The truncation invalidates the last page, which is removed. This loses one item. - assert_eq!(journal.pruning_boundary().await, 0); - assert_eq!(journal.size().await, 4); + assert_eq!(journal.pruning_boundary(), 0); + assert_eq!(journal.size(), 4); drop(journal); // Delete the second blob and re-init @@ -1751,7 +1705,7 @@ mod tests { .await .expect("Failed to re-initialize journal"); // Only the first blob remains - assert_eq!(journal.size().await, 3); + assert_eq!(journal.size(), 3); journal.destroy().await.unwrap(); }); @@ -1762,7 +1716,7 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7) .await .expect("failed to initialize journal at size"); @@ -1775,8 +1729,8 @@ mod tests { .expect("failed to append data"); } journal.sync().await.expect("failed to sync journal"); - assert_eq!(journal.pruning_boundary().await, 7); - assert_eq!(journal.size().await, 15); + assert_eq!(journal.pruning_boundary(), 7); + assert_eq!(journal.size(), 15); drop(journal); // Corrupt the oldest section by truncating one byte (drops one item on recovery). @@ -1798,7 +1752,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 10 items per blob. let cfg = test_cfg(&context, NZU64!(10)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); // Add only a single item @@ -1806,7 +1760,7 @@ mod tests { .append(&test_digest(0)) .await .expect("failed to append data"); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); journal.sync().await.expect("Failed to sync journal"); drop(journal); @@ -1820,13 +1774,13 @@ mod tests { blob.sync().await.expect("Failed to sync blob"); // Re-initialize the journal to simulate a restart - let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .expect("Failed to re-initialize journal"); // Since there was only a single item appended which we then corrupted, recovery should // leave us in the state of an empty journal. - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 0); assert!(bounds.is_empty()); // Make sure journal still works for appending. @@ -1834,7 +1788,7 @@ mod tests { .append(&test_digest(0)) .await .expect("failed to append data"); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); journal.destroy().await.unwrap(); }); @@ -1846,7 +1800,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 10 items per blob. let cfg = test_cfg(&context, NZU64!(10)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -1855,7 +1809,7 @@ mod tests { .append(&test_digest(0)) .await .expect("failed to append data"); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); journal.sync().await.expect("Failed to sync journal"); drop(journal); @@ -1871,13 +1825,13 @@ mod tests { blob.sync().await.expect("Failed to sync blob"); // Re-initialize the journal to simulate a restart - let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .expect("Failed to re-initialize journal"); // The zero-filled pages are detected as invalid (bad checksum) and truncated. // No items should be lost since we called sync before the corruption. - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); // Make sure journal still works for appending. journal @@ -1895,7 +1849,7 @@ mod tests { executor.start(|context| async move { // Initialize the journal, allowing a max of 2 items per blob. let cfg = test_cfg(&context, NZU64!(2)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); assert!(matches!(journal.rewind(0).await, Ok(()))); @@ -1909,10 +1863,10 @@ mod tests { .append(&test_digest(0)) .await .expect("failed to append data 0"); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); assert!(matches!(journal.rewind(1).await, Ok(()))); // should be no-op assert!(matches!(journal.rewind(0).await, Ok(()))); - assert_eq!(journal.size().await, 0); + assert_eq!(journal.size(), 0); // append 7 items for i in 0..7 { @@ -1922,15 +1876,15 @@ mod tests { .expect("failed to append data"); assert_eq!(pos, i); } - assert_eq!(journal.size().await, 7); + assert_eq!(journal.size(), 7); // rewind back to item #4, which should prune 2 blobs assert!(matches!(journal.rewind(4).await, Ok(()))); - assert_eq!(journal.size().await, 4); + assert_eq!(journal.size(), 4); // rewind back to empty and ensure all blobs are rewound over assert!(matches!(journal.rewind(0).await, Ok(()))); - assert_eq!(journal.size().await, 0); + assert_eq!(journal.size(), 0); // stress test: add 100 items, rewind 49, repeat x10. for _ in 0..10 { @@ -1940,10 +1894,10 @@ mod tests { .await .expect("failed to append data"); } - journal.rewind(journal.size().await - 49).await.unwrap(); + journal.rewind(journal.size() - 49).await.unwrap(); } const ITEMS_REMAINING: u64 = 10 * (100 - 49); - assert_eq!(journal.size().await, ITEMS_REMAINING); + assert_eq!(journal.size(), ITEMS_REMAINING); journal.sync().await.expect("Failed to sync journal"); drop(journal); @@ -1951,7 +1905,7 @@ mod tests { // Repeat with a different blob size (3 items per blob) let mut cfg = test_cfg(&context, NZU64!(3)); cfg.partition = "test-partition-2".into(); - let journal = Journal::init(context.child("second"), cfg.clone()) + let mut journal = Journal::init(context.child("second"), cfg.clone()) .await .expect("failed to initialize journal"); for _ in 0..10 { @@ -1961,22 +1915,23 @@ mod tests { .await .expect("failed to append data"); } - journal.rewind(journal.size().await - 49).await.unwrap(); + journal.rewind(journal.size() - 49).await.unwrap(); } - assert_eq!(journal.size().await, ITEMS_REMAINING); + assert_eq!(journal.size(), ITEMS_REMAINING); journal.sync().await.expect("Failed to sync journal"); drop(journal); // Make sure re-opened journal is as expected - let journal: Journal<_, Digest> = Journal::init(context.child("third"), cfg.clone()) - .await - .expect("failed to re-initialize journal"); - assert_eq!(journal.size().await, 10 * (100 - 49)); + let mut journal: Journal<_, Digest> = + Journal::init(context.child("third"), cfg.clone()) + .await + .expect("failed to re-initialize journal"); + assert_eq!(journal.size(), 10 * (100 - 49)); // Make sure rewinding works after pruning journal.prune(300).await.expect("pruning failed"); - assert_eq!(journal.size().await, ITEMS_REMAINING); + assert_eq!(journal.size(), ITEMS_REMAINING); // Rewinding prior to our prune point should fail. assert!(matches!( journal.rewind(299).await, @@ -1985,7 +1940,7 @@ mod tests { // Rewinding to the prune point should work. // always remain in the journal. assert!(matches!(journal.rewind(300).await, Ok(()))); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 300); assert!(bounds.is_empty()); @@ -2006,7 +1961,7 @@ mod tests { executor.start(|context: Context| async move { // Use a small items_per_blob to keep the test focused on a single blob let cfg = test_cfg(&context, NZU64!(100)); - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -2023,7 +1978,7 @@ mod tests { .await .expect("failed to append data"); } - assert_eq!(journal.size().await, 10); + assert_eq!(journal.size(), 10); journal.sync().await.expect("Failed to sync journal"); drop(journal); @@ -2057,7 +2012,7 @@ mod tests { let remaining_logical_bytes = (full_pages - 1) * PAGE_SIZE.get() as u64; let expected_items = remaining_logical_bytes / 32; // 32 = Digest::SIZE assert_eq!( - journal.size().await, + journal.size(), expected_items, "Journal should recover to {} items after truncation", expected_items @@ -2093,12 +2048,12 @@ mod tests { }; // === Test 1: Basic single item operation === - let journal = Journal::init(context.child("first"), cfg.clone()) + let mut journal = Journal::init(context.child("first"), cfg.clone()) .await .expect("failed to initialize journal"); // Verify empty state - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 0); assert!(bounds.is_empty()); @@ -2108,14 +2063,14 @@ mod tests { .await .expect("failed to append"); assert_eq!(pos, 0); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); // Sync journal.sync().await.expect("failed to sync"); // Read from size() - 1 let value = journal - .read(journal.size().await - 1) + .read(journal.size() - 1) .await .expect("failed to read"); assert_eq!(value, test_digest(0)); @@ -2127,11 +2082,11 @@ mod tests { .await .expect("failed to append"); assert_eq!(pos, i); - assert_eq!(journal.size().await, i + 1); + assert_eq!(journal.size(), i + 1); // Verify we can read the just-appended item at size() - 1 let value = journal - .read(journal.size().await - 1) + .read(journal.size() - 1) .await .expect("failed to read"); assert_eq!(value, test_digest(i)); @@ -2149,14 +2104,14 @@ mod tests { journal.prune(5).await.expect("failed to prune"); // Size should still be 10 - assert_eq!(journal.size().await, 10); + assert_eq!(journal.size(), 10); // bounds.start should be 5 - assert_eq!(journal.bounds().await.start, 5); + assert_eq!(journal.bounds().start, 5); // Reading from size() - 1 (position 9) should still work let value = journal - .read(journal.size().await - 1) + .read(journal.size() - 1) .await .expect("failed to read"); assert_eq!(value, test_digest(9)); @@ -2181,7 +2136,7 @@ mod tests { // Verify we can read from size() - 1 let value = journal - .read(journal.size().await - 1) + .read(journal.size() - 1) .await .expect("failed to read"); assert_eq!(value, test_digest(i)); @@ -2196,14 +2151,14 @@ mod tests { .expect("failed to re-initialize journal"); // Verify size is preserved - assert_eq!(journal.size().await, 15); + assert_eq!(journal.size(), 15); // Verify bounds.start is preserved - assert_eq!(journal.bounds().await.start, 5); + assert_eq!(journal.bounds().start, 5); // Reading from size() - 1 should work after restart let value = journal - .read(journal.size().await - 1) + .read(journal.size() - 1) .await .expect("failed to read"); assert_eq!(value, test_digest(14)); @@ -2217,7 +2172,7 @@ mod tests { // === Test 5: Restart after pruning with non-zero index === // Fresh journal for this test - let journal = Journal::init(context.child("third"), cfg.clone()) + let mut journal = Journal::init(context.child("third"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -2228,7 +2183,7 @@ mod tests { // Prune to position 5 (removes positions 0-4) journal.prune(5).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 5); @@ -2242,12 +2197,12 @@ mod tests { .expect("failed to re-initialize journal"); // Verify state after restart - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 5); // Reading from size() - 1 (position 9) should work - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, test_digest(109)); // Verify all retained positions (5-9) work @@ -2258,7 +2213,7 @@ mod tests { journal.destroy().await.expect("failed to destroy journal"); // === Test 6: Prune all items (edge case) === - let journal = Journal::init(context.child("storage"), cfg.clone()) + let mut journal = Journal::init(context.child("storage"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -2269,19 +2224,19 @@ mod tests { // Prune all items journal.prune(5).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 5); // Size unchanged assert!(bounds.is_empty()); // All pruned // size() - 1 = 4, but position 4 is pruned - let result = journal.read(journal.size().await - 1).await; + let result = journal.read(journal.size() - 1).await; assert!(matches!(result, Err(Error::ItemPruned(4)))); // After appending, reading works again journal.append(&test_digest(205)).await.unwrap(); - assert_eq!(journal.bounds().await.start, 5); + assert_eq!(journal.bounds().start, 5); assert_eq!( - journal.read(journal.size().await - 1).await.unwrap(), + journal.read(journal.size() - 1).await.unwrap(), test_digest(205) ); @@ -2294,19 +2249,19 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 0) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 0); assert!(bounds.is_empty()); // Next append should get position 0 let pos = journal.append(&test_digest(100)).await.unwrap(); assert_eq!(pos, 0); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); assert_eq!(journal.read(0).await.unwrap(), test_digest(100)); journal.destroy().await.unwrap(); @@ -2320,19 +2275,19 @@ mod tests { let cfg = test_cfg(&context, NZU64!(5)); // Initialize at position 10 (exactly at section 2 boundary with items_per_blob=5) - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert!(bounds.is_empty()); // Next append should get position 10 let pos = journal.append(&test_digest(1000)).await.unwrap(); assert_eq!(pos, 10); - assert_eq!(journal.size().await, 11); + assert_eq!(journal.size(), 11); assert_eq!(journal.read(10).await.unwrap(), test_digest(1000)); // Can continue appending @@ -2351,12 +2306,12 @@ mod tests { let cfg = test_cfg(&context, NZU64!(5)); // Initialize at position 7 (middle of section 1 with items_per_blob=5) - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 7); // No data exists yet after init_at_size assert!(bounds.is_empty()); @@ -2368,10 +2323,10 @@ mod tests { // Next append should get position 7 let pos = journal.append(&test_digest(700)).await.unwrap(); assert_eq!(pos, 7); - assert_eq!(journal.size().await, 8); + assert_eq!(journal.size(), 8); assert_eq!(journal.read(7).await.unwrap(), test_digest(700)); // Now bounds.start should be 7 (first data position) - assert_eq!(journal.bounds().await.start, 7); + assert_eq!(journal.bounds().start, 7); journal.destroy().await.unwrap(); }); @@ -2384,7 +2339,7 @@ mod tests { let cfg = test_cfg(&context, NZU64!(5)); // Initialize at position 15 - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15) .await .unwrap(); @@ -2395,18 +2350,18 @@ mod tests { assert_eq!(pos, 15 + i); } - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); // Sync and reopen journal.sync().await.unwrap(); drop(journal); - let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .unwrap(); // Size and data should be preserved - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 20); assert_eq!(bounds.start, 15); @@ -2436,7 +2391,7 @@ mod tests { .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 15); assert!(bounds.is_empty()); @@ -2444,11 +2399,11 @@ mod tests { drop(journal); // Reopen and verify size persisted - let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 15); assert!(bounds.is_empty()); @@ -2468,12 +2423,12 @@ mod tests { let cfg = test_cfg(&context, NZU64!(5)); // Initialize at a large position (position 1000) - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 1000) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 1000); assert!(bounds.is_empty()); @@ -2493,7 +2448,7 @@ mod tests { let cfg = test_cfg(&context, NZU64!(5)); // Initialize at position 20 - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 20) .await .unwrap(); @@ -2503,12 +2458,12 @@ mod tests { journal.append(&test_digest(2000 + i)).await.unwrap(); } - assert_eq!(journal.size().await, 30); + assert_eq!(journal.size(), 30); // Prune to position 25 journal.prune(25).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 30); assert_eq!(bounds.start, 25); @@ -2530,7 +2485,7 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(10)); - let journal = Journal::init(context.child("journal"), cfg.clone()) + let mut journal = Journal::init(context.child("journal"), cfg.clone()) .await .expect("failed to initialize journal"); @@ -2538,12 +2493,12 @@ mod tests { for i in 0..25u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 25); + assert_eq!(journal.size(), 25); journal.sync().await.unwrap(); // Clear to position 100, effectively resetting the journal journal.clear_to_size(100).await.unwrap(); - assert_eq!(journal.size().await, 100); + assert_eq!(journal.size(), 100); // Old positions should fail for i in 0..25 { @@ -2552,18 +2507,18 @@ mod tests { // Verify size persists after restart without writing any data drop(journal); - let journal = + let mut journal = Journal::<_, Digest>::init(context.child("journal_after_clear"), cfg.clone()) .await .expect("failed to re-initialize journal after clear"); - assert_eq!(journal.size().await, 100); + assert_eq!(journal.size(), 100); // Append new data starting at position 100 for i in 100..105u64 { let pos = journal.append(&test_digest(i)).await.unwrap(); assert_eq!(pos, i); } - assert_eq!(journal.size().await, 105); + assert_eq!(journal.size(), 105); // New positions should be readable for i in 100..105u64 { @@ -2578,7 +2533,7 @@ mod tests { .await .expect("failed to re-initialize journal"); - assert_eq!(journal.size().await, 105); + assert_eq!(journal.size(), 105); for i in 100..105u64 { assert_eq!(journal.read(i).await.unwrap(), test_digest(i)); } @@ -2593,23 +2548,21 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone()) .await .unwrap(); for i in 0..5u64 { journal.append(&test_digest(i)).await.unwrap(); } - let inner = journal.inner.read().await; - let tail_section = inner.size / journal.items_per_blob; - inner.journal.sync(tail_section).await.unwrap(); - drop(inner); + let tail_section = journal.size / journal.items_per_blob; + journal.journal.sync(tail_section).await.unwrap(); drop(journal); let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 0); assert_eq!(bounds.end, 5); journal.destroy().await.unwrap(); @@ -2622,21 +2575,19 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7) .await .unwrap(); for i in 0..3u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.inner.read().await.journal.newest_section(), Some(2)); + assert_eq!(journal.journal.newest_section(), Some(2)); journal.sync().await.unwrap(); // Simulate metadata deletion (corruption). - let mut inner = journal.inner.write().await; - inner.metadata.clear(); - inner.metadata.sync().await.unwrap(); - drop(inner); + journal.metadata.clear(); + journal.metadata.sync().await.unwrap(); drop(journal); // Section 1 has items 7,8,9 but metadata is missing, so falls back to blob-based boundary. @@ -2658,23 +2609,21 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7) .await .unwrap(); for i in 0..3u64 { journal.append(&test_digest(i)).await.unwrap(); } - let inner = journal.inner.read().await; - let tail_section = inner.size / journal.items_per_blob; - inner.journal.sync(tail_section).await.unwrap(); - drop(inner); + let tail_section = journal.size / journal.items_per_blob; + journal.journal.sync(tail_section).await.unwrap(); drop(journal); let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 7); assert_eq!(bounds.end, 10); journal.destroy().await.unwrap(); @@ -2686,26 +2635,24 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7) .await .unwrap(); for i in 0..10u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 17); + assert_eq!(journal.size(), 17); journal.prune(10).await.unwrap(); - let inner = journal.inner.read().await; - let tail_section = inner.size / journal.items_per_blob; - inner.journal.sync(tail_section).await.unwrap(); - drop(inner); + let tail_section = journal.size / journal.items_per_blob; + journal.journal.sync(tail_section).await.unwrap(); drop(journal); let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 10); assert_eq!(bounds.end, 17); journal.destroy().await.unwrap(); @@ -2720,7 +2667,7 @@ mod tests { executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); // init_at_size(7) sets pruning_boundary = 7 (mid-section in section 1) - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7) .await .unwrap(); @@ -2730,7 +2677,7 @@ mod tests { } // Prune to position 5 (section 1 start) should NOT move boundary back from 7 to 5 journal.prune(5).await.unwrap(); - assert_eq!(journal.bounds().await.start, 7); + assert_eq!(journal.bounds().start, 7); journal.destroy().await.unwrap(); }); } @@ -2745,7 +2692,7 @@ mod tests { // Initialize at position 7 (mid-section with items_per_blob=5) // Section 1 (positions 5-9) begins mid-section: only positions 7, 8, 9 have data - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7) .await .unwrap(); @@ -2755,13 +2702,12 @@ mod tests { let pos = journal.append(&test_digest(100 + i)).await.unwrap(); assert_eq!(pos, 7 + i); } - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); journal.sync().await.unwrap(); // Replay from pruning_boundary { - let reader = journal.reader().await; - let stream = reader + let stream = journal .replay(NZUsize!(1024), 7) .await .expect("failed to replay"); @@ -2781,8 +2727,7 @@ mod tests { // Replay from mid-stream (position 12) { - let reader = journal.reader().await; - let stream = reader + let stream = journal .replay(NZUsize!(1024), 12) .await .expect("failed to replay from mid-stream"); @@ -2811,7 +2756,7 @@ mod tests { executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(5)); - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10) .await .unwrap(); @@ -2820,15 +2765,15 @@ mod tests { for i in 0..3u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 13); + assert_eq!(journal.size(), 13); // Rewind to position 11 should work journal.rewind(11).await.unwrap(); - assert_eq!(journal.size().await, 11); + assert_eq!(journal.size(), 11); // Rewind to position 10 (pruning_boundary) should work journal.rewind(10).await.unwrap(); - assert_eq!(journal.size().await, 10); + assert_eq!(journal.size(), 10); // Rewind to before pruning_boundary should fail let result = journal.rewind(9).await; @@ -2845,7 +2790,7 @@ mod tests { let cfg = test_cfg(&context, NZU64!(5)); // Setup: Create a journal with some data and mid-section metadata - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7) .await .unwrap(); @@ -2861,13 +2806,10 @@ mod tests { context.remove(&blob_part, None).await.unwrap(); // Recovery should see no blobs and return empty journal, ignoring metadata - let journal = Journal::<_, Digest>::init( - context.child("crash").with_attribute("index", 1), - cfg.clone(), - ) - .await - .expect("init failed after clear crash"); - let bounds = journal.bounds().await; + let journal = Journal::<_, Digest>::init(context.child("crash1"), cfg.clone()) + .await + .expect("init failed after clear crash"); + let bounds = journal.bounds(); assert_eq!(bounds.end, 0); assert_eq!(bounds.start, 0); drop(journal); @@ -2900,15 +2842,12 @@ mod tests { blob.sync().await.unwrap(); // Ensure it exists // Recovery should warn "metadata ahead" and use blob state (0, 0) - let journal = Journal::<_, Digest>::init( - context.child("crash").with_attribute("index", 2), - cfg.clone(), - ) - .await - .expect("init failed after create crash"); + let journal = Journal::<_, Digest>::init(context.child("crash2"), cfg.clone()) + .await + .expect("init failed after create crash"); // Should recover to blob state (section 0 aligned) - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 0); // Size is 0 because blob is empty assert_eq!(bounds.end, 0); @@ -2924,7 +2863,7 @@ mod tests { // Setup: Init at 12 (Section 2, offset 2) // Metadata = 12 - let journal = + let mut journal = Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 12) .await .unwrap(); @@ -2951,7 +2890,7 @@ mod tests { .expect("init failed after clear_to_size crash"); // Should fallback to blobs - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 0); assert_eq!(bounds.end, 0); journal.destroy().await.unwrap(); @@ -2967,7 +2906,9 @@ mod tests { .await .unwrap(); - let items = journal.reader().await.read_many(&[]).await.unwrap(); + let items = super::super::Reader::read_many(&journal, &[]) + .await + .unwrap(); assert!(items.is_empty()); journal.destroy().await.unwrap(); @@ -2980,14 +2921,16 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(10)); - let journal = Journal::init(context.child("j"), cfg).await.unwrap(); + let mut journal = Journal::init(context.child("j"), cfg).await.unwrap(); for i in 0..5u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 5); + assert_eq!(journal.size(), 5); - let items = journal.reader().await.read_many(&[0, 2, 4]).await.unwrap(); + let items = super::super::Reader::read_many(&journal, &[0, 2, 4]) + .await + .unwrap(); assert_eq!(items, vec![test_digest(0), test_digest(2), test_digest(4)]); journal.destroy().await.unwrap(); @@ -3000,15 +2943,17 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(3)); - let journal = Journal::init(context.child("j"), cfg).await.unwrap(); + let mut journal = Journal::init(context.child("j"), cfg).await.unwrap(); for i in 0..9u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 9); + assert_eq!(journal.size(), 9); // Blobs: [0,1,2], [3,4,5], [6,7,8] - let items = journal.reader().await.read_many(&[1, 4, 7]).await.unwrap(); + let items = super::super::Reader::read_many(&journal, &[1, 4, 7]) + .await + .unwrap(); assert_eq!(items, vec![test_digest(1), test_digest(4), test_digest(7)]); journal.destroy().await.unwrap(); @@ -3021,23 +2966,27 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(3)); - let journal = Journal::init(context.child("j"), cfg).await.unwrap(); + let mut journal = Journal::init(context.child("j"), cfg).await.unwrap(); for i in 0..9u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 9); + assert_eq!(journal.size(), 9); journal.sync().await.unwrap(); // Prune first blob [0,1,2]. journal.prune(3).await.unwrap(); - assert_eq!(journal.bounds().await, 3..9); + assert_eq!(journal.bounds(), 3..9); - let items = journal.reader().await.read_many(&[3, 5, 8]).await.unwrap(); + let items = super::super::Reader::read_many(&journal, &[3, 5, 8]) + .await + .unwrap(); assert_eq!(items, vec![test_digest(3), test_digest(5), test_digest(8)]); // Pruned position should error. - let err = journal.reader().await.read_many(&[1]).await.unwrap_err(); + let err = super::super::Reader::read_many(&journal, &[1]) + .await + .unwrap_err(); assert!(matches!(err, Error::ItemPruned(1))); journal.destroy().await.unwrap(); @@ -3049,14 +2998,16 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(10)); - let journal = Journal::init(context.child("j"), cfg).await.unwrap(); + let mut journal = Journal::init(context.child("j"), cfg).await.unwrap(); for i in 0..3u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 3); + assert_eq!(journal.size(), 3); - let err = journal.reader().await.read_many(&[0, 5]).await.unwrap_err(); + let err = super::super::Reader::read_many(&journal, &[0, 5]) + .await + .unwrap_err(); assert!(matches!(err, Error::ItemOutOfRange(5))); journal.destroy().await.unwrap(); @@ -3069,23 +3020,23 @@ mod tests { let executor = deterministic::Runner::default(); executor.start(|context| async move { let cfg = test_cfg(&context, NZU64!(4)); - let journal = Journal::init(context.child("j"), cfg).await.unwrap(); + let mut journal = Journal::init(context.child("j"), cfg).await.unwrap(); for i in 0..20u64 { journal.append(&test_digest(i)).await.unwrap(); } - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); journal.sync().await.unwrap(); let positions: Vec = (0..20).collect(); - let reader = journal.reader().await; - let batch = reader.read_many(&positions).await.unwrap(); + let batch = super::super::Reader::read_many(&journal, &positions) + .await + .unwrap(); for &pos in &positions { - let single = reader.read(pos).await.unwrap(); + let single = journal.read(pos).await.unwrap(); assert_eq!(batch[pos as usize], single); } - drop(reader); journal.destroy().await.unwrap(); }); diff --git a/storage/src/journal/contiguous/variable.rs b/storage/src/journal/contiguous/variable.rs index 6f4af2bbecf..0c1d2783aab 100644 --- a/storage/src/journal/contiguous/variable.rs +++ b/storage/src/journal/contiguous/variable.rs @@ -3,7 +3,6 @@ //! This journal enforces section fullness: all non-final sections are full and synced. //! On init, only the last section needs to be replayed to determine the exact size. -use super::Reader as _; use crate::{ journal::{ contiguous::{fixed, Contiguous, Many, Mutable}, @@ -14,10 +13,7 @@ use crate::{ }; use commonware_codec::{Codec, CodecShared}; use commonware_runtime::buffer::paged::CacheRef; -use commonware_utils::{ - sync::{AsyncRwLockReadGuard, UpgradableAsyncRwLock}, - NZUsize, -}; +use commonware_utils::NZUsize; #[commonware_macros::stability(ALPHA)] use core::ops::Range; use futures::{stream, Stream, StreamExt as _}; @@ -96,84 +92,6 @@ impl Config { } } -/// Inner journal state protected by a lock for interior mutability. -struct Inner { - /// The underlying variable-length data journal. - data: variable::Journal, - - /// The next position to be assigned on append (total items appended). - /// - /// # Invariant - /// - /// Always >= `pruning_boundary`. Equal when data journal is empty or fully pruned. - size: u64, - - /// The position before which all items have been pruned. - /// - /// After normal operation and pruning, the value is section-aligned. - /// After `init_at_size(N)`, the value may be mid-section. - /// - /// # Invariant - /// - /// Never decreases (pruning only moves forward). - pruning_boundary: u64, -} - -impl Inner { - /// Read the item at the given position using the provided offsets reader. - /// - /// # Errors - /// - /// - Returns [Error::ItemPruned] if the item at `position` has been pruned. - /// - Returns [Error::ItemOutOfRange] if `position` is beyond the journal size. - /// - Returns other errors if storage or decoding fails. - async fn read( - &self, - position: u64, - items_per_section: u64, - offsets: &impl super::Reader, - ) -> Result { - if position >= self.size { - return Err(Error::ItemOutOfRange(position)); - } - if position < self.pruning_boundary { - return Err(Error::ItemPruned(position)); - } - - let offset = offsets.read(position).await?; - let section = position_to_section(position, items_per_section); - - self.data.get(section, offset).await - } - - /// Read an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise. - fn try_read_sync( - &self, - position: u64, - items_per_section: u64, - offsets: &impl super::Reader, - ) -> Option { - let mut buf = Vec::new(); - self.try_read_sync_into(position, items_per_section, offsets, &mut buf) - } - - /// Read an item synchronously using caller-provided buffer. - fn try_read_sync_into( - &self, - position: u64, - items_per_section: u64, - offsets: &impl super::Reader, - buf: &mut Vec, - ) -> Option { - if position >= self.size || position < self.pruning_boundary { - return None; - } - let offset = offsets.try_read_sync(position)?; - let section = position_to_section(position, items_per_section); - self.data.try_get_sync_into(section, offset, buf) - } -} - /// A contiguous journal with variable-size entries. /// /// This journal manages section assignment automatically, allowing callers to append items @@ -212,11 +130,25 @@ impl Inner { /// data.bounds().start. This should never occur because we always prune the data journal /// before the offsets journal. pub struct Journal { - /// Inner state for data journal metadata. + /// The underlying variable-length data journal. + data: variable::Journal, + + /// The next position to be assigned on append (total items appended). + /// + /// # Invariant + /// + /// Always >= `pruning_boundary`. Equal when data journal is empty or fully pruned. + size: u64, + + /// The position before which all items have been pruned. + /// + /// After normal operation and pruning, the value is section-aligned. + /// After `init_at_size(N)`, the value may be mid-section. + /// + /// # Invariant /// - /// Serializes persistence and write operations (`sync`, `append`, `prune`, `rewind`) to prevent - /// race conditions while allowing concurrent reads during sync. - inner: UpgradableAsyncRwLock>, + /// Never decreases (pruning only moves forward). + pruning_boundary: u64, /// Index mapping positions to byte offsets within the data journal. /// The section can be calculated from the position using items_per_section. @@ -234,24 +166,39 @@ pub struct Journal { compression: Option, } -/// A reader guard that holds a consistent snapshot of the variable journal's bounds. -pub struct Reader<'a, E: Context, V: Codec> { - guard: AsyncRwLockReadGuard<'a, Inner>, - offsets: fixed::Reader<'a, E, u64>, - items_per_section: u64, +/// Borrowed view of a [`Journal`] returned by [`Contiguous::reader`]. +pub struct Reader<'a, E: Context, V: Codec>(&'a Journal); + +impl Reader<'_, E, V> { + /// Return the bounds of the journal. + pub const fn bounds(&self) -> std::ops::Range { + self.0.pruning_boundary..self.0.size + } + + /// Read the item at the given position. + pub async fn read(&self, position: u64) -> Result { + self.0.read_at(position).await + } + + /// Replay items from the given position. + pub async fn replay( + &self, + buffer_size: NonZeroUsize, + start_pos: u64, + ) -> Result> + Send + '_, Error> { + super::Reader::replay(self, buffer_size, start_pos).await + } } impl super::Reader for Reader<'_, E, V> { type Item = V; fn bounds(&self) -> std::ops::Range { - self.guard.pruning_boundary..self.guard.size + self.0.pruning_boundary..self.0.size } async fn read(&self, position: u64) -> Result { - self.guard - .read(position, self.items_per_section, &self.offsets) - .await + self.0.read_at(position).await } async fn read_many(&self, positions: &[u64]) -> Result, Error> { @@ -263,11 +210,11 @@ impl super::Reader for Reader<'_, E, V> { positions.windows(2).all(|w| w[0] < w[1]), "positions must be sorted and unique" ); - if positions[0] < self.guard.pruning_boundary { + if positions[0] < self.0.pruning_boundary { return Err(Error::ItemPruned(positions[0])); } let last_position = *positions.last().expect("positions is not empty"); - if last_position >= self.guard.size { + if last_position >= self.0.size { return Err(Error::ItemOutOfRange(last_position)); } @@ -277,12 +224,7 @@ impl super::Reader for Reader<'_, E, V> { let mut miss_positions = Vec::with_capacity(positions.len()); let mut buf = Vec::new(); for (i, &position) in positions.iter().enumerate() { - if let Some(item) = self.guard.try_read_sync_into( - position, - self.items_per_section, - &self.offsets, - &mut buf, - ) { + if let Some(item) = self.0.try_read_sync_into(position, &mut buf) { result.push(Some(item)); } else { result.push(None); @@ -296,9 +238,7 @@ impl super::Reader for Reader<'_, E, V> { } // Read the offsets of all items that were not found in the cache. - let miss_offsets = self - .offsets - .read_many(&miss_positions) + let miss_offsets = super::Reader::read_many(&self.0.offsets, &miss_positions) .await .map_err(|e| match e { Error::ItemOutOfRange(e) | Error::ItemPruned(e) => { @@ -311,10 +251,12 @@ impl super::Reader for Reader<'_, E, V> { // consecutive read for each run. let mut group_start = 0; while group_start < miss_positions.len() { - let section = position_to_section(miss_positions[group_start], self.items_per_section); + let section = + position_to_section(miss_positions[group_start], self.0.items_per_section); let mut group_end = group_start + 1; while group_end < miss_positions.len() - && position_to_section(miss_positions[group_end], self.items_per_section) == section + && position_to_section(miss_positions[group_end], self.0.items_per_section) + == section { group_end += 1; } @@ -329,7 +271,7 @@ impl super::Reader for Reader<'_, E, V> { } let items = self - .guard + .0 .data .get_many_consecutive(section, &miss_offsets[run_start..run_end]) .await?; @@ -346,8 +288,7 @@ impl super::Reader for Reader<'_, E, V> { } fn try_read_sync(&self, position: u64) -> Option { - self.guard - .try_read_sync(position, self.items_per_section, &self.offsets) + self.0.try_read_sync(position) } async fn replay( @@ -356,25 +297,25 @@ impl super::Reader for Reader<'_, E, V> { start_pos: u64, ) -> Result> + Send, Error> { // Validate bounds. - if start_pos < self.guard.pruning_boundary { + if start_pos < self.0.pruning_boundary { return Err(Error::ItemPruned(start_pos)); } - if start_pos > self.guard.size { + if start_pos > self.0.size { return Err(Error::ItemOutOfRange(start_pos)); } // Get the starting offset and section. For empty range (start_pos == size), // use a section beyond existing data so data.replay returns empty naturally. - let (start_section, start_offset) = if start_pos < self.guard.size { - let offset = self.offsets.read(start_pos).await?; - let section = position_to_section(start_pos, self.items_per_section); + let (start_section, start_offset) = if start_pos < self.0.size { + let offset = super::Reader::read(&self.0.offsets, start_pos).await?; + let section = position_to_section(start_pos, self.0.items_per_section); (section, offset) } else { (u64::MAX, 0) }; let inner_stream = self - .guard + .0 .data .replay(start_section, start_offset, buffer_size) .await?; @@ -430,11 +371,9 @@ impl Journal { Self::align_journals(&mut data, &mut offsets, items_per_section).await?; Ok(Self { - inner: UpgradableAsyncRwLock::new(Inner { - data, - size, - pruning_boundary, - }), + data, + size, + pruning_boundary, offsets, items_per_section, compression: cfg.compression, @@ -474,11 +413,9 @@ impl Journal { .await?; Ok(Self { - inner: UpgradableAsyncRwLock::new(Inner { - data, - size, - pruning_boundary: size, - }), + data, + size, + pruning_boundary: size, offsets, items_per_section: cfg.items_per_section.get(), compression: cfg.compression, @@ -526,9 +463,9 @@ impl Journal { ); // Initialize contiguous journal - let journal = Self::init(context.child("journal"), cfg.clone()).await?; + let mut journal = Self::init(context.child("journal"), cfg.clone()).await?; - let size = journal.size().await; + let size = journal.size(); // No existing data - initialize at the start of the sync range if needed if size == 0 { @@ -562,7 +499,7 @@ impl Journal { } // Prune to lower bound if needed - let bounds = journal.reader().await.bounds(); + let bounds = journal.reader().bounds(); if !bounds.is_empty() && bounds.start < range.start { debug!( oldest_pos = bounds.start, @@ -587,36 +524,30 @@ impl Journal { /// # Warning /// /// - This operation is not guaranteed to survive restarts until `commit` or `sync` is called. - pub async fn rewind(&self, size: u64) -> Result<(), Error> { - let mut inner = self.inner.write().await; - + pub async fn rewind(&mut self, size: u64) -> Result<(), Error> { // Validate rewind target - match size.cmp(&inner.size) { + match size.cmp(&self.size) { std::cmp::Ordering::Greater => return Err(Error::InvalidRewind(size)), std::cmp::Ordering::Equal => return Ok(()), // No-op std::cmp::Ordering::Less => {} } // Rewind never updates the pruning boundary. - if size < inner.pruning_boundary { + if size < self.pruning_boundary { return Err(Error::ItemPruned(size)); } // Read the offset of the first item to discard (at position 'size'). - let discard_offset = { - let offsets_reader = self.offsets.reader().await; - offsets_reader.read(size).await? - }; + let discard_offset = super::Reader::read(&self.offsets, size).await?; let discard_section = position_to_section(size, self.items_per_section); - inner - .data + self.data .rewind_to_offset(discard_section, discard_offset) .await?; self.offsets.rewind(size).await?; // Update our size - inner.size = size; + self.size = size; Ok(()) } @@ -636,7 +567,7 @@ impl Journal { /// /// Errors may leave the journal in an inconsistent state. The journal should be closed and /// reopened to trigger alignment in [Journal::init]. - pub async fn append(&self, item: &V) -> Result { + pub async fn append(&mut self, item: &V) -> Result { self.append_many(Many::Flat(std::slice::from_ref(item))) .await } @@ -645,13 +576,13 @@ impl Journal { /// /// Acquires the write lock once for all items instead of per-item. /// Returns [Error::EmptyAppend] if items is empty. - pub async fn append_many<'a>(&'a self, items: Many<'a, V>) -> Result { + pub async fn append_many<'a>(&'a mut self, items: Many<'a, V>) -> Result { if items.is_empty() { return Err(Error::EmptyAppend); } let items_count = items.len(); - // Encode every item into a single buffer for bulk-writing before grabbing write guard. + // Encode every item into a single buffer for bulk-writing. let mut encoded = Vec::new(); let mut item_starts = Vec::with_capacity(items_count); let mut encode = |item: &V| { @@ -673,13 +604,10 @@ impl Journal { } } - // Mutating operations are serialized by taking the write guard. - let mut inner = self.inner.write().await; - let mut written = 0; while written < items_count { - let section = position_to_section(inner.size, self.items_per_section); - let pos_in_section = inner.size % self.items_per_section; + let section = position_to_section(self.size, self.items_per_section); + let pos_in_section = self.size % self.items_per_section; let remaining_space = (self.items_per_section - pos_in_section) as usize; let batch_count = remaining_space.min(items_count - written); let batch_start = item_starts[written]; @@ -690,7 +618,7 @@ impl Journal { // Append pre-encoded data to the data journal, then convert relative item starts // into absolute offsets. - let base_offset = inner + let base_offset = self .data .append_raw(section, &encoded[batch_start..batch_end]) .await?; @@ -709,39 +637,60 @@ impl Journal { .offsets .append_many(Many::Flat(&absolute_offsets)) .await?; - assert_eq!(last_offsets_pos, inner.size + batch_count as u64 - 1); + assert_eq!(last_offsets_pos, self.size + batch_count as u64 - 1); - inner.size += batch_count as u64; + self.size += batch_count as u64; written += batch_count; - // The section was filled and must be synced. Downgrade so readers can continue - // during the sync while mutators remain blocked. - if inner.size.is_multiple_of(self.items_per_section) { - let inner_ref = inner.downgrade_to_upgradable(); - futures::try_join!(inner_ref.data.sync(section), self.offsets.sync())?; - if written == items_count { - return Ok(inner_ref.size - 1); - } - inner = inner_ref.upgrade().await; + // The section was filled and must be synced. + if self.size.is_multiple_of(self.items_per_section) { + futures::try_join!(self.data.sync(section), self.offsets.sync())?; } } - Ok(inner.size - 1) + Ok(self.size - 1) } - /// Acquire a reader guard that holds a consistent view of the journal. - pub async fn reader(&self) -> Reader<'_, E, V> { - Reader { - guard: self.inner.read().await, - offsets: self.offsets.reader().await, - items_per_section: self.items_per_section, - } + /// Acquire a reader that borrows the journal. + pub const fn reader(&self) -> Reader<'_, E, V> { + Reader(self) } /// Return the total number of items in the journal, irrespective of pruning. The next value /// appended to the journal will be at this position. - pub async fn size(&self) -> u64 { - self.inner.read().await.size + pub const fn size(&self) -> u64 { + self.size + } + + /// Read the item at the given position. + async fn read_at(&self, position: u64) -> Result { + if position >= self.size { + return Err(Error::ItemOutOfRange(position)); + } + if position < self.pruning_boundary { + return Err(Error::ItemPruned(position)); + } + + let offset = super::Reader::read(&self.offsets, position).await?; + let section = position_to_section(position, self.items_per_section); + + self.data.get(section, offset).await + } + + /// Read an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise. + fn try_read_sync(&self, position: u64) -> Option { + let mut buf = Vec::new(); + self.try_read_sync_into(position, &mut buf) + } + + /// Read an item synchronously using caller-provided buffer. + fn try_read_sync_into(&self, position: u64, buf: &mut Vec) -> Option { + if position >= self.size || position < self.pruning_boundary { + return None; + } + let offset = super::Reader::try_read_sync(&self.offsets, position)?; + let section = position_to_section(position, self.items_per_section); + self.data.try_get_sync_into(section, offset, buf) } /// Prune items at positions strictly less than `min_position`. @@ -754,23 +703,21 @@ impl Journal { /// /// Errors may leave the journal in an inconsistent state. The journal should be closed and /// reopened to trigger alignment in [Journal::init]. - pub async fn prune(&self, min_position: u64) -> Result { - let mut inner = self.inner.write().await; - - if min_position <= inner.pruning_boundary { + pub async fn prune(&mut self, min_position: u64) -> Result { + if min_position <= self.pruning_boundary { return Ok(false); } // Cap min_position to size to maintain the invariant pruning_boundary <= size - let min_position = min_position.min(inner.size); + let min_position = min_position.min(self.size); // Calculate section number let min_section = position_to_section(min_position, self.items_per_section); - let pruned = inner.data.prune(min_section).await?; + let pruned = self.data.prune(min_section).await?; if pruned { - let new_oldest = (min_section * self.items_per_section).max(inner.pruning_boundary); - inner.pruning_boundary = new_oldest; + let new_oldest = (min_section * self.items_per_section).max(self.pruning_boundary); + self.pruning_boundary = new_oldest; self.offsets.prune(new_oldest).await?; } Ok(pruned) @@ -780,30 +727,22 @@ impl Journal { /// /// This is faster than `sync()` but recovery will be required on startup if a crash occurs /// before the next call to `sync()`. - pub async fn commit(&self) -> Result<(), Error> { - // Serialize with append/prune/rewind so section selection is stable, while still allowing - // concurrent readers. - let inner = self.inner.upgradable_read().await; - - let section = position_to_section(inner.size, self.items_per_section); - inner.data.sync(section).await + pub async fn commit(&mut self) -> Result<(), Error> { + let section = position_to_section(self.size, self.items_per_section); + self.data.sync(section).await } /// Durably persist the journal and ensure recovery is not required on startup. /// /// This is slower than `commit()` but ensures the journal doesn't require recovery on startup. - pub async fn sync(&self) -> Result<(), Error> { - // Serialize with append/prune/rewind so section selection is stable, while still allowing - // concurrent readers. - let inner = self.inner.upgradable_read().await; - + pub async fn sync(&mut self) -> Result<(), Error> { // Persist only the current (final) section of the data journal. // All non-final sections are already persisted per Invariant #1. - let section = position_to_section(inner.size, self.items_per_section); + let section = position_to_section(self.size, self.items_per_section); // Persist both journals concurrently. These journals may not exist yet if the // previous section was just filled. This is checked internally. - futures::try_join!(inner.data.sync(section), self.offsets.sync())?; + futures::try_join!(self.data.sync(section), self.offsets.sync())?; Ok(()) } @@ -812,8 +751,7 @@ impl Journal { /// /// This destroys both the data journal and the offsets journal. pub async fn destroy(self) -> Result<(), Error> { - let inner = self.inner.into_inner(); - inner.data.destroy().await?; + self.data.destroy().await?; self.offsets.destroy().await } @@ -822,13 +760,11 @@ impl Journal { /// Unlike `destroy`, this keeps the journal alive so it can be reused. /// After clearing, the journal will behave as if initialized with `init_at_size(new_size)`. #[commonware_macros::stability(ALPHA)] - pub(crate) async fn clear_to_size(&self, new_size: u64) -> Result<(), Error> { - let mut inner = self.inner.write().await; - inner.data.clear().await?; - + pub(crate) async fn clear_to_size(&mut self, new_size: u64) -> Result<(), Error> { + self.data.clear().await?; self.offsets.clear_to_size(new_size).await?; - inner.size = new_size; - inner.pruning_boundary = new_size; + self.size = new_size; + self.pruning_boundary = new_size; Ok(()) } @@ -867,10 +803,7 @@ impl Journal { let data_empty = data.is_empty() || (data.num_sections() == 1 && items_in_last_section == 0); if data_empty { - let offsets_bounds = { - let offsets_reader = offsets.reader().await; - offsets_reader.bounds() - }; + let offsets_bounds = { offsets.bounds() }; let size = offsets_bounds.end; if !data.is_empty() { @@ -914,10 +847,7 @@ impl Journal { // Align pruning state // We always prune data before offsets, so offsets should never be "ahead" by a section. { - let offsets_bounds = { - let offsets_reader = offsets.reader().await; - offsets_reader.bounds() - }; + let offsets_bounds = { offsets.bounds() }; match ( offsets_bounds.is_empty(), offsets_bounds.start.cmp(&data_oldest_pos), @@ -964,8 +894,7 @@ impl Journal { // so the subtraction in oldest_items cannot underflow. // Re-fetch bounds since prune may have been called above. let (offsets_bounds, data_size) = { - let offsets_reader = offsets.reader().await; - let offsets_bounds = offsets_reader.bounds(); + let offsets_bounds = offsets.bounds(); let data_size = if data_first_section == data_last_section { offsets_bounds.start + items_in_last_section } else { @@ -990,8 +919,7 @@ impl Journal { // Final invariant checks let pruning_boundary = { - let offsets_reader = offsets.reader().await; - let offsets_bounds = offsets_reader.bounds(); + let offsets_bounds = offsets.bounds(); assert_eq!(offsets_bounds.end, data_size); // After alignment, offsets and data must be in the same section. @@ -1035,8 +963,7 @@ impl Journal { // Find where to start replaying let (start_section, resume_offset, skip_first) = { - let offsets_reader = offsets.reader().await; - let offsets_bounds = offsets_reader.bounds(); + let offsets_bounds = offsets.bounds(); if offsets_bounds.is_empty() { // Offsets empty -- start from first data section // SAFETY: data is non-empty (checked above) @@ -1044,7 +971,7 @@ impl Journal { (first_section, 0, false) } else if offsets_bounds.start < offsets_size { // Offsets has items -- resume from last indexed position - let last_offset = offsets_reader.read(offsets_size - 1).await?; + let last_offset = super::Reader::read(offsets, offsets_size - 1).await?; let last_section = position_to_section(offsets_size - 1, items_per_section); (last_section, last_offset, true) } else { @@ -1085,11 +1012,11 @@ impl Contiguous for Journal { type Item = V; async fn reader(&self) -> impl super::Reader + '_ { - Self::reader(self).await + Self::reader(self) } async fn size(&self) -> u64 { - Self::size(self).await + Self::size(self) } } @@ -1114,12 +1041,12 @@ impl Mutable for Journal { impl Persistable for Journal { type Error = Error; - async fn commit(&self) -> Result<(), Error> { - self.commit().await + async fn commit(&mut self) -> Result<(), Error> { + Self::commit(self).await } - async fn sync(&self) -> Result<(), Error> { - self.sync().await + async fn sync(&mut self) -> Result<(), Error> { + Self::sync(self).await } async fn destroy(self) -> Result<(), Error> { @@ -1160,51 +1087,47 @@ impl crate::journal::authenticated::Inner for Jou impl Journal { /// Test helper: Read the item at the given position. pub(crate) async fn read(&self, position: u64) -> Result { - self.reader().await.read(position).await + self.reader().read(position).await } /// Test helper: Return the bounds of the journal. - pub(crate) async fn bounds(&self) -> std::ops::Range { - self.reader().await.bounds() + pub(crate) fn bounds(&self) -> std::ops::Range { + self.reader().bounds() } /// Test helper: Prune the internal data journal directly (simulates crash scenario). - pub(crate) async fn test_prune_data(&self, section: u64) -> Result { - let mut inner = self.inner.write().await; - inner.data.prune(section).await + pub(crate) async fn test_prune_data(&mut self, section: u64) -> Result { + self.data.prune(section).await } /// Test helper: Prune the internal offsets journal directly (simulates crash scenario). - pub(crate) async fn test_prune_offsets(&self, position: u64) -> Result { + pub(crate) async fn test_prune_offsets(&mut self, position: u64) -> Result { self.offsets.prune(position).await } /// Test helper: Rewind the internal offsets journal directly (simulates crash scenario). - pub(crate) async fn test_rewind_offsets(&self, position: u64) -> Result<(), Error> { + pub(crate) async fn test_rewind_offsets(&mut self, position: u64) -> Result<(), Error> { self.offsets.rewind(position).await } /// Test helper: Get the size of the internal offsets journal. - pub(crate) async fn test_offsets_size(&self) -> u64 { - self.offsets.size().await + pub(crate) fn test_offsets_size(&self) -> u64 { + self.offsets.size() } /// Test helper: Append directly to the internal data journal (simulates crash scenario). pub(crate) async fn test_append_data( - &self, + &mut self, section: u64, item: V, ) -> Result<(u64, u32), Error> { - let mut inner = self.inner.write().await; - inner.data.append(section, &item).await + self.data.append(section, &item).await } /// Test helper: Sync the internal data journal. - pub(crate) async fn test_sync_data(&self) -> Result<(), Error> { - let inner = self.inner.read().await; - inner - .data - .sync(inner.data.newest_section().unwrap_or(0)) + pub(crate) async fn test_sync_data(&mut self) -> Result<(), Error> { + self.data + .sync(self.data.newest_section().unwrap_or(0)) .await } } @@ -1217,7 +1140,7 @@ mod tests { use commonware_runtime::{ buffer::paged::CacheRef, deterministic, Runner, Storage, Supervisor as _, }; - use commonware_utils::{sequence::FixedBytes, NZUsize, NZU16, NZU64}; + use commonware_utils::{NZUsize, NZU16, NZU64}; use futures::FutureExt as _; use std::num::NonZeroU16; @@ -1228,116 +1151,6 @@ mod tests { const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(1024); const SMALL_PAGE_SIZE: NonZeroU16 = NZU16!(512); - #[test_traced] - fn test_variable_append_many_compressed() { - let executor = deterministic::Runner::default(); - executor.start(|context| async move { - let cfg = Config { - partition: "append-many-compressed".into(), - items_per_section: NZU64!(3), - compression: Some(1), - codec_config: (), - page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)), - write_buffer: NZUsize!(1024), - }; - let journal = Journal::<_, FixedBytes<32>>::init(context.child("journal"), cfg) - .await - .unwrap(); - let items = [ - FixedBytes::new([0; 32]), - FixedBytes::new([1; 32]), - FixedBytes::new([2; 32]), - FixedBytes::new([3; 32]), - FixedBytes::new([4; 32]), - ]; - - let last = journal.append_many(Many::Flat(&items)).await.unwrap(); - assert_eq!(last, 4); - for (pos, item) in items.iter().enumerate() { - assert_eq!(journal.read(pos as u64).await.unwrap(), *item); - } - - journal.destroy().await.unwrap(); - }); - } - - #[test_traced] - fn test_variable_read_many_after_reopen() { - let executor = deterministic::Runner::default(); - executor.start(|context| async move { - let cfg = Config { - partition: "read-many-after-reopen".into(), - items_per_section: NZU64!(5), - compression: None, - codec_config: (), - page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)), - write_buffer: NZUsize!(1024), - }; - - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) - .await - .unwrap(); - for i in 0..20u64 { - journal.append(&(i * 100)).await.unwrap(); - } - journal.sync().await.unwrap(); - drop(journal); - - let cfg = Config { - page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)), - ..cfg - }; - let journal = Journal::<_, u64>::init(context.child("second"), cfg) - .await - .unwrap(); - let reader = journal.reader().await; - let items = reader.read_many(&[1, 2, 3, 7, 8, 12, 18]).await.unwrap(); - assert_eq!(items, vec![100, 200, 300, 700, 800, 1200, 1800]); - drop(reader); - - journal.destroy().await.unwrap(); - }); - } - - #[test_traced] - fn test_variable_read_many_consecutive_after_reopen() { - let executor = deterministic::Runner::default(); - executor.start(|context| async move { - let cfg = Config { - partition: "read-many-consecutive-after-reopen".into(), - items_per_section: NZU64!(20), - compression: None, - codec_config: (), - page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)), - write_buffer: NZUsize!(1024), - }; - - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) - .await - .unwrap(); - for i in 0..20u64 { - journal.append(&(i * 100)).await.unwrap(); - } - journal.sync().await.unwrap(); - drop(journal); - - let cfg = Config { - page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)), - ..cfg - }; - let journal = Journal::<_, u64>::init(context.child("second"), cfg) - .await - .unwrap(); - let reader = journal.reader().await; - let positions: Vec = (3..10).collect(); - let items = reader.read_many(&positions).await.unwrap(); - assert_eq!(items, vec![300, 400, 500, 600, 700, 800, 900]); - drop(reader); - - journal.destroy().await.unwrap(); - }); - } - /// Test that complete offsets partition loss after pruning is detected as unrecoverable. /// /// When the offsets partition is completely lost and the data has been pruned, we cannot @@ -1357,7 +1170,7 @@ mod tests { }; // === Phase 1: Create journal with data and prune === - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1368,7 +1181,7 @@ mod tests { // Prune to position 20 (removes sections 0-1, keeps sections 2-3) journal.prune(20).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 20); assert_eq!(bounds.end, 40); @@ -1413,7 +1226,7 @@ mod tests { }; // === Setup: Create journal with data === - let variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1432,15 +1245,15 @@ mod tests { .expect("Failed to remove data partition"); // === Verify init aligns the mismatch === - let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) .await .expect("Should align offsets to match empty data"); // Size should be preserved - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); // But no items remain (both journals pruned) - assert!(journal.bounds().await.is_empty()); + assert!(journal.bounds().is_empty()); // All reads should fail with ItemPruned for i in 0..20 { @@ -1474,7 +1287,7 @@ mod tests { }; // Initialize journal - let journal = Journal::<_, u64>::init(context, cfg).await.unwrap(); + let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap(); // Append 40 items across 4 sections (0-3) for i in 0..40u64 { @@ -1483,7 +1296,7 @@ mod tests { // Test 1: Full replay { - let reader = journal.reader().await; + let reader = journal.reader(); let stream = reader.replay(NZUsize!(20), 0).await.unwrap(); futures::pin_mut!(stream); for i in 0..40u64 { @@ -1496,7 +1309,7 @@ mod tests { // Test 2: Partial replay from middle of section { - let reader = journal.reader().await; + let reader = journal.reader(); let stream = reader.replay(NZUsize!(20), 15).await.unwrap(); futures::pin_mut!(stream); for i in 15..40u64 { @@ -1509,7 +1322,7 @@ mod tests { // Test 3: Partial replay from section boundary { - let reader = journal.reader().await; + let reader = journal.reader(); let stream = reader.replay(NZUsize!(20), 20).await.unwrap(); futures::pin_mut!(stream); for i in 20..40u64 { @@ -1523,19 +1336,19 @@ mod tests { // Test 4: Prune and verify replay from pruned journal.prune(20).await.unwrap(); { - let reader = journal.reader().await; + let reader = journal.reader(); let res = reader.replay(NZUsize!(20), 0).await; assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_)))); } { - let reader = journal.reader().await; + let reader = journal.reader(); let res = reader.replay(NZUsize!(20), 19).await; assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_)))); } // Test 5: Replay from exactly at pruning boundary after prune { - let reader = journal.reader().await; + let reader = journal.reader(); let stream = reader.replay(NZUsize!(20), 20).await.unwrap(); futures::pin_mut!(stream); for i in 20..40u64 { @@ -1548,7 +1361,7 @@ mod tests { // Test 6: Replay from the end { - let reader = journal.reader().await; + let reader = journal.reader(); let stream = reader.replay(NZUsize!(20), 40).await.unwrap(); futures::pin_mut!(stream); assert!(stream.next().await.is_none()); @@ -1556,7 +1369,7 @@ mod tests { // Test 7: Replay beyond the end (should error) { - let reader = journal.reader().await; + let reader = journal.reader(); let res = reader.replay(NZUsize!(20), 41).await; assert!(matches!( res, @@ -1576,7 +1389,7 @@ mod tests { let label = test_name.replace('-', "_"); let context = context .child("test") - .with_attribute("name", &label) + .with_attribute("name", label) .with_attribute("index", idx); async move { let cfg = Config { @@ -1609,7 +1422,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let journal = Journal::<_, u64>::init(context, cfg).await.unwrap(); + let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap(); // Append items across 4 sections: [0-9], [10-19], [20-29], [30-39] for i in 0..40u64 { @@ -1617,7 +1430,7 @@ mod tests { } // Initial state: all items accessible - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 0); assert_eq!(bounds.end, 40); @@ -1626,7 +1439,7 @@ mod tests { assert!(pruned); // Variable-specific guarantee: oldest is EXACTLY at section boundary - assert_eq!(journal.bounds().await.start, 10); + assert_eq!(journal.bounds().start, 10); // Items 0-9 should be pruned, 10+ should be accessible assert!(matches!( @@ -1641,7 +1454,7 @@ mod tests { assert!(pruned); // Variable-specific guarantee: oldest is EXACTLY at section boundary - assert_eq!(journal.bounds().await.start, 20); + assert_eq!(journal.bounds().start, 20); // Items 0-19 should be pruned, 20+ should be accessible assert!(matches!( @@ -1660,7 +1473,7 @@ mod tests { assert!(pruned); // Variable-specific guarantee: oldest is EXACTLY at section boundary - assert_eq!(journal.bounds().await.start, 30); + assert_eq!(journal.bounds().start, 30); // Items 0-29 should be pruned, 30+ should be accessible assert!(matches!( @@ -1675,7 +1488,7 @@ mod tests { assert_eq!(journal.read(39).await.unwrap(), 3900); // Size should still be 40 (pruning doesn't affect size) - assert_eq!(journal.size().await, 40); + assert_eq!(journal.size(), 40); journal.destroy().await.unwrap(); }); @@ -1696,7 +1509,7 @@ mod tests { }; // === Phase 1: Create journal and append data === - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1704,7 +1517,7 @@ mod tests { journal.append(&(i * 100)).await.unwrap(); } - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 100); assert_eq!(bounds.start, 0); @@ -1713,7 +1526,7 @@ mod tests { assert!(pruned); // All data is pruned - no items remain - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 100); assert!(bounds.is_empty()); @@ -1729,12 +1542,12 @@ mod tests { drop(journal); // === Phase 3: Re-init and verify position preserved === - let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) .await .unwrap(); // Size should be preserved, but no items remain - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 100); assert!(bounds.is_empty()); @@ -1749,7 +1562,7 @@ mod tests { // === Phase 4: Append new data === // Next append should get position 100 journal.append(&10000).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 101); // Now we have one item at position 100 assert_eq!(bounds.start, 100); @@ -1782,7 +1595,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1793,7 +1606,7 @@ mod tests { // Prune to position 10 normally (both data and offsets journals pruned) variable.prune(10).await.unwrap(); - assert_eq!(variable.bounds().await.start, 10); + assert_eq!(variable.bounds().start, 10); // === Simulate crash: Prune data journal but not offsets journal === // Manually prune data journal to section 2 (position 20) @@ -1809,7 +1622,7 @@ mod tests { .unwrap(); // Init should auto-repair: offsets journal pruned to match data journal - let bounds = variable.bounds().await; + let bounds = variable.bounds(); assert_eq!(bounds.start, 20); assert_eq!(bounds.end, 40); @@ -1845,7 +1658,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1882,7 +1695,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1891,7 +1704,7 @@ mod tests { variable.append(&(i * 100)).await.unwrap(); } - assert_eq!(variable.size().await, 15); + assert_eq!(variable.size(), 15); // Manually append 5 more items directly to data journal only for i in 15..20u64 { @@ -1908,7 +1721,7 @@ mod tests { .unwrap(); // Init should rebuild offsets journal from data journal replay - let bounds = variable.bounds().await; + let bounds = variable.bounds(); assert_eq!(bounds.end, 20); assert_eq!(bounds.start, 0); @@ -1918,7 +1731,7 @@ mod tests { } // Offsets journal should be fully rebuilt to match data journal - assert_eq!(variable.test_offsets_size().await, 20); + assert_eq!(variable.test_offsets_size(), 20); variable.destroy().await.unwrap(); }); @@ -1939,7 +1752,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -1950,7 +1763,7 @@ mod tests { // Prune to position 10 normally (both data and offsets journals pruned) variable.prune(10).await.unwrap(); - assert_eq!(variable.bounds().await.start, 10); + assert_eq!(variable.bounds().start, 10); // === Simulate crash: Multiple prunes on data journal, not on offsets journal === // Manually prune data journal to section 3 (position 30) @@ -1966,7 +1779,7 @@ mod tests { .unwrap(); // Init should auto-repair: offsets journal pruned to match data journal - let bounds = variable.bounds().await; + let bounds = variable.bounds(); assert_eq!(bounds.start, 30); assert_eq!(bounds.end, 50); @@ -2008,7 +1821,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -2017,7 +1830,7 @@ mod tests { variable.append(&(i * 100)).await.unwrap(); } - assert_eq!(variable.size().await, 25); + assert_eq!(variable.size(), 25); // === Simulate crash during rewind(5) === // Rewind offsets journal to size 5 (keeps positions 0-4) @@ -2028,12 +1841,12 @@ mod tests { drop(variable); // === Verify recovery === - let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone()) + let mut variable = Journal::<_, u64>::init(context.child("second"), cfg.clone()) .await .unwrap(); // Init should rebuild offsets[5-24] from data journal across all 3 sections - let bounds = variable.bounds().await; + let bounds = variable.bounds(); assert_eq!(bounds.end, 25); assert_eq!(bounds.start, 0); @@ -2043,7 +1856,7 @@ mod tests { } // Verify offsets journal fully rebuilt - assert_eq!(variable.test_offsets_size().await, 25); + assert_eq!(variable.test_offsets_size(), 25); // Verify next append gets position 25 let pos = variable.append(&2500).await.unwrap(); @@ -2070,7 +1883,7 @@ mod tests { }; // === Phase 1: Create journal with one full section === - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -2078,13 +1891,13 @@ mod tests { for i in 0..10u64 { journal.append(&(i * 100)).await.unwrap(); } - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 0); // === Phase 2: Prune to create empty journal === journal.prune(10).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert!(bounds.is_empty()); // Empty! @@ -2107,7 +1920,7 @@ mod tests { .expect("Should recover from crash after data sync but before offsets sync"); // All data should be recovered - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 20); assert_eq!(bounds.start, 10); @@ -2139,7 +1952,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); @@ -2159,7 +1972,7 @@ mod tests { .unwrap(); // Data should be intact and offsets rebuilt - assert_eq!(journal.size().await, 15); + assert_eq!(journal.size(), 15); for i in 0..15u64 { assert_eq!(journal.read(i).await.unwrap(), i * 100); } @@ -2181,20 +1994,21 @@ mod tests { write_buffer: NZUsize!(1024), }; - let journal = Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 0) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 0) + .await + .unwrap(); // Size should be 0 - assert_eq!(journal.size().await, 0); + assert_eq!(journal.size(), 0); // No oldest retained position (empty journal) - assert!(journal.bounds().await.is_empty()); + assert!(journal.bounds().is_empty()); // Next append should get position 0 let pos = journal.append(&100).await.unwrap(); assert_eq!(pos, 0); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); assert_eq!(journal.read(0).await.unwrap(), 100); journal.destroy().await.unwrap(); @@ -2215,13 +2029,13 @@ mod tests { }; // Initialize at position 10 (exactly at section 1 boundary with items_per_section=5) - let journal = + let mut journal = Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 10) .await .unwrap(); // Size should be 10 - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); // No data yet, so no oldest retained position @@ -2230,7 +2044,7 @@ mod tests { // Next append should get position 10 let pos = journal.append(&1000).await.unwrap(); assert_eq!(pos, 10); - assert_eq!(journal.size().await, 11); + assert_eq!(journal.size(), 11); assert_eq!(journal.read(10).await.unwrap(), 1000); // Can continue appending @@ -2256,12 +2070,13 @@ mod tests { }; // Initialize at position 7 (middle of section 1 with items_per_section=5) - let journal = Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 7) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 7) + .await + .unwrap(); // Size should be 7 - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 7); // No data yet, so no oldest retained position @@ -2270,7 +2085,7 @@ mod tests { // Next append should get position 7 let pos = journal.append(&700).await.unwrap(); assert_eq!(pos, 7); - assert_eq!(journal.size().await, 8); + assert_eq!(journal.size(), 8); assert_eq!(journal.read(7).await.unwrap(), 700); journal.destroy().await.unwrap(); @@ -2291,9 +2106,10 @@ mod tests { }; // Initialize at position 15 - let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15) + .await + .unwrap(); // Append some items for i in 0..5u64 { @@ -2301,18 +2117,18 @@ mod tests { assert_eq!(pos, 15 + i); } - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); // Sync and reopen journal.sync().await.unwrap(); drop(journal); - let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) .await .unwrap(); // Size and data should be preserved - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 20); assert_eq!(bounds.start, 15); @@ -2348,7 +2164,7 @@ mod tests { .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 15); assert!(bounds.is_empty()); @@ -2356,11 +2172,11 @@ mod tests { drop(journal); // Reopen and verify size persisted - let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 15); assert!(bounds.is_empty()); @@ -2388,9 +2204,10 @@ mod tests { }; // Initialize at position 7 (mid-section, 7 % 5 = 2) - let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) + .await + .unwrap(); // Append 3 items at positions 7, 8, 9 (fills rest of section 1) for i in 0..3u64 { @@ -2398,7 +2215,7 @@ mod tests { assert_eq!(pos, 7 + i); } - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 7); @@ -2412,7 +2229,7 @@ mod tests { .unwrap(); // Size and bounds.start should be preserved correctly - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 7); @@ -2443,9 +2260,10 @@ mod tests { }; // Initialize at position 7 (mid-section) - let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) + .await + .unwrap(); // Append 8 items: positions 7-14 (section 1: 3 items, section 2: 5 items) for i in 0..8u64 { @@ -2453,7 +2271,7 @@ mod tests { assert_eq!(pos, 7 + i); } - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 15); assert_eq!(bounds.start, 7); @@ -2467,7 +2285,7 @@ mod tests { .unwrap(); // Verify state preserved - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 15); assert_eq!(bounds.start, 7); @@ -2495,7 +2313,7 @@ mod tests { }; // Phase 1: Create data and offsets, then simulate data-only pruning crash. - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); for i in 0..7u64 { @@ -2504,40 +2322,33 @@ mod tests { journal.sync().await.unwrap(); // Simulate crash after data was cleared but before offsets were pruned. - journal.inner.write().await.data.clear().await.unwrap(); + journal.data.clear().await.unwrap(); drop(journal); // Phase 2: Init triggers data-empty repair and should treat journal as fully pruned at size 7. - let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 7); assert!(bounds.is_empty()); // Append one item at position 7. let pos = journal.append(&777).await.unwrap(); assert_eq!(pos, 7); - assert_eq!(journal.size().await, 8); + assert_eq!(journal.size(), 8); assert_eq!(journal.read(7).await.unwrap(), 777); // Sync only the data journal to simulate a crash before offsets are synced. let section = 7 / cfg.items_per_section.get(); - journal - .inner - .write() - .await - .data - .sync(section) - .await - .unwrap(); + journal.data.sync(section).await.unwrap(); drop(journal); // Phase 3: Reopen and verify we did not lose the appended item. let journal = Journal::<_, u64>::init(context.child("third"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 8); assert_eq!(bounds.start, 7); assert_eq!(journal.read(7).await.unwrap(), 777); @@ -2561,9 +2372,10 @@ mod tests { }; // Initialize at position 7 (mid-section) - let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) + .await + .unwrap(); // Append 3 items for i in 0..3u64 { @@ -2571,7 +2383,7 @@ mod tests { } // Sync only the data journal, not offsets (simulate crash) - journal.inner.write().await.data.sync(1).await.unwrap(); + journal.data.sync(1).await.unwrap(); // Don't sync offsets - simulates crash after data write but before offsets write drop(journal); @@ -2581,7 +2393,7 @@ mod tests { .unwrap(); // Verify recovery - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 7); @@ -2607,20 +2419,21 @@ mod tests { write_buffer: NZUsize!(1024), }; - let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) - .await - .unwrap(); + let mut journal = + Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7) + .await + .unwrap(); // Append a few items at positions 7..9 for i in 0..3u64 { let pos = journal.append(&(700 + i)).await.unwrap(); assert_eq!(pos, 7 + i); } - assert_eq!(journal.bounds().await.start, 7); + assert_eq!(journal.bounds().start, 7); // Prune to a position within the same section should not move bounds.start backwards. journal.prune(8).await.unwrap(); - assert_eq!(journal.bounds().await.start, 7); + assert_eq!(journal.bounds().start, 7); assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6)))); assert_eq!(journal.read(7).await.unwrap(), 700); @@ -2642,12 +2455,12 @@ mod tests { }; // Initialize at a large position (position 1000) - let journal = + let mut journal = Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 1000) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 1000); // No data yet, so no oldest retained position assert!(bounds.is_empty()); @@ -2675,7 +2488,7 @@ mod tests { }; // Initialize at position 20 - let journal = + let mut journal = Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 20) .await .unwrap(); @@ -2685,12 +2498,12 @@ mod tests { journal.append(&(2000 + i)).await.unwrap(); } - assert_eq!(journal.size().await, 30); + assert_eq!(journal.size(), 30); // Prune to position 25 journal.prune(25).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 30); assert_eq!(bounds.start, 25); @@ -2724,7 +2537,7 @@ mod tests { // Initialize journal with sync boundaries when no existing data exists let lower_bound = 10; let upper_bound = 26; - let journal = Journal::init_sync( + let mut journal = Journal::init_sync( context.child("storage"), cfg.clone(), lower_bound..upper_bound, @@ -2732,7 +2545,7 @@ mod tests { .await .expect("Failed to initialize journal with sync boundaries"); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, lower_bound); assert!(bounds.is_empty()); @@ -2764,7 +2577,7 @@ mod tests { }; // Create initial journal with data in multiple sections - let journal = + let mut journal = Journal::::init(context.child("storage"), cfg.clone()) .await .expect("Failed to create initial journal"); @@ -2780,7 +2593,7 @@ mod tests { // lower_bound: 8 (section 1), upper_bound: 31 (last location 30, section 6) let lower_bound = 8; let upper_bound = 31; - let journal = Journal::::init_sync( + let mut journal = Journal::::init_sync( context.child("storage"), cfg.clone(), lower_bound..upper_bound, @@ -2788,10 +2601,10 @@ mod tests { .await .expect("Failed to initialize journal with overlap"); - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); // Verify oldest retained is pruned to lower_bound's section boundary (5) - assert_eq!(journal.bounds().await.start, 5); // Section 1 starts at position 5 + assert_eq!(journal.bounds().start, 5); // Section 1 starts at position 5 // Verify data integrity: positions before 5 are pruned assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_)))); @@ -2858,7 +2671,7 @@ mod tests { }; // Create initial journal with data exactly matching sync range - let journal = + let mut journal = Journal::::init(context.child("storage"), cfg.clone()) .await .expect("Failed to create initial journal"); @@ -2873,7 +2686,7 @@ mod tests { // Initialize with sync boundaries that exactly match existing data let lower_bound = 5; // section 1 let upper_bound = 20; // section 3 - let journal = Journal::::init_sync( + let mut journal = Journal::::init_sync( context.child("storage"), cfg.clone(), lower_bound..upper_bound, @@ -2881,10 +2694,10 @@ mod tests { .await .expect("Failed to initialize journal with exact match"); - assert_eq!(journal.size().await, 20); + assert_eq!(journal.size(), 20); // Verify pruning to lower bound (section 1 boundary = position 5) - assert_eq!(journal.bounds().await.start, 5); // Section 1 starts at position 5 + assert_eq!(journal.bounds().start, 5); // Section 1 starts at position 5 // Verify positions before 5 are pruned assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_)))); @@ -2927,7 +2740,7 @@ mod tests { }; // Create initial journal with data beyond sync range - let journal = + let mut journal = Journal::::init(context.child("initial"), cfg.clone()) .await .expect("Failed to create initial journal"); @@ -2971,7 +2784,7 @@ mod tests { }; // Create initial journal with stale data - let journal = + let mut journal = Journal::::init(context.child("first"), cfg.clone()) .await .expect("Failed to create initial journal"); @@ -2994,10 +2807,10 @@ mod tests { .await .expect("Failed to initialize journal with stale data"); - assert_eq!(journal.size().await, 15); + assert_eq!(journal.size(), 15); // Verify fresh journal (all old data destroyed, starts at position 15) - assert!(journal.bounds().await.is_empty()); + assert!(journal.bounds().is_empty()); // Verify old positions don't exist assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_)))); @@ -3024,7 +2837,7 @@ mod tests { }; // Create journal with data at section boundaries - let journal = + let mut journal = Journal::::init(context.child("storage"), cfg.clone()) .await .expect("Failed to create initial journal"); @@ -3039,7 +2852,7 @@ mod tests { // Test sync boundaries exactly at section boundaries let lower_bound = 15; // Exactly at section boundary (15/5 = 3) let upper_bound = 25; // Last element exactly at section boundary (24/5 = 4) - let journal = Journal::::init_sync( + let mut journal = Journal::::init_sync( context.child("storage"), cfg.clone(), lower_bound..upper_bound, @@ -3047,10 +2860,10 @@ mod tests { .await .expect("Failed to initialize journal at boundaries"); - assert_eq!(journal.size().await, 25); + assert_eq!(journal.size(), 25); // Verify oldest retained is at section 3 boundary (position 15) - assert_eq!(journal.bounds().await.start, 15); + assert_eq!(journal.bounds().start, 15); // Verify positions before 15 are pruned assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_)))); @@ -3092,7 +2905,7 @@ mod tests { }; // Create journal with data in multiple sections - let journal = + let mut journal = Journal::::init(context.child("storage"), cfg.clone()) .await .expect("Failed to create initial journal"); @@ -3107,7 +2920,7 @@ mod tests { // Test sync boundaries within the same section let lower_bound = 10; // operation 10 (section 2: 10/5 = 2) let upper_bound = 15; // Last operation 14 (section 2: 14/5 = 2) - let journal = Journal::::init_sync( + let mut journal = Journal::::init_sync( context.child("storage"), cfg.clone(), lower_bound..upper_bound, @@ -3115,11 +2928,11 @@ mod tests { .await .expect("Failed to initialize journal with same-section bounds"); - assert_eq!(journal.size().await, 15); + assert_eq!(journal.size(), 15); // Both operations are in section 2, so sections 0, 1 should be pruned, section 2 retained // Oldest retained position should be at section 2 boundary (position 10) - assert_eq!(journal.bounds().await.start, 10); + assert_eq!(journal.bounds().start, 10); // Verify positions before 10 are pruned assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_)))); @@ -3163,35 +2976,35 @@ mod tests { }; // === Test 1: Basic single item operation === - let journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone()) .await .unwrap(); // Verify empty state - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 0); assert!(bounds.is_empty()); // Append 1 item (value = position * 100, so position 0 has value 0) let pos = journal.append(&0).await.unwrap(); assert_eq!(pos, 0); - assert_eq!(journal.size().await, 1); + assert_eq!(journal.size(), 1); // Sync journal.sync().await.unwrap(); // Read from size() - 1 - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, 0); // === Test 2: Multiple items with single item per section === for i in 1..10u64 { let pos = journal.append(&(i * 100)).await.unwrap(); assert_eq!(pos, i); - assert_eq!(journal.size().await, i + 1); + assert_eq!(journal.size(), i + 1); // Verify we can read the just-appended item at size() - 1 - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, i * 100); } @@ -3208,13 +3021,13 @@ mod tests { assert!(pruned); // Size should still be 10 - assert_eq!(journal.size().await, 10); + assert_eq!(journal.size(), 10); // bounds.start should be 5 - assert_eq!(journal.bounds().await.start, 5); + assert_eq!(journal.bounds().start, 5); // Reading from bounds.end - 1 (position 9) should still work - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, 900); // Reading from pruned positions should return ItemPruned @@ -3236,7 +3049,7 @@ mod tests { assert_eq!(pos, i); // Verify we can read from size() - 1 - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, i * 100); } @@ -3249,13 +3062,13 @@ mod tests { .unwrap(); // Verify size is preserved - assert_eq!(journal.size().await, 15); + assert_eq!(journal.size(), 15); // Verify bounds.start is preserved - assert_eq!(journal.bounds().await.start, 5); + assert_eq!(journal.bounds().start, 5); // Reading from bounds.end - 1 should work after restart - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, 1400); // Reading all retained positions should work @@ -3267,7 +3080,7 @@ mod tests { // === Test 5: Restart after pruning with non-zero index (KEY SCENARIO) === // Fresh journal for this test - let journal = Journal::<_, u64>::init(context.child("third"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("third"), cfg.clone()) .await .unwrap(); @@ -3278,7 +3091,7 @@ mod tests { // Prune to position 5 (removes positions 0-4) journal.prune(5).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 5); @@ -3292,12 +3105,12 @@ mod tests { .unwrap(); // Verify state after restart - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 10); assert_eq!(bounds.start, 5); // KEY TEST: Reading from bounds.end - 1 (position 9) should work - let value = journal.read(journal.size().await - 1).await.unwrap(); + let value = journal.read(journal.size() - 1).await.unwrap(); assert_eq!(value, 9000); // Verify all retained positions (5-9) work @@ -3310,7 +3123,7 @@ mod tests { // === Test 6: Prune all items (edge case) === // This tests the scenario where prune removes everything. // Callers must check bounds().is_empty() before reading. - let journal = Journal::<_, u64>::init(context.child("fifth"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("fifth"), cfg.clone()) .await .unwrap(); @@ -3321,17 +3134,17 @@ mod tests { // Prune all items journal.prune(5).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 5); // Size unchanged assert!(bounds.is_empty()); // All pruned // bounds.end - 1 = 4, but position 4 is pruned - let result = journal.read(journal.size().await - 1).await; + let result = journal.read(journal.size() - 1).await; assert!(matches!(result, Err(crate::journal::Error::ItemPruned(4)))); // After appending, reading works again journal.append(&500).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.start, 5); assert_eq!(journal.read(bounds.end - 1).await.unwrap(), 500); @@ -3352,7 +3165,7 @@ mod tests { write_buffer: NZUsize!(1024), }; - let journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone()) + let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone()) .await .unwrap(); @@ -3360,14 +3173,14 @@ mod tests { for i in 0..25u64 { journal.append(&(i * 100)).await.unwrap(); } - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 25); assert_eq!(bounds.start, 0); journal.sync().await.unwrap(); // Clear to position 100, effectively resetting the journal journal.clear_to_size(100).await.unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 100); assert!(bounds.is_empty()); @@ -3381,11 +3194,11 @@ mod tests { // Verify size persists after restart without writing any data drop(journal); - let journal = + let mut journal = Journal::<_, u64>::init(context.child("journal_after_clear"), cfg.clone()) .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 100); assert!(bounds.is_empty()); @@ -3394,7 +3207,7 @@ mod tests { let pos = journal.append(&(i * 100)).await.unwrap(); assert_eq!(pos, i); } - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 105); assert_eq!(bounds.start, 100); @@ -3411,7 +3224,7 @@ mod tests { .await .unwrap(); - let bounds = journal.bounds().await; + let bounds = journal.bounds(); assert_eq!(bounds.end, 105); assert_eq!(bounds.start, 100); for i in 100..105u64 { diff --git a/storage/src/lib.rs b/storage/src/lib.rs index d7344e0cd05..1fb1e65881f 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -55,7 +55,7 @@ commonware_macros::stability_scope!(BETA, cfg(feature = "std") { /// Durably persist the structure, guaranteeing the current state will survive a crash. /// /// For a stronger guarantee that eliminates potential recovery, use [Self::sync] instead. - fn commit(&self) -> impl std::future::Future> + Send { + fn commit(&mut self) -> impl std::future::Future> + Send { self.sync() } @@ -63,7 +63,7 @@ commonware_macros::stability_scope!(BETA, cfg(feature = "std") { /// no recovery will be needed on startup. /// /// This provides a stronger guarantee than [Self::commit] but may be slower. - fn sync(&self) -> impl std::future::Future> + Send; + fn sync(&mut self) -> impl std::future::Future> + Send; /// Destroy the structure, removing all associated storage. /// diff --git a/storage/src/merkle/persisted/full.rs b/storage/src/merkle/persisted/full.rs index 782e840c443..cefb538d4a0 100644 --- a/storage/src/merkle/persisted/full.rs +++ b/storage/src/merkle/persisted/full.rs @@ -10,7 +10,7 @@ use crate::{ journal::{ contiguous::{ fixed::{Config as JConfig, Journal}, - Many, Reader, + Many, }, Error as JError, }, @@ -26,11 +26,7 @@ use commonware_codec::DecodeExt; use commonware_cryptography::Digest; use commonware_parallel::{Sequential, Strategy}; use commonware_runtime::{buffer::paged::CacheRef, Clock, Metrics, Storage as RStorage}; -use commonware_utils::{ - range::NonEmptyRange, - sequence::prefixed_u64::U64, - sync::{AsyncMutex, RwLock}, -}; +use commonware_utils::{range::NonEmptyRange, sequence::prefixed_u64::U64}; use std::{ collections::BTreeMap, num::{NonZeroU64, NonZeroUsize}, @@ -85,18 +81,6 @@ impl UnmerkleizedBatch { } } -/// Fields of [Merkle] that are protected by an [RwLock] for interior mutability. -pub(crate) struct Inner { - /// A memory resident Merkle structure used to build the structure and cache updates. It caches - /// all un-synced nodes, and the pinned node set as derived from both its own pruning boundary - /// and the full structure's pruning boundary. - pub(crate) mem: Mem, - - /// The highest position for which this structure has been pruned, or 0 if it has never been - /// pruned. - pub(crate) pruned_to_pos: Position, -} - /// Configuration for a journal-backed Merkle structure. #[derive(Clone)] pub struct Config { @@ -143,20 +127,20 @@ pub struct SyncConfig { /// A Merkle structure backed by a fixed-item-length journal. pub struct Merkle { - /// Lock-protected mutable state. - pub(crate) inner: RwLock>, + /// A memory resident Merkle structure used to build the structure and cache updates. + pub(crate) mem: Mem, + + /// The highest position for which this structure has been pruned, or 0 if it has never been + /// pruned. + pub(crate) pruned_to_pos: Position, /// Stores all unpruned nodes. pub(crate) journal: Journal, /// Stores the pinned nodes for the current pruning boundary, and the corresponding pruning - /// boundary used to generate them. The metadata remains empty until pruning is invoked, and its - /// contents change only when the pruning boundary moves. + /// boundary used to generate them. pub(crate) metadata: Metadata>, - /// Serializes concurrent sync calls. - pub(crate) sync_lock: AsyncMutex<()>, - /// The strategy to use for parallelization. pub(crate) strategy: S, } @@ -171,12 +155,12 @@ impl Merkle Position { - self.inner.read().mem.size() + self.mem.size() } /// Return the total number of leaves in the structure. pub fn leaves(&self) -> Location { - self.inner.read().mem.leaves() + self.mem.leaves() } /// Attempt to get a node from the metadata, with fallback to journal lookup if it fails. @@ -205,7 +189,7 @@ impl Merkle Ok(node), Err(JError::ItemPruned(_)) => { @@ -219,8 +203,7 @@ impl Merkle std::ops::Range> { - let inner = self.inner.read(); - Location::try_from(inner.pruned_to_pos).expect("valid pruned_to_pos")..inner.mem.leaves() + Location::try_from(self.pruned_to_pos).expect("valid pruned_to_pos")..self.mem.leaves() } /// Adds the pinned nodes based on `prune_pos` to `mem`. @@ -264,7 +247,7 @@ impl Merkle = Journal::init(context.child("merkle_journal_peek"), journal_cfg).await?; - let journal_size = Position::::new(journal.size().await); + let journal_size = Position::::new(journal.size()); if journal_size == 0 { let mem = Mem::init(MemConfig { @@ -337,8 +320,9 @@ impl Merkle::init(context.child("merkle_journal"), journal_cfg).await?; - let mut journal_size = Position::::new(journal.size().await); + let mut journal = + Journal::::init(context.child("merkle_journal"), journal_cfg).await?; + let mut journal_size = Position::::new(journal.size()); let metadata_cfg = MConfig { partition: cfg.metadata_partition, @@ -355,13 +339,10 @@ impl Merkle Merkle journal_bounds_start { // Metadata is ahead of journal (crashed before completing journal prune). // Prune the journal to match metadata. journal.prune(*metadata_prune_pos).await?; - if journal.reader().await.bounds().start != journal_bounds_start { + if journal.bounds().start != journal_bounds_start { // This should only happen in the event of some failure during the last attempt to // prune the journal. warn!( @@ -430,7 +411,7 @@ impl Merkle Merkle Merkle Merkle = + let mut journal: Journal = Journal::init(context.child("merkle_journal"), journal_cfg).await?; - let mut journal_size = Position::::new(journal.size().await); + let mut journal_size = Position::::new(journal.size()); // If a crash left the journal at an invalid size (e.g., a leaf was written // but its parent nodes were not), rewind to the last valid size. @@ -545,7 +523,7 @@ impl Merkle Merkle Merkle, ) -> Result, D>, Error> { - assert!(prune_to_pos >= self.inner.get_mut().pruned_to_pos); + assert!(prune_to_pos >= self.pruned_to_pos); let prune_loc = Location::try_from(prune_to_pos).expect("valid prune_to_pos"); let mut pinned_nodes = BTreeMap::new(); @@ -652,13 +627,12 @@ impl Merkle) -> Result, Error> { { - let inner = self.inner.read(); - if let Some(node) = inner.mem.get_node(position) { + if let Some(node) = self.mem.get_node(position) { return Ok(Some(node)); } } - match self.journal.reader().await.read(*position).await { + match self.journal.read(*position).await { Ok(item) => Ok(Some(item)), Err(JError::ItemPruned(_)) => Ok(None), Err(e) => Err(Error::Journal(e)), @@ -666,17 +640,14 @@ impl Merkle Result<(), Error> { - let _sync_guard = self.sync_lock.lock().await; - - let journal_size = Position::::new(self.journal.size().await); + pub async fn sync(&mut self) -> Result<(), Error> { + let journal_size = Position::::new(self.journal.size()); // Snapshot nodes in the mem that are missing from the journal, along with the pinned // node set for the current pruning boundary. let (sync_target_leaves, missing_nodes, pinned_nodes) = { - let inner = self.inner.read(); - let size = inner.mem.size(); - let sync_target_leaves = inner.mem.leaves(); + let size = self.mem.size(); + let sync_target_leaves = self.mem.leaves(); assert!( journal_size <= size, @@ -688,16 +659,16 @@ impl Merkle Merkle Merkle) -> Result<(), Error> { let pos = Position::try_from(loc)?; { - let inner = self.inner.get_mut(); - if loc > inner.mem.leaves() { + if loc > self.mem.leaves() { return Err(Error::LeafOutOfBounds(loc)); } - if pos <= inner.pruned_to_pos { + if pos <= self.pruned_to_pos { return Ok(()); } } @@ -752,9 +718,9 @@ impl Merkle Merkle, inactive_peaks: usize, ) -> Result> { - self.inner.read().mem.root(hasher, inactive_peaks) + self.mem.root(hasher, inactive_peaks) } /// Prune as many nodes as possible, leaving behind at most items_per_blob nodes in the current /// blob. pub async fn prune_all(&mut self) -> Result<(), Error> { - let leaves = self.inner.get_mut().mem.leaves(); + let leaves = self.mem.leaves(); if leaves != 0 { self.prune(leaves).await?; } @@ -794,14 +760,13 @@ impl Merkle::new(self.journal.size().await); + let journal_size = Position::::new(self.journal.size()); // Write the nodes cached in the memory-resident structure to the journal, aborting after // write_count nodes have been written. let mut written_count = 0usize; - for i in *journal_size..*inner.mem.size() { - let node = *inner.mem.get_node_unchecked(Position::new(i)); + for i in *journal_size..*self.mem.size() { + let node = *self.mem.get_node_unchecked(Position::new(i)); self.journal.append(&node).await?; written_count += 1; if written_count >= write_limit { @@ -816,14 +781,14 @@ impl Merkle BTreeMap, D> { - self.inner.read().mem.pinned_nodes() + self.mem.pinned_nodes() } #[cfg(test)] /// Simulate a crash after pruning metadata is written but before the journal is pruned. pub async fn simulate_pruning_failure(mut self, prune_to: Location) -> Result<(), Error> { let prune_to_pos = Position::try_from(prune_to)?; - assert!(prune_to_pos <= self.inner.get_mut().mem.size()); + assert!(prune_to_pos <= self.mem.size()); // Flush items cached in the mem to disk to ensure the current state is recoverable. self.sync().await?; @@ -843,7 +808,7 @@ impl Merkle) -> Result<(), Error> { - self.inner.get_mut().mem.apply_batch(batch)?; + self.mem.apply_batch(batch)?; Ok(()) } @@ -852,22 +817,19 @@ impl Merkle Arc> { - let inner = self.inner.read(); - batch::MerkleizedBatch::from_mem_with_strategy(&inner.mem, self.strategy.clone()) + batch::MerkleizedBatch::from_mem_with_strategy(&self.mem, self.strategy.clone()) } /// Borrow the committed Mem through the read lock. Holds the lock for /// the duration of the closure. pub fn with_mem(&self, f: impl FnOnce(&Mem) -> R) -> R { - let inner = self.inner.read(); - f(&inner.mem) + f(&self.mem) } /// Create a new speculative batch with this structure as its parent. pub fn new_batch(&self) -> UnmerkleizedBatch { - let inner = self.inner.read(); UnmerkleizedBatch { - inner: inner.mem.new_batch_with_strategy(self.strategy.clone()), + inner: self.mem.new_batch_with_strategy(self.strategy.clone()), } } @@ -892,7 +854,7 @@ impl Merkle dest, None => { - let pruned_to_pos = self.inner.get_mut().pruned_to_pos; + let pruned_to_pos = self.pruned_to_pos; return Err(if pruned_to_pos == 0 { Error::Empty } else { @@ -904,13 +866,13 @@ impl Merkle::new(self.journal.size().await); + let journal_size = Position::::new(self.journal.size()); if new_size < journal_size { self.journal.rewind(*new_size).await?; self.journal.sync().await?; @@ -919,10 +881,10 @@ impl Merkle= Position::try_from(inner.mem.bounds().start).expect("valid mem bounds start") + + if new_size >= Position::try_from(self.mem.bounds().start).expect("valid mem bounds start") { - inner.mem.truncate(new_size); + self.mem.truncate(new_size); } else { let mut pinned_nodes = Vec::new(); for pos in F::nodes_to_pin(destination_loc) { @@ -930,16 +892,16 @@ impl Merkle Readable } fn get_node(&self, pos: Position) -> Option { - self.inner.read().mem.get_node(pos) + self.mem.get_node(pos) } fn pruning_boundary(&self) -> Location { - self.inner.read().mem.pruning_boundary() + self.mem.pruning_boundary() } } @@ -1419,7 +1381,7 @@ mod tests { // Simulate a crash that wrote a leaf but not its parent nodes by appending one // extra digest to the journal. This creates an invalid structure size. { - let journal: Journal<_, Digest> = Journal::init( + let mut journal: Journal<_, Digest> = Journal::init( context.child("corrupt"), JConfig { partition: "journal-partition".into(), @@ -1430,10 +1392,10 @@ mod tests { ) .await .unwrap(); - assert_eq!(journal.size().await, expected_size); + assert_eq!(journal.size(), expected_size); journal.append(&Sha256::hash(b"orphan")).await.unwrap(); journal.sync().await.unwrap(); - assert_eq!(journal.size().await, expected_size + 1); + assert_eq!(journal.size(), expected_size + 1); } let mmr = @@ -2955,7 +2917,7 @@ mod tests { // leaf (for the 4th element) but not its parent nodes. This makes the // journal size invalid. { - let journal: Journal<_, Digest> = Journal::init( + let mut journal: Journal<_, Digest> = Journal::init( context.child("corrupt"), JConfig { partition: "journal-partition".into(), @@ -2966,10 +2928,10 @@ mod tests { ) .await .unwrap(); - assert_eq!(journal.size().await, valid_size); + assert_eq!(journal.size(), valid_size); journal.append(&Sha256::hash(b"orphan")).await.unwrap(); journal.sync().await.unwrap(); - assert_eq!(journal.size().await, valid_size + 1); + assert_eq!(journal.size(), valid_size + 1); } // init_sync should recover by rewinding to the last valid size. diff --git a/storage/src/metadata/storage.rs b/storage/src/metadata/storage.rs index ddcece8c5f1..9cae50cd79d 100644 --- a/storage/src/metadata/storage.rs +++ b/storage/src/metadata/storage.rs @@ -6,7 +6,7 @@ use commonware_runtime::{ telemetry::metrics::{Counter, Gauge, GaugeExt, MetricsExt as _}, Blob, BufMut, Error as RError, }; -use commonware_utils::{sync::AsyncMutex, Span}; +use commonware_utils::Span; use futures::future::try_join_all; use std::collections::{BTreeMap, BTreeSet, HashMap}; use tracing::{debug, warn}; @@ -74,7 +74,7 @@ pub struct Metadata { map: BTreeMap, partition: String, - state: AsyncMutex>, + state: State, sync_overwrites: Counter, sync_rewrites: Counter, @@ -121,12 +121,12 @@ impl Metadata { map, partition: cfg.partition, - state: AsyncMutex::new(State { + state: State { cursor, next_version, key_order_changed: next_version, // rewrite on startup because we don't have a diff record blobs: [left_wrapper, right_wrapper], - }), + }, sync_rewrites, sync_overwrites, @@ -227,9 +227,9 @@ impl Metadata { // Mark key as modified. // // We need to mark both blobs as modified because we may need to update both files. - let state = self.state.get_mut(); - state.blobs[state.cursor].modified.insert(key.clone()); - state.blobs[1 - state.cursor].modified.insert(key.clone()); + let cursor = self.state.cursor; + self.state.blobs[cursor].modified.insert(key.clone()); + self.state.blobs[1 - cursor].modified.insert(key.clone()); Some(value) } @@ -241,8 +241,7 @@ impl Metadata { self.map.clear(); // Mark key order as changed - let state = self.state.get_mut(); - state.key_order_changed = state.next_version; + self.state.key_order_changed = self.state.next_version; self.keys.set(0); } @@ -258,12 +257,12 @@ impl Metadata { // Mark key as modified. // // We need to mark both blobs as modified because we may need to update both files. - let state = self.state.get_mut(); if previous.is_some() { - state.blobs[state.cursor].modified.insert(key.clone()); - state.blobs[1 - state.cursor].modified.insert(key); + let cursor = self.state.cursor; + self.state.blobs[cursor].modified.insert(key.clone()); + self.state.blobs[1 - cursor].modified.insert(key); } else { - state.key_order_changed = state.next_version; + self.state.key_order_changed = self.state.next_version; } let _ = self.keys.try_set(self.map.len()); previous @@ -307,8 +306,7 @@ impl Metadata { // Mark key as modified. if past.is_some() { - let state = self.state.get_mut(); - state.key_order_changed = state.next_version; + self.state.key_order_changed = self.state.next_version; } let _ = self.keys.try_set(self.map.len()); @@ -329,40 +327,35 @@ impl Metadata { // If the number of keys has changed, mark the key order as changed if new_len != old_len { - let state = self.state.get_mut(); - state.key_order_changed = state.next_version; + self.state.key_order_changed = self.state.next_version; let _ = self.keys.try_set(self.map.len()); } } /// Atomically commit the current state of [Metadata]. - pub async fn sync(&self) -> Result<(), Error> { - // Acquire lock on sync state which will prevent concurrent sync calls while not blocking - // reads from the metadata map. - let mut state = self.state.lock().await; - + pub async fn sync(&mut self) -> Result<(), Error> { // Extract values we need - let cursor = state.cursor; - let next_version = state.next_version; - let key_order_changed = state.key_order_changed; + let cursor = self.state.cursor; + let next_version = self.state.next_version; + let key_order_changed = self.state.key_order_changed; // Compute next version. // // While it is possible that extremely high-frequency updates to metadata could cause an // eventual overflow of version, syncing once per millisecond would overflow in 584,942,417 // years. - let past_version = state.blobs[cursor].version; + let past_version = self.state.blobs[cursor].version; let next_next_version = next_version.checked_add(1).expect("version overflow"); // Get target blob (the one we will modify) let target_cursor = 1 - cursor; // Update the state. - state.cursor = target_cursor; - state.next_version = next_next_version; + self.state.cursor = target_cursor; + self.state.next_version = next_next_version; // Get a mutable reference to the target blob. - let target = &mut state.blobs[target_cursor]; + let target = &mut self.state.blobs[target_cursor]; // Determine if we can overwrite existing data in place, and prepare the list of data to // write in that event. @@ -452,8 +445,7 @@ impl Metadata { /// Remove the underlying blobs for this [Metadata]. pub async fn destroy(self) -> Result<(), Error> { - let state = self.state.into_inner(); - for (i, wrapper) in state.blobs.into_iter().enumerate() { + for (i, wrapper) in self.state.blobs.into_iter().enumerate() { drop(wrapper.blob); self.context .remove(&self.partition, Some(BLOB_NAMES[i])) diff --git a/storage/src/ordinal/mod.rs b/storage/src/ordinal/mod.rs index fb09ad7d427..b4b2a3446d3 100644 --- a/storage/src/ordinal/mod.rs +++ b/storage/src/ordinal/mod.rs @@ -235,10 +235,9 @@ mod tests { .await .expect("Failed to remove blob"); - // Both concurrent sync calls must observe the in-flight durability failure. - let (first, second) = futures::future::join(store.sync(), store.sync()).await; - assert!(first.is_err(), "first sync unexpectedly succeeded"); - assert!(second.is_err(), "second sync unexpectedly succeeded"); + // Sync must observe the durability failure. + let result = store.sync().await; + assert!(result.is_err(), "sync unexpectedly succeeded"); }); } diff --git a/storage/src/ordinal/storage.rs b/storage/src/ordinal/storage.rs index a71ec7c829c..a5f102c7d43 100644 --- a/storage/src/ordinal/storage.rs +++ b/storage/src/ordinal/storage.rs @@ -10,7 +10,7 @@ use commonware_runtime::{ telemetry::metrics::{Counter, MetricsExt as _}, Blob, Buf, BufMut, BufferPooler, Error as RError, }; -use commonware_utils::{bitmap::BitMap, sync::AsyncMutex}; +use commonware_utils::bitmap::BitMap; use futures::future::try_join_all; use std::{ collections::{btree_map::Entry, BTreeMap, BTreeSet}, @@ -81,10 +81,8 @@ pub struct Ordinal> { // RMap for interval tracking intervals: RMap, - // Pending sections to be synced. The async mutex serializes - // concurrent sync calls so a second sync cannot return before - // the first has finished flushing. - pending: AsyncMutex>, + // Pending sections to be synced. + pending: BTreeSet, // Metrics puts: Counter, @@ -244,7 +242,7 @@ impl> Ordinal { config, blobs, intervals, - pending: AsyncMutex::new(BTreeSet::new()), + pending: BTreeSet::new(), puts, gets, has, @@ -280,7 +278,7 @@ impl> Ordinal { let offset = (index % items_per_blob) * Record::::SIZE as u64; let record = Record::new(value); blob.write_at(offset, record.encode_mut()).await?; - self.pending.lock().await.insert(section); + self.pending.insert(section); // Add to intervals self.intervals.insert(index); @@ -388,33 +386,26 @@ impl> Ordinal { } // Clean pending entries that fall into pruned sections. - self.pending - .lock() - .await - .retain(|§ion| section >= min_section); + self.pending.retain(|§ion| section >= min_section); Ok(()) } /// Write all pending entries and sync all modified [Blob]s. - pub async fn sync(&self) -> Result<(), Error> { + pub async fn sync(&mut self) -> Result<(), Error> { self.syncs.inc(); - // Hold the lock across the entire flush so a concurrent sync - // cannot return before durability is established. - let mut pending = self.pending.lock().await; - if pending.is_empty() { + if self.pending.is_empty() { return Ok(()); } - let mut futures = Vec::with_capacity(pending.len()); - for section in pending.iter() { + let mut futures = Vec::with_capacity(self.pending.len()); + for section in self.pending.iter() { futures.push(self.blobs.get(section).unwrap().sync()); } try_join_all(futures).await?; - // Clear pending sections. - pending.clear(); + self.pending.clear(); Ok(()) } @@ -442,12 +433,12 @@ impl> Ordinal { impl Persistable for Ordinal { type Error = Error; - async fn commit(&self) -> Result<(), Self::Error> { - self.sync().await + async fn commit(&mut self) -> Result<(), Self::Error> { + Self::sync(self).await } - async fn sync(&self) -> Result<(), Self::Error> { - self.sync().await + async fn sync(&mut self) -> Result<(), Self::Error> { + Self::sync(self).await } async fn destroy(self) -> Result<(), Self::Error> { diff --git a/storage/src/qmdb/any/db.rs b/storage/src/qmdb/any/db.rs index 40af1161492..8f4902a0aaa 100644 --- a/storage/src/qmdb/any/db.rs +++ b/storage/src/qmdb/any/db.rs @@ -662,13 +662,13 @@ where } /// Sync all database state to disk. - pub async fn sync(&self) -> Result<(), crate::qmdb::Error> { + pub async fn sync(&mut self) -> Result<(), crate::qmdb::Error> { self.log.sync().await.map_err(Into::into) } /// Durably commit the journal state published by prior [`Db::apply_batch`] /// calls. - pub async fn commit(&self) -> Result<(), crate::qmdb::Error> { + pub async fn commit(&mut self) -> Result<(), crate::qmdb::Error> { self.log.commit().await.map_err(Into::into) } @@ -691,11 +691,11 @@ where { type Error = crate::qmdb::Error; - async fn commit(&self) -> Result<(), crate::qmdb::Error> { + async fn commit(&mut self) -> Result<(), crate::qmdb::Error> { Self::commit(self).await } - async fn sync(&self) -> Result<(), crate::qmdb::Error> { + async fn sync(&mut self) -> Result<(), crate::qmdb::Error> { Self::sync(self).await } diff --git a/storage/src/qmdb/any/mod.rs b/storage/src/qmdb/any/mod.rs index d8be8976072..70f2fb0d831 100644 --- a/storage/src/qmdb/any/mod.rs +++ b/storage/src/qmdb/any/mod.rs @@ -2535,16 +2535,12 @@ pub(crate) mod test { db.apply_batch(merkleized).await.unwrap(); } - let (child_merkleized, commit_result) = futures::join!( - async { - assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(0))); - let mut child = db.new_batch(); - child = child.write(key(1), Some(val(1))); - child.merkleize(&db, None).await.unwrap() - }, - db.commit(), - ); - commit_result.unwrap(); + db.commit().await.unwrap(); + + assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(0))); + let mut child = db.new_batch(); + child = child.write(key(1), Some(val(1))); + let child_merkleized = child.merkleize(&db, None).await.unwrap(); db.apply_batch(child_merkleized).await.unwrap(); db.commit().await.unwrap(); @@ -2589,7 +2585,7 @@ mod bitmap_tests { } /// Commit, drop, reopen, and assert the rebuilt bitmap matches the in-memory bitmap. - async fn assert_oracle_round_trip(db: AnyTest, context: Context, label: &str) -> AnyTest { + async fn assert_oracle_round_trip(mut db: AnyTest, context: Context, label: &str) -> AnyTest { let pre_active = bitmap_active_locs(&db); let pre_len = db.bitmap.len(); let pre_pruned = db.bitmap.pruned_bits(); diff --git a/storage/src/qmdb/any/sync/mod.rs b/storage/src/qmdb/any/sync/mod.rs index e4d65617a6b..9815cba705a 100644 --- a/storage/src/qmdb/any/sync/mod.rs +++ b/storage/src/qmdb/any/sync/mod.rs @@ -194,7 +194,7 @@ macro_rules! impl_sync_database { else { return false; }; - if Location::new(journal.reader().await.bounds().start) > target.range.start() { + if Location::new(journal.reader().bounds().start) > target.range.start() { return false; } diff --git a/storage/src/qmdb/current/db.rs b/storage/src/qmdb/current/db.rs index a56c7b4fd49..a68b3def11a 100644 --- a/storage/src/qmdb/current/db.rs +++ b/storage/src/qmdb/current/db.rs @@ -35,7 +35,6 @@ use commonware_parallel::{Sequential, Strategy}; use commonware_utils::{ bitmap::{self, Readable as _}, sequence::prefixed_u64::U64, - sync::AsyncMutex, }; use core::{num::NonZeroU64, ops::Range}; use futures::future::try_join_all; @@ -74,7 +73,7 @@ pub struct Db< /// Persists: /// - The number of pruned bitmap chunks at key [PRUNED_CHUNKS_PREFIX] /// - The grafted tree pinned nodes at key [NODE_PREFIX] - pub(super) metadata: AsyncMutex>>, + pub(super) metadata: Metadata>, /// Strategy used to parallelize batch operations across the ops tree, the grafted tree, /// and grafted leaf computation. @@ -596,8 +595,8 @@ where } /// Sync the metadata to disk. - pub(crate) async fn sync_metadata(&self) -> Result<(), Error> { - let mut metadata = self.metadata.lock().await; + pub(crate) async fn sync_metadata(&mut self) -> Result<(), Error> { + let metadata = &mut self.metadata; metadata.clear(); // Snapshot the pruning boundary under the read lock; the guard drops before any await. @@ -638,12 +637,12 @@ where { /// Durably commit the journal state published by prior [`Db::apply_batch`] /// calls. - pub async fn commit(&self) -> Result<(), Error> { + pub async fn commit(&mut self) -> Result<(), Error> { self.any.commit().await } /// Sync all database state to disk. - pub async fn sync(&self) -> Result<(), Error> { + pub async fn sync(&mut self) -> Result<(), Error> { self.any.sync().await?; // Write the bitmap pruning boundary to disk so that next startup doesn't have to @@ -653,7 +652,7 @@ where /// Destroy the db, removing all data from disk. pub async fn destroy(self) -> Result<(), Error> { - self.metadata.into_inner().destroy().await?; + self.metadata.destroy().await?; self.any.destroy().await } } @@ -702,11 +701,11 @@ where { type Error = Error; - async fn commit(&self) -> Result<(), Error> { + async fn commit(&mut self) -> Result<(), Error> { Self::commit(self).await } - async fn sync(&self) -> Result<(), Error> { + async fn sync(&mut self) -> Result<(), Error> { Self::sync(self).await } diff --git a/storage/src/qmdb/current/mod.rs b/storage/src/qmdb/current/mod.rs index 52e509bf074..2ee96540a95 100644 --- a/storage/src/qmdb/current/mod.rs +++ b/storage/src/qmdb/current/mod.rs @@ -268,7 +268,7 @@ use crate::{ use commonware_codec::{CodecShared, FixedSize}; use commonware_cryptography::Hasher; use commonware_parallel::{Sequential, Strategy}; -use commonware_utils::{bitmap::Prunable as BitMap, sync::AsyncMutex}; +use commonware_utils::bitmap::Prunable as BitMap; use std::sync::Arc; pub mod batch; @@ -391,7 +391,7 @@ where Ok(db::Db { any, grafted_tree, - metadata: AsyncMutex::new(metadata), + metadata, strategy, root, }) @@ -2886,16 +2886,12 @@ pub mod tests { let parent_merkleized = batch.merkleize(&db, None).await.unwrap(); db.apply_batch(parent_merkleized).await.unwrap(); - let (child_merkleized, commit_result) = futures::join!( - async { - assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(0))); - let mut child = db.new_batch(); - child = child.write(key(1), Some(val(1))); - child.merkleize(&db, None).await.unwrap() - }, - db.commit(), - ); - commit_result.unwrap(); + db.commit().await.unwrap(); + + assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(0))); + let mut child = db.new_batch(); + child = child.write(key(1), Some(val(1))); + let child_merkleized = child.merkleize(&db, None).await.unwrap(); db.apply_batch(child_merkleized).await.unwrap(); db.commit().await.unwrap(); diff --git a/storage/src/qmdb/current/sync/mod.rs b/storage/src/qmdb/current/sync/mod.rs index 7098513f1c2..3418cf04b53 100644 --- a/storage/src/qmdb/current/sync/mod.rs +++ b/storage/src/qmdb/current/sync/mod.rs @@ -71,9 +71,7 @@ use crate::{ use commonware_codec::{Codec, CodecShared, Read as CodecRead}; use commonware_cryptography::{DigestOf, Hasher}; use commonware_parallel::Strategy; -use commonware_utils::{ - bitmap::Prunable as BitMap, channel::oneshot, range::NonEmptyRange, sync::AsyncMutex, Array, -}; +use commonware_utils::{bitmap::Prunable as BitMap, channel::oneshot, range::NonEmptyRange, Array}; use std::sync::Arc; #[cfg(test)] @@ -221,10 +219,10 @@ where db::init_metadata::>(context.child("metadata"), &metadata_partition) .await?; - let current_db = db::Db { + let mut current_db = db::Db { any, grafted_tree, - metadata: AsyncMutex::new(metadata), + metadata, strategy, root, }; @@ -300,7 +298,7 @@ macro_rules! impl_current_sync_database { else { return false; }; - let reader = journal.reader().await; + let reader = journal.reader(); let bounds = reader.bounds(); if Location::new(bounds.start) > target.range.start() { return false; diff --git a/storage/src/qmdb/immutable/mod.rs b/storage/src/qmdb/immutable/mod.rs index cd32f83421d..68ec335aa8b 100644 --- a/storage/src/qmdb/immutable/mod.rs +++ b/storage/src/qmdb/immutable/mod.rs @@ -583,12 +583,12 @@ where /// Sync all database state to disk. While this isn't necessary to ensure durability of /// committed operations, periodic invocation may reduce memory usage and the time required to /// recover the database on restart. - pub async fn sync(&self) -> Result<(), Error> { + pub async fn sync(&mut self) -> Result<(), Error> { Ok(self.journal.sync().await?) } /// Durably commit the journal state published by prior [`Immutable::apply_batch`] calls. - pub async fn commit(&self) -> Result<(), Error> { + pub async fn commit(&mut self) -> Result<(), Error> { Ok(self.journal.commit().await?) } diff --git a/storage/src/qmdb/immutable/sync/mod.rs b/storage/src/qmdb/immutable/sync/mod.rs index 59e3aed9055..499bf226555 100644 --- a/storage/src/qmdb/immutable/sync/mod.rs +++ b/storage/src/qmdb/immutable/sync/mod.rs @@ -130,7 +130,7 @@ where ); let root = journal.root(inactive_peaks)?; - let db = Self { + let mut db = Self { journal, root, snapshot, diff --git a/storage/src/qmdb/immutable/sync/tests.rs b/storage/src/qmdb/immutable/sync/tests.rs index 1c91eaf541d..25f2e6c5fde 100644 --- a/storage/src/qmdb/immutable/sync/tests.rs +++ b/storage/src/qmdb/immutable/sync/tests.rs @@ -69,7 +69,7 @@ pub(crate) trait SyncTestHarness: Sized + 'static { config: ConfigOf, ) -> impl Future + Send; fn destroy(db: Self::Db) -> impl Future + Send; - fn db_sync(db: &Self::Db) -> impl Future + Send; + fn db_sync(db: &mut Self::Db) -> impl Future + Send; fn apply_ops( db: Self::Db, @@ -252,7 +252,7 @@ where reached_target_tx: None, max_retained_roots: 8, }; - let synced_db: DbOf = sync::sync(config).await.unwrap(); + let mut synced_db: DbOf = sync::sync(config).await.unwrap(); assert_eq!(H::db_root(&synced_db), target_root); let expected_root = H::db_root(&synced_db); @@ -260,7 +260,7 @@ where let expected_op_count = bounds.end; let expected_oldest_retained_loc = bounds.start; - H::db_sync(&synced_db).await; + H::db_sync(&mut synced_db).await; drop(synced_db); let reopened_db = H::init_db_with_config(context.child("reopened"), db_config).await; @@ -989,7 +989,7 @@ pub(crate) mod harnesses { db.destroy().await.unwrap(); } - async fn db_sync(db: &Self::Db) { + async fn db_sync(db: &mut Self::Db) { db.sync().await.unwrap(); } diff --git a/storage/src/qmdb/keyless/mod.rs b/storage/src/qmdb/keyless/mod.rs index d23178556c4..cdc718cbe6a 100644 --- a/storage/src/qmdb/keyless/mod.rs +++ b/storage/src/qmdb/keyless/mod.rs @@ -398,12 +398,12 @@ where /// Sync all database state to disk. While this isn't necessary to ensure durability of /// committed operations, periodic invocation may reduce memory usage and the time required to /// recover the database on restart. - pub async fn sync(&self) -> Result<(), Error> { + pub async fn sync(&mut self) -> Result<(), Error> { self.journal.sync().await.map_err(Into::into) } /// Durably commit the journal state published by prior [`Keyless::apply_batch`] calls. - pub async fn commit(&self) -> Result<(), Error> { + pub async fn commit(&mut self) -> Result<(), Error> { self.journal.commit().await.map_err(Into::into) } diff --git a/storage/src/qmdb/keyless/sync/mod.rs b/storage/src/qmdb/keyless/sync/mod.rs index 75485ef812c..2ba25664d79 100644 --- a/storage/src/qmdb/keyless/sync/mod.rs +++ b/storage/src/qmdb/keyless/sync/mod.rs @@ -108,7 +108,7 @@ where ); let root = journal.root(inactive_peaks)?; - let db = Self { + let mut db = Self { journal, root, last_commit_loc, diff --git a/storage/src/qmdb/keyless/sync/tests.rs b/storage/src/qmdb/keyless/sync/tests.rs index e7c253769be..ca8b98adbd1 100644 --- a/storage/src/qmdb/keyless/sync/tests.rs +++ b/storage/src/qmdb/keyless/sync/tests.rs @@ -64,7 +64,7 @@ pub(crate) trait SyncTestHarness: Sized + 'static { config: ConfigOf, ) -> impl Future + Send; fn destroy(db: Self::Db) -> impl Future + Send; - fn db_sync(db: &Self::Db) -> impl Future + Send; + fn db_sync(db: &mut Self::Db) -> impl Future + Send; fn apply_ops( db: Self::Db, @@ -264,7 +264,7 @@ where reached_target_tx: None, max_retained_roots: 8, }; - let synced_db: DbOf = sync::sync(config).await.unwrap(); + let mut synced_db: DbOf = sync::sync(config).await.unwrap(); assert_eq!(H::db_root(&synced_db), target_root); let expected_root = H::db_root(&synced_db); @@ -272,7 +272,7 @@ where let expected_op_count = bounds.end; let expected_oldest_retained_loc = bounds.start; - H::db_sync(&synced_db).await; + H::db_sync(&mut synced_db).await; drop(synced_db); let reopened_db = H::init_db_with_config(context.child("reopened"), db_config).await; @@ -894,7 +894,7 @@ pub(crate) mod harnesses { db.destroy().await.unwrap(); } - async fn db_sync(db: &Self::Db) { + async fn db_sync(db: &mut Self::Db) { db.sync().await.unwrap(); } diff --git a/storage/src/qmdb/store/db.rs b/storage/src/qmdb/store/db.rs index a2176c5c105..0bde31aece1 100644 --- a/storage/src/qmdb/store/db.rs +++ b/storage/src/qmdb/store/db.rs @@ -80,7 +80,7 @@ use crate::{ index::{unordered::Index, Unordered as _}, journal::contiguous::{ variable::{Config as JournalConfig, Journal}, - Mutable as _, Reader, + Mutable as _, }, merkle::mmr::Location, qmdb::{ @@ -271,7 +271,7 @@ where /// if the location precedes the oldest retained location. The location is otherwise assumed /// valid. async fn get_op(&self, loc: Location) -> Result, Error> { - let reader = self.log.reader().await; + let reader = self.log.reader(); assert!(*loc < reader.bounds().end); reader.read(*loc).await.map_err(|e| match e { crate::journal::Error::ItemPruned(_) => Error::OperationPruned(loc), @@ -281,14 +281,14 @@ where /// Return [start, end) where `start` and `end - 1` are the Locations of the oldest and newest /// retained operations respectively. - pub async fn bounds(&self) -> std::ops::Range { - let bounds = self.log.reader().await.bounds(); + pub const fn bounds(&self) -> std::ops::Range { + let bounds = self.log.reader().bounds(); Location::new(bounds.start)..Location::new(bounds.end) } /// Return the Location of the next operation appended to this db. - pub async fn size(&self) -> Location { - Location::new(self.log.size().await) + pub const fn size(&self) -> Location { + Location::new(self.log.size()) } /// Return the inactivity floor location. This is the location before which all operations are @@ -300,7 +300,7 @@ where /// Get the metadata associated with the last commit. pub async fn get_metadata(&self) -> Result, Error> { let Operation::CommitFloor(metadata, _) = - self.log.reader().await.read(*self.last_commit_loc).await? + self.log.reader().read(*self.last_commit_loc).await? else { unreachable!("last commit should be a commit floor operation"); }; @@ -310,7 +310,7 @@ where /// Prune historical operations prior to `prune_loc`. This does not affect the db's root /// or current snapshot. - pub async fn prune(&self, prune_loc: Location) -> Result<(), Error> { + pub async fn prune(&mut self, prune_loc: Location) -> Result<(), Error> { if prune_loc > self.inactivity_floor_loc { return Err(Error::PruneBeyondMinRequired( prune_loc, @@ -324,7 +324,7 @@ where return Ok(()); } - let bounds = self.log.reader().await.bounds(); + let bounds = self.log.reader().bounds(); let log_size = Location::new(bounds.end); let oldest_retained_loc = Location::new(bounds.start); debug!( @@ -357,17 +357,13 @@ where // startup. log.sync().await?; - let last_commit_loc = Location::new( - log.size() - .await - .checked_sub(1) - .expect("commit should exist"), - ); + let last_commit_loc = + Location::new(log.size().checked_sub(1).expect("commit should exist")); // Build the snapshot. let mut snapshot = Index::new(context.child("snapshot"), cfg.translator); let (inactivity_floor_loc, active_keys) = { - let reader = log.reader().await; + let reader = log.reader(); let op = reader.read(*last_commit_loc).await?; let inactivity_floor_loc = op.has_floor().expect("last op should be a commit"); if inactivity_floor_loc > last_commit_loc { @@ -394,7 +390,7 @@ where /// Sync all database state to disk. While this isn't necessary to ensure durability of /// committed operations, periodic invocation may reduce memory usage and the time required to /// recover the database on restart. - pub async fn sync(&self) -> Result<(), Error> { + pub async fn sync(&mut self) -> Result<(), Error> { self.log.sync().await.map_err(Into::into) } @@ -430,7 +426,7 @@ where for (key, value) in diff { if let Some(value) = value { let updated = { - let reader = self.log.reader().await; + let reader = self.log.reader(); let new_loc = reader.bounds().end; update_key::( &mut self.snapshot, @@ -450,7 +446,7 @@ where .await?; } else { let deleted = { - let reader = self.log.reader().await; + let reader = self.log.reader(); delete_key::(&mut self.snapshot, &reader, &key) .await? }; @@ -465,7 +461,7 @@ where // Raise the inactivity floor by `self.steps` steps, plus 1 to account for the previous // commit becoming inactive. if self.is_empty() { - self.inactivity_floor_loc = self.size().await; + self.inactivity_floor_loc = self.size(); debug!(tip = ?self.inactivity_floor_loc, "db is empty, raising floor to tip"); } else { let steps_to_take = self.steps + 1; @@ -484,12 +480,12 @@ where self.steps = 0; - let end_loc = self.size().await; + let end_loc = self.size(); Ok(start_loc..end_loc) } /// Durably commit the journal state published by prior [`Db::apply_batch`] calls. - pub async fn commit(&self) -> Result<(), Error> { + pub async fn commit(&mut self) -> Result<(), Error> { self.log.commit().await.map_err(Into::into) } } @@ -503,11 +499,11 @@ where { type Error = Error; - async fn commit(&self) -> Result<(), Error> { + async fn commit(&mut self) -> Result<(), Error> { Self::commit(self).await } - async fn sync(&self) -> Result<(), Error> { + async fn sync(&mut self) -> Result<(), Error> { self.sync().await } @@ -563,8 +559,8 @@ mod test { let executor = deterministic::Runner::default(); executor.start(|mut context| async move { let mut db = create_test_store(context.child("store").with_attribute("index", 0)).await; - assert_eq!(db.bounds().await.end, 1); - assert_eq!(db.log.bounds().await.start, 0); + assert_eq!(db.bounds().end, 1); + assert_eq!(db.log.bounds().start, 0); assert_eq!(db.inactivity_floor_loc(), 0); assert!(matches!(db.prune(db.inactivity_floor_loc()).await, Ok(()))); assert!(matches!( @@ -580,7 +576,7 @@ mod test { drop(db); let mut db = create_test_store(context.child("store").with_attribute("index", 1)).await; - assert_eq!(db.bounds().await.end, 1); + assert_eq!(db.bounds().end, 1); // Test calling commit on an empty db which should make it (durably) non-empty. let metadata = vec![1, 2, 3]; @@ -589,7 +585,7 @@ mod test { assert_eq!(range.start, 1); assert_eq!(range.end, 2); db.commit().await.unwrap(); - assert_eq!(db.bounds().await.end, 2); + assert_eq!(db.bounds().end, 2); assert!(matches!(db.prune(db.inactivity_floor_loc()).await, Ok(()))); assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone())); @@ -609,7 +605,7 @@ mod test { db.commit().await.unwrap(); // Distance should equal 3 after the second commit, with inactivity_floor // referencing the previous commit operation. - assert!(db.bounds().await.end - db.inactivity_floor_loc <= 3); + assert!(db.bounds().end - db.inactivity_floor_loc <= 3); assert!(db.get_metadata().await.unwrap().is_none()); } @@ -625,7 +621,7 @@ mod test { let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await; // Ensure the store is empty - assert_eq!(db.bounds().await.end, 1); + assert_eq!(db.bounds().end, 1); assert_eq!(db.inactivity_floor_loc, 0); let key = Digest::random(&mut ctx); @@ -639,7 +635,7 @@ mod test { // CommitFloor: 3 new ops on top of the initial commit. apply_entries(&mut db, [(key, Some(value.clone()))]).await; - assert_eq!(*db.bounds().await.end, 4); + assert_eq!(*db.bounds().end, 4); assert_eq!(*db.inactivity_floor_loc, 2); // Fetch the value @@ -654,7 +650,7 @@ mod test { let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await; // Ensure the re-opened store removed the uncommitted operations - assert_eq!(*db.bounds().await.end, 1); + assert_eq!(*db.bounds().end, 1); assert_eq!(*db.inactivity_floor_loc, 0); assert!(db.get_metadata().await.unwrap().is_none()); @@ -673,14 +669,14 @@ mod test { db.commit().await.unwrap(); assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone())); - assert_eq!(*db.bounds().await.end, 4); + assert_eq!(*db.bounds().end, 4); assert_eq!(*db.inactivity_floor_loc, 2); // Re-open the store let mut db = create_test_store(ctx.child("store").with_attribute("index", 2)).await; // Ensure the re-opened store retained the committed operations - assert_eq!(*db.bounds().await.end, 4); + assert_eq!(*db.bounds().end, 4); assert_eq!(*db.inactivity_floor_loc, 2); // Fetch the value, ensuring it is still present @@ -693,7 +689,7 @@ mod test { apply_entries(&mut db, [(k1, Some(v1.clone()))]).await; apply_entries(&mut db, [(k2, Some(v2.clone()))]).await; - assert_eq!(*db.bounds().await.end, 10); + assert_eq!(*db.bounds().end, 10); assert_eq!(*db.inactivity_floor_loc, 5); // Each apply_entries writes a CommitFloor with None metadata, replacing @@ -704,7 +700,7 @@ mod test { assert_eq!(db.get_metadata().await.unwrap(), None); // commit() is just an fsync now, so bounds and floor are unchanged. - assert_eq!(*db.bounds().await.end, 10); + assert_eq!(*db.bounds().end, 10); assert_eq!(*db.inactivity_floor_loc, 5); // Ensure all keys can be accessed, despite the first section being pruned. @@ -753,7 +749,7 @@ mod test { drop(db); // Re-open the store, prune it, then ensure it replays the log correctly. - let db = create_test_store(ctx.child("store").with_attribute("index", 1)).await; + let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await; db.prune(db.inactivity_floor_loc()).await.unwrap(); let iter = db.snapshot.get(&k); @@ -761,14 +757,14 @@ mod test { // First apply_entries: Update + 1 move + CommitFloor = 3 ops. Subsequent 99: Update + 2 // moves + CommitFloor = 4 ops each. Total: 1 (init) + 3 + 99*4 = 400. - assert_eq!(*db.bounds().await.end, 400); + assert_eq!(*db.bounds().end, 400); // Only the last Update and CommitFloor are active → floor = 398. assert_eq!(*db.inactivity_floor_loc, 398); let floor = db.inactivity_floor_loc; // All blobs prior to the inactivity floor are pruned, so the oldest retained location // is the first in the last retained blob. - assert_eq!(db.log.bounds().await.start, *floor - *floor % 7); + assert_eq!(db.log.bounds().start, *floor - *floor % 7); db.destroy().await.unwrap(); }); @@ -890,7 +886,7 @@ mod test { apply_entries(&mut db, [(k_b, Some(v_b.clone()))]).await; db.commit().await.unwrap(); - assert_eq!(*db.bounds().await.end, 7); + assert_eq!(*db.bounds().end, 7); assert_eq!(*db.inactivity_floor_loc, 3); assert_eq!(db.get(&k_a).await.unwrap().unwrap(), v_a); @@ -898,7 +894,7 @@ mod test { apply_entries(&mut db, [(k_a, Some(v_c.clone()))]).await; db.commit().await.unwrap(); - assert_eq!(*db.bounds().await.end, 15); + assert_eq!(*db.bounds().end, 15); assert_eq!(*db.inactivity_floor_loc, 12); assert_eq!(db.get(&k_a).await.unwrap().unwrap(), v_c); assert_eq!(db.get(&k_b).await.unwrap().unwrap(), v_a); @@ -927,7 +923,7 @@ mod test { } drop(db); let mut db = create_test_store(context.child("store").with_attribute("index", 1)).await; - assert_eq!(*db.bounds().await.end, 1); + assert_eq!(*db.bounds().end, 1); // Apply the updates and commit them. for i in 0u64..ELEMENTS { @@ -958,18 +954,18 @@ mod test { apply_entries(&mut db, [(k, None)]).await; } db.commit().await.unwrap(); - let final_count = db.bounds().await.end; + let final_count = db.bounds().end; let final_floor = db.inactivity_floor_loc; // Sync and reopen the store to ensure the state is preserved. db.sync().await.unwrap(); drop(db); - let db = create_test_store(context.child("store").with_attribute("index", 2)).await; - assert_eq!(db.bounds().await.end, final_count); + let mut db = create_test_store(context.child("store").with_attribute("index", 2)).await; + assert_eq!(db.bounds().end, final_count); assert_eq!(db.inactivity_floor_loc, final_floor); db.prune(db.inactivity_floor_loc()).await.unwrap(); - assert_eq!(db.log.bounds().await.start, *final_floor - *final_floor % 7); + assert_eq!(db.log.bounds().start, *final_floor - *final_floor % 7); assert_eq!(db.snapshot.items(), 857); db.destroy().await.unwrap(); @@ -984,7 +980,7 @@ mod test { let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await; // Ensure the store is empty - assert_eq!(db.bounds().await.end, 1); + assert_eq!(db.bounds().end, 1); assert_eq!(db.inactivity_floor_loc, 0); let key = Digest::random(&mut ctx); @@ -999,7 +995,7 @@ mod test { // Insert a key-value pair let batch = batch.update(key, value.clone()); - assert_eq!(db.bounds().await.end, 1); // The batch is not applied yet + assert_eq!(db.bounds().end, 1); // The batch is not applied yet assert_eq!(db.inactivity_floor_loc, 0); // Fetch the value @@ -1012,7 +1008,7 @@ mod test { let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await; // Ensure the batch was not applied since we didn't commit. - assert_eq!(db.bounds().await.end, 1); + assert_eq!(db.bounds().end, 1); assert_eq!(db.inactivity_floor_loc, 0); assert!(db.get_metadata().await.unwrap().is_none()); @@ -1036,7 +1032,7 @@ mod test { let db = create_test_store(ctx.child("store").with_attribute("index", 2)).await; // Ensure the re-opened store retained the committed operations - assert_eq!(db.bounds().await.end, 4); + assert_eq!(db.bounds().end, 4); assert_eq!(db.inactivity_floor_loc, 2); // Fetch the value, ensuring it is still present @@ -1072,7 +1068,7 @@ mod test { } #[allow(dead_code)] - fn assert_commit_is_send(db: &Db, TwoCap>) { + fn assert_commit_is_send(db: &mut Db, TwoCap>) { is_send(db.commit()); } } diff --git a/storage/src/qmdb/sync/journal.rs b/storage/src/qmdb/sync/journal.rs index 669bd9358df..dadb5174819 100644 --- a/storage/src/qmdb/sync/journal.rs +++ b/storage/src/qmdb/sync/journal.rs @@ -106,7 +106,7 @@ where config: Self::Config, range: NonEmptyRange>, ) -> Result { - let journal = Self::init(context, config).await?; + let mut journal = Self::init(context, config).await?; let size = Contiguous::size(&journal).await; // Fresh journal already aligned with the sync start - nothing to do. diff --git a/storage/src/queue/conformance.rs b/storage/src/queue/conformance.rs index d886d57dd6c..17ea679cee0 100644 --- a/storage/src/queue/conformance.rs +++ b/storage/src/queue/conformance.rs @@ -58,7 +58,7 @@ impl Conformance for QueueConformance { let dequeue_count = items_count / 2; for _ in 0..dequeue_count { let (pos, _) = queue.dequeue().await.unwrap().unwrap(); - queue.ack(pos).await.unwrap(); + queue.ack(pos).unwrap(); } // Sync (commit + prune), then drop @@ -74,7 +74,7 @@ impl Conformance for QueueConformance { .unwrap(); while let Some((pos, item)) = queue.dequeue().await.unwrap() { assert_eq!(item, data[pos as usize]); - queue.ack(pos).await.unwrap(); + queue.ack(pos).unwrap(); } queue.sync().await.unwrap(); drop(queue); diff --git a/storage/src/queue/mod.rs b/storage/src/queue/mod.rs index 9efe480ee3f..15db4c86ca1 100644 --- a/storage/src/queue/mod.rs +++ b/storage/src/queue/mod.rs @@ -25,7 +25,7 @@ //! result = reader.recv() => { //! let Some((pos, item)) = result? else { break }; //! // Process item... -//! reader.ack(pos).await?; +//! reader.ack(pos)?; //! } //! _ = shutdown => break, //! } @@ -69,7 +69,7 @@ //! println!("Processing item at position {}", position); //! //! // Acknowledge after successful processing -//! queue.ack(position).await.unwrap(); +//! queue.ack(position).unwrap(); //! } //! }); //! ``` diff --git a/storage/src/queue/shared.rs b/storage/src/queue/shared.rs index 38bb0753dd3..3d983c471ba 100644 --- a/storage/src/queue/shared.rs +++ b/storage/src/queue/shared.rs @@ -62,11 +62,11 @@ impl Writer { items: impl IntoIterator, ) -> Result, Error> { let mut queue = self.queue.lock().await; - let start = queue.size().await; + let start = queue.size(); for item in items { queue.append(item).await?; } - let end = queue.size().await; + let end = queue.size(); if end > start { queue.commit().await?; } @@ -107,7 +107,7 @@ impl Writer { /// Returns the total number of items that have been enqueued. pub async fn size(&self) -> u64 { - self.queue.lock().await.size().await + self.queue.lock().await.size() } } @@ -166,7 +166,7 @@ impl Reader { /// /// Returns [super::Error::PositionOutOfRange] if the position is invalid. pub async fn ack(&self, position: u64) -> Result<(), Error> { - self.queue.lock().await.ack(position).await + self.queue.lock().await.ack(position) } /// See [Queue::ack_up_to]. @@ -175,7 +175,7 @@ impl Reader { /// /// Returns [super::Error::PositionOutOfRange] if `up_to` is invalid. pub async fn ack_up_to(&self, up_to: u64) -> Result<(), Error> { - self.queue.lock().await.ack_up_to(up_to).await + self.queue.lock().await.ack_up_to(up_to) } /// See [Queue::ack_floor]. @@ -190,7 +190,7 @@ impl Reader { /// See [Queue::is_empty]. pub async fn is_empty(&self) -> bool { - self.queue.lock().await.is_empty().await + self.queue.lock().await.is_empty() } /// See [Queue::reset]. diff --git a/storage/src/queue/storage.rs b/storage/src/queue/storage.rs index 632b8600a4a..4a403aa8bf0 100644 --- a/storage/src/queue/storage.rs +++ b/storage/src/queue/storage.rs @@ -1,11 +1,7 @@ //! Queue storage implementation. use super::{metrics, Error}; -use crate::{ - journal::contiguous::{variable, Reader as _}, - rmap::RMap, - Context, Persistable, -}; +use crate::{journal::contiguous::variable, rmap::RMap, Context, Persistable}; use commonware_codec::CodecShared; use commonware_runtime::{buffer::paged::CacheRef, telemetry::metrics::GaugeExt}; use std::num::{NonZeroU64, NonZeroUsize}; @@ -124,7 +120,7 @@ impl Queue { // On restart, ack_floor is the pruning boundary (items below are deleted). // acked_above is empty (in-memory state lost on restart). - let bounds = journal.reader().await.bounds(); + let bounds = journal.reader().bounds(); let acked_above = RMap::new(); debug!(floor = bounds.start, size = bounds.end, "queue initialized"); @@ -183,7 +179,7 @@ impl Queue { /// /// Returns an error if the underlying storage operation fails. pub async fn dequeue(&mut self) -> Result, Error> { - let reader = self.journal.reader().await; + let reader = self.journal.reader(); let size = reader.bounds().end; // Fast-forward above ack floor @@ -217,8 +213,8 @@ impl Queue { /// # Errors /// /// Returns [Error::PositionOutOfRange] if `position >= queue size`. - pub async fn ack(&mut self, position: u64) -> Result<(), Error> { - let size = self.journal.size().await; + pub fn ack(&mut self, position: u64) -> Result<(), Error> { + let size = self.journal.size(); if position >= size { return Err(Error::PositionOutOfRange(position, size)); } @@ -259,8 +255,8 @@ impl Queue { /// # Errors /// /// Returns [Error::PositionOutOfRange] if `up_to > queue size`. - pub async fn ack_up_to(&mut self, up_to: u64) -> Result<(), Error> { - let size = self.journal.size().await; + pub fn ack_up_to(&mut self, up_to: u64) -> Result<(), Error> { + let size = self.journal.size(); if up_to > size { return Err(Error::PositionOutOfRange(up_to, size)); } @@ -302,15 +298,15 @@ impl Queue { /// /// This count is not affected by pruning. It represents the position that the /// next enqueued item will receive. - pub async fn size(&self) -> u64 { - self.journal.size().await + pub const fn size(&self) -> u64 { + self.journal.size() } /// Returns whether all enqueued items have been acknowledged. - pub async fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { // If acked_above is non-empty, there's a gap at ack_floor (otherwise floor // would have advanced). So all items acked implies ack_floor == size. - self.ack_floor >= self.journal.size().await + self.ack_floor >= self.journal.size() } /// Reset the read position to the ack floor so [Self::dequeue] re-delivers @@ -328,8 +324,8 @@ impl Queue { /// Returns the number of items not yet read (test-only). #[cfg(test)] - pub(crate) async fn pending(&self) -> u64 { - self.journal.size().await.saturating_sub(self.read_pos) + pub(crate) const fn pending(&self) -> u64 { + self.journal.size().saturating_sub(self.read_pos) } /// Returns the count of acknowledged items above the ack floor (test-only). @@ -345,12 +341,12 @@ impl Queue { impl Persistable for Queue { type Error = Error; - async fn commit(&self) -> Result<(), Error> { + async fn commit(&mut self) -> Result<(), Error> { self.journal.commit().await?; Ok(()) } - async fn sync(&self) -> Result<(), Error> { + async fn sync(&mut self) -> Result<(), Error> { self.journal.sync().await?; self.journal.prune(self.ack_floor).await?; Ok(()) @@ -397,9 +393,9 @@ mod tests { .unwrap(); // Queue should be empty initially - assert!(queue.is_empty().await); - assert_eq!(queue.pending().await, 0); - assert_eq!(queue.size().await, 0); + assert!(queue.is_empty()); + assert_eq!(queue.pending(), 0); + assert_eq!(queue.size(), 0); // Enqueue items let pos0 = queue.enqueue(b"item0".to_vec()).await.unwrap(); @@ -409,28 +405,28 @@ mod tests { assert_eq!(pos0, 0); assert_eq!(pos1, 1); assert_eq!(pos2, 2); - assert_eq!(queue.size().await, 3); - assert_eq!(queue.pending().await, 3); - assert!(!queue.is_empty().await); + assert_eq!(queue.size(), 3); + assert_eq!(queue.pending(), 3); + assert!(!queue.is_empty()); // Dequeue items let (p, item) = queue.dequeue().await.unwrap().unwrap(); assert_eq!(p, 0); assert_eq!(item, b"item0"); - assert_eq!(queue.pending().await, 2); + assert_eq!(queue.pending(), 2); let (p, item) = queue.dequeue().await.unwrap().unwrap(); assert_eq!(p, 1); assert_eq!(item, b"item1"); - assert_eq!(queue.pending().await, 1); + assert_eq!(queue.pending(), 1); let (p, item) = queue.dequeue().await.unwrap().unwrap(); assert_eq!(p, 2); assert_eq!(item, b"item2"); - assert_eq!(queue.pending().await, 0); + assert_eq!(queue.pending(), 0); // Queue still has unacked items - assert!(!queue.is_empty().await); + assert!(!queue.is_empty()); assert!(queue.dequeue().await.unwrap().is_none()); }); } @@ -449,7 +445,7 @@ mod tests { queue.append(vec![i]).await.unwrap(); } queue.commit().await.unwrap(); - assert_eq!(queue.size().await, 5); + assert_eq!(queue.size(), 5); // Dequeue and verify order for i in 0..5 { @@ -464,10 +460,10 @@ mod tests { } queue.commit().await.unwrap(); queue.enqueue(vec![8]).await.unwrap(); - assert_eq!(queue.size().await, 9); + assert_eq!(queue.size(), 9); - queue.ack_up_to(9).await.unwrap(); - assert!(queue.is_empty().await); + queue.ack_up_to(9).unwrap(); + assert!(queue.is_empty()); }); } @@ -492,7 +488,7 @@ mod tests { let mut queue = Queue::<_, Vec>::init(context.child("second"), cfg) .await .unwrap(); - assert_eq!(queue.size().await, 4); + assert_eq!(queue.size(), 4); for i in 0..4 { let (pos, item) = queue.dequeue().await.unwrap().unwrap(); assert_eq!(pos, i); @@ -520,12 +516,12 @@ mod tests { for i in 0..5 { let (pos, _) = queue.dequeue().await.unwrap().unwrap(); assert_eq!(pos, i); - queue.ack(pos).await.unwrap(); + queue.ack(pos).unwrap(); assert_eq!(queue.ack_floor(), i + 1); } // All items acked - assert!(queue.is_empty().await); + assert!(queue.is_empty()); assert_eq!(queue.ack_floor(), 5); }); } @@ -550,24 +546,24 @@ mod tests { } // Ack out of order: 2, 4, 1, 3, 0 - queue.ack(2).await.unwrap(); + queue.ack(2).unwrap(); assert_eq!(queue.ack_floor(), 0); // Floor doesn't move assert!(queue.is_acked(2)); - queue.ack(4).await.unwrap(); + queue.ack(4).unwrap(); assert_eq!(queue.ack_floor(), 0); assert!(queue.is_acked(4)); - queue.ack(1).await.unwrap(); + queue.ack(1).unwrap(); assert_eq!(queue.ack_floor(), 0); - queue.ack(3).await.unwrap(); + queue.ack(3).unwrap(); assert_eq!(queue.ack_floor(), 0); // Ack 0 - floor should advance to 5 (consuming 1,2,3,4) - queue.ack(0).await.unwrap(); + queue.ack(0).unwrap(); assert_eq!(queue.ack_floor(), 5); - assert!(queue.is_empty().await); + assert!(queue.is_empty()); }); } @@ -586,7 +582,7 @@ mod tests { } // Batch ack items 0-4 - queue.ack_up_to(5).await.unwrap(); + queue.ack_up_to(5).unwrap(); assert_eq!(queue.ack_floor(), 5); // Items 0-4 should be acked @@ -619,17 +615,17 @@ mod tests { } // Ack some items out of order first - queue.ack(7).await.unwrap(); - queue.ack(8).await.unwrap(); + queue.ack(7).unwrap(); + queue.ack(8).unwrap(); assert_eq!(queue.acked_above_count(), 2); // Batch ack up to 5 - queue.ack_up_to(5).await.unwrap(); + queue.ack_up_to(5).unwrap(); assert_eq!(queue.ack_floor(), 5); assert_eq!(queue.acked_above_count(), 2); // Now batch ack up to 9 - should consume the acked_above entries - queue.ack_up_to(9).await.unwrap(); + queue.ack_up_to(9).unwrap(); assert_eq!(queue.ack_floor(), 9); assert_eq!(queue.acked_above_count(), 0); }); @@ -650,13 +646,13 @@ mod tests { } // Ack items 5, 6, 7 first - queue.ack(5).await.unwrap(); - queue.ack(6).await.unwrap(); - queue.ack(7).await.unwrap(); + queue.ack(5).unwrap(); + queue.ack(6).unwrap(); + queue.ack(7).unwrap(); assert_eq!(queue.ack_floor(), 0); // Batch ack up to 5 - should coalesce with 5, 6, 7 - queue.ack_up_to(5).await.unwrap(); + queue.ack_up_to(5).unwrap(); assert_eq!(queue.ack_floor(), 8); // Consumed 5, 6, 7 }); } @@ -674,15 +670,15 @@ mod tests { queue.enqueue(b"item1".to_vec()).await.unwrap(); // Can't ack_up_to beyond queue size - let err = queue.ack_up_to(5).await.unwrap_err(); + let err = queue.ack_up_to(5).unwrap_err(); assert!(matches!(err, Error::PositionOutOfRange(5, 2))); // Can ack_up_to at queue size - queue.ack_up_to(2).await.unwrap(); + queue.ack_up_to(2).unwrap(); assert_eq!(queue.ack_floor(), 2); // Acking up_to at or below floor is a no-op - queue.ack_up_to(1).await.unwrap(); + queue.ack_up_to(1).unwrap(); assert_eq!(queue.ack_floor(), 2); }); } @@ -702,8 +698,8 @@ mod tests { } // Ack items 1 and 3 before reading - queue.ack(1).await.unwrap(); - queue.ack(3).await.unwrap(); + queue.ack(1).unwrap(); + queue.ack(3).unwrap(); // Dequeue should skip 1 and 3 let (p, item) = queue.dequeue().await.unwrap().unwrap(); @@ -735,15 +731,15 @@ mod tests { queue.enqueue(b"item1".to_vec()).await.unwrap(); // Can't ack position beyond queue size - let err = queue.ack(5).await.unwrap_err(); + let err = queue.ack(5).unwrap_err(); assert!(matches!(err, Error::PositionOutOfRange(5, 2))); // Can ack unread items - queue.ack(1).await.unwrap(); + queue.ack(1).unwrap(); assert!(queue.is_acked(1)); // Double ack is a no-op - queue.ack(1).await.unwrap(); + queue.ack(1).unwrap(); }); } @@ -765,7 +761,7 @@ mod tests { // Read and ack some items for i in 0..15 { queue.dequeue().await.unwrap(); - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } assert_eq!(queue.ack_floor(), 15); @@ -794,7 +790,7 @@ mod tests { // First batch: ack items 0-14 for i in 0..15 { queue.dequeue().await.unwrap(); - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } assert_eq!(queue.ack_floor(), 15); @@ -804,10 +800,10 @@ mod tests { assert_eq!(item, vec![15]); // Second batch: ack items 15-29 - queue.ack(15).await.unwrap(); + queue.ack(15).unwrap(); for i in 16..30 { queue.dequeue().await.unwrap(); - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } assert_eq!(queue.ack_floor(), 30); @@ -817,15 +813,15 @@ mod tests { assert_eq!(item, vec![30]); // Third batch: ack remaining items - queue.ack(30).await.unwrap(); + queue.ack(30).unwrap(); for i in 31..50 { queue.dequeue().await.unwrap(); - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } assert_eq!(queue.ack_floor(), 50); // Queue should be empty now - assert!(queue.is_empty().await); + assert!(queue.is_empty()); assert!(queue.dequeue().await.unwrap().is_none()); }); } @@ -848,9 +844,9 @@ mod tests { } // Ack items 0, 1, 2 - but items_per_section=10, so no pruning - queue.ack(0).await.unwrap(); - queue.ack(1).await.unwrap(); - queue.ack(2).await.unwrap(); + queue.ack(0).unwrap(); + queue.ack(1).unwrap(); + queue.ack(2).unwrap(); assert_eq!(queue.ack_floor(), 3); queue.sync().await.unwrap(); @@ -894,7 +890,7 @@ mod tests { // Ack items 0-14 to advance floor past section 0 for i in 0..15 { - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } assert_eq!(queue.ack_floor(), 15); @@ -902,7 +898,7 @@ mod tests { queue.sync().await.unwrap(); // Verify pruning occurred - let pruning_boundary = queue.journal.bounds().await.start; + let pruning_boundary = queue.journal.bounds().start; assert!(pruning_boundary > 0, "expected some pruning to occur"); pruning_boundary @@ -915,7 +911,7 @@ mod tests { .unwrap(); // ack_floor = pruning_boundary (items 0-9 were pruned) - let pruning_boundary = queue.journal.bounds().await.start; + let pruning_boundary = queue.journal.bounds().start; assert_eq!(queue.ack_floor(), pruning_boundary); assert_eq!(pruning_boundary, expected_pruning_boundary); @@ -979,7 +975,7 @@ mod tests { // Read and ack some for i in 0..5 { queue.dequeue().await.unwrap(); - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } assert_eq!(queue.ack_floor(), 5); assert_eq!(queue.read_position(), 5); @@ -1010,7 +1006,7 @@ mod tests { .unwrap(); // Operations on empty queue - assert!(queue.is_empty().await); + assert!(queue.is_empty()); assert!(queue.dequeue().await.unwrap().is_none()); queue.sync().await.unwrap(); queue.reset(); @@ -1040,7 +1036,7 @@ mod tests { .await .unwrap(); - assert_eq!(queue.size().await, 2); + assert_eq!(queue.size(), 2); let (_, item) = queue.dequeue().await.unwrap().unwrap(); assert_eq!(item, b"item0"); @@ -1067,7 +1063,7 @@ mod tests { // Ack every 3rd item (sparse acking) for i in (0..100).step_by(3) { - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } // Dequeue should skip acked items @@ -1098,7 +1094,7 @@ mod tests { // Ack items 1-8 (not 0) for i in 1..9 { - queue.ack(i).await.unwrap(); + queue.ack(i).unwrap(); } // Acked_above should have items 1-8 @@ -1106,7 +1102,7 @@ mod tests { assert!(queue.acked_above_count() > 0); // Now ack 0 - floor should advance to 9, consuming all acked_above - queue.ack(0).await.unwrap(); + queue.ack(0).unwrap(); assert_eq!(queue.ack_floor(), 9); assert_eq!(queue.acked_above_count(), 0); }); @@ -1132,7 +1128,7 @@ mod tests { assert_eq!(queue.read_position(), 3); // Batch ack past read position - queue.ack_up_to(7).await.unwrap(); + queue.ack_up_to(7).unwrap(); assert_eq!(queue.ack_floor(), 7); // Dequeue should skip 3-6 and return 7 @@ -1193,8 +1189,8 @@ mod tests { ); // Sequential ack advances floor - queue.ack(0).await.unwrap(); - queue.ack(1).await.unwrap(); + queue.ack(0).unwrap(); + queue.ack(1).unwrap(); let encoded = context.encode(); assert!( encoded.contains("test_metrics_floor 2"), @@ -1202,8 +1198,8 @@ mod tests { ); // Out-of-order ack: floor stays until gap fills - queue.ack(4).await.unwrap(); - queue.ack(6).await.unwrap(); + queue.ack(4).unwrap(); + queue.ack(6).unwrap(); let encoded = context.encode(); assert!( encoded.contains("test_metrics_floor 2"), @@ -1211,8 +1207,8 @@ mod tests { ); // Fill gap coalesces floor forward - queue.ack(2).await.unwrap(); - queue.ack(3).await.unwrap(); + queue.ack(2).unwrap(); + queue.ack(3).unwrap(); let encoded = context.encode(); assert!( encoded.contains("test_metrics_floor 5"), @@ -1220,7 +1216,7 @@ mod tests { ); // ack_up_to advances floor past sparse ack at 6 - queue.ack_up_to(8).await.unwrap(); + queue.ack_up_to(8).unwrap(); let encoded = context.encode(); assert!( encoded.contains("test_metrics_floor 8"), @@ -1228,8 +1224,8 @@ mod tests { ); // Ack remaining - queue.ack(8).await.unwrap(); - queue.ack(9).await.unwrap(); + queue.ack(8).unwrap(); + queue.ack(9).unwrap(); let encoded = context.encode(); assert!( encoded.contains("test_metrics_floor 10"), @@ -1259,7 +1255,7 @@ mod tests { queue.enqueue(vec![i]).await.unwrap(); } let (pos, _) = queue.dequeue().await.unwrap().unwrap(); - queue.ack(pos).await.unwrap(); + queue.ack(pos).unwrap(); let encoded = context.encode(); assert!( @@ -1268,8 +1264,8 @@ mod tests { ); // Ack remaining items out-of-order to advance floor to 3 - queue.ack(2).await.unwrap(); - queue.ack(1).await.unwrap(); + queue.ack(2).unwrap(); + queue.ack(1).unwrap(); assert_eq!(queue.ack_floor(), 3); // next metric is still 1 (no dequeue yet)