Skip to main content

commonware_storage/journal/segmented/
fixed.rs

1//! Segmented journal for fixed-size items.
2//!
3//! # Format
4//!
5//! Data is stored in one blob per section. Items are stored sequentially:
6//!
7//! ```text
8//! +--------+--------+--------+----------+
9//! | item_0 | item_1 |   ...  | item_n-1 |
10//! +--------+--------+--------+----------+
11//! ```
12//!
13//! # Sync
14//!
15//! Data written to `Journal` may not be immediately persisted to `Storage`. Use the
16//! `sync` method to force pending data to be written.
17//!
18//! # Pruning
19//!
20//! All data must be assigned to a `section`. This allows pruning entire sections
21//! (and their corresponding blobs) independently.
22
23use super::manager::{AppendFactory, Config as ManagerConfig, Manager};
24use crate::journal::Error;
25use commonware_codec::{CodecFixed, CodecFixedShared, DecodeExt as _, ReadExt as _};
26use commonware_runtime::{
27    buffer::paged::{CacheRef, Replay},
28    Blob, Buf, Handle, Metrics, Storage,
29};
30use commonware_utils::NZUsize;
31use futures::{
32    stream::{self, Stream},
33    StreamExt,
34};
35use std::{marker::PhantomData, num::NonZeroUsize};
36use tracing::{trace, warn};
37
38/// State for replaying a single section's blob.
39struct ReplayState<B: Blob> {
40    section: u64,
41    replay: Replay<B>,
42    position: u64,
43    done: bool,
44}
45
46/// Configuration for the fixed segmented journal.
47#[derive(Clone)]
48pub struct Config {
49    /// The partition to use for storing blobs.
50    pub partition: String,
51
52    /// The page cache to use for caching data.
53    pub page_cache: CacheRef,
54
55    /// The size of the write buffer to use for each blob.
56    pub write_buffer: NonZeroUsize,
57}
58
59/// A segmented journal with fixed-size entries.
60///
61/// Each section is stored in a separate blob. Within each blob, items are fixed-size.
62///
63/// # Repair
64///
65/// Like
66/// [sqlite](https://github.com/sqlite/sqlite/blob/8658a8df59f00ec8fcfea336a2a6a4b5ef79d2ee/src/wal.c#L1504-L1505)
67/// and
68/// [rocksdb](https://github.com/facebook/rocksdb/blob/0c533e61bc6d89fdf1295e8e0bcee4edb3aef401/include/rocksdb/options.h#L441-L445),
69/// the first invalid data read will be considered the new end of the journal (and the
70/// underlying [Blob] will be truncated to the last valid item). Repair occurs during
71/// init by checking each blob's size.
72pub struct Journal<E: Storage + Metrics, A: CodecFixed> {
73    manager: Manager<E, AppendFactory>,
74    _array: PhantomData<A>,
75}
76
77impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
78    /// Size of each entry.
79    pub const CHUNK_SIZE: usize = A::SIZE;
80    const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE as u64;
81
82    /// Initialize a new `Journal` instance.
83    ///
84    /// All backing blobs are opened but not read during initialization. Use `replay`
85    /// to iterate over all items.
86    pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
87        let manager_cfg = ManagerConfig {
88            partition: cfg.partition,
89            factory: AppendFactory {
90                write_buffer: cfg.write_buffer,
91                page_cache_ref: cfg.page_cache,
92            },
93        };
94        let mut manager = Manager::init(context, manager_cfg).await?;
95
96        // Repair any blobs with trailing bytes (incomplete items from crash)
97        let sections: Vec<_> = manager.sections().collect();
98        for section in sections {
99            let size = manager.size(section)?;
100            if !size.is_multiple_of(Self::CHUNK_SIZE_U64) {
101                let valid_size = size - (size % Self::CHUNK_SIZE_U64);
102                warn!(
103                    section,
104                    invalid_size = size,
105                    new_size = valid_size,
106                    "trailing bytes detected: truncating"
107                );
108                manager.rewind_section(section, valid_size).await?;
109                // Startup repair is exceptional; make it durable immediately so callers do not
110                // need to track repaired sections separately.
111                manager.sync(section).await?;
112            }
113        }
114
115        Ok(Self {
116            manager,
117            _array: PhantomData,
118        })
119    }
120
121    /// Append a new item to the journal in the given section.
122    ///
123    /// Returns the position of the item within the section (0-indexed).
124    pub async fn append(&mut self, section: u64, item: &A) -> Result<u64, Error> {
125        let blob = self.manager.get_or_create(section).await?;
126
127        // Encode the item
128        let buf = item.encode_mut();
129        let offset = blob.append(&buf).await?;
130        if !offset.is_multiple_of(Self::CHUNK_SIZE_U64) {
131            return Err(Error::InvalidBlobSize(section, offset));
132        }
133        let position = offset / Self::CHUNK_SIZE_U64;
134        trace!(section, position, "appended item");
135
136        Ok(position)
137    }
138
139    /// Read the item at the given section and position.
140    ///
141    /// # Errors
142    ///
143    /// - [Error::AlreadyPrunedToSection] if the section has been pruned.
144    /// - [Error::SectionOutOfRange] if the section doesn't exist.
145    /// - [Error::ItemOutOfRange] if the position is beyond the blob size.
146    pub async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
147        let blob = self
148            .manager
149            .get(section)?
150            .ok_or(Error::SectionOutOfRange(section))?;
151
152        let offset = position
153            .checked_mul(Self::CHUNK_SIZE_U64)
154            .ok_or(Error::ItemOutOfRange(position))?;
155
156        // The read validates bounds against the blob's logical size.
157        let buf = blob
158            .read_at(offset, Self::CHUNK_SIZE)
159            .await
160            .map_err(|err| match err {
161                commonware_runtime::Error::BlobInsufficientLength
162                | commonware_runtime::Error::OffsetOverflow => Error::ItemOutOfRange(position),
163                err => Error::Runtime(err),
164            })?;
165        A::decode(buf.coalesce()).map_err(Error::Codec)
166    }
167
168    /// Read multiple items from the same section into a caller buffer.
169    ///
170    /// `buf` must be at least `positions.len() * CHUNK_SIZE` bytes. All positions must be
171    /// strictly increasing and within the section's bounds.
172    ///
173    /// Returns the decoded items and the number served without a blob read (page cache or tip
174    /// buffer hits).
175    pub async fn get_many(
176        &self,
177        section: u64,
178        positions: &[u64],
179        buf: &mut [u8],
180    ) -> Result<(Vec<A>, usize), Error> {
181        assert!(
182            positions.is_sorted_by(|a, b| a < b),
183            "positions must be strictly increasing"
184        );
185        if positions.is_empty() {
186            return Ok((Vec::new(), 0));
187        }
188        assert!(
189            buf.len() >= positions.len() * Self::CHUNK_SIZE,
190            "get_many requires buf.len() >= positions.len() * CHUNK_SIZE"
191        );
192        let buf = &mut buf[..positions.len() * Self::CHUNK_SIZE];
193        let blob = self
194            .manager
195            .get(section)?
196            .ok_or(Error::SectionOutOfRange(section))?;
197
198        let offsets: Vec<u64> = positions
199            .iter()
200            .map(|&p| {
201                p.checked_mul(Self::CHUNK_SIZE_U64)
202                    .ok_or(Error::ItemOutOfRange(p))
203            })
204            .collect::<Result<_, _>>()?;
205
206        let hits = blob
207            .read_many_into(buf, &offsets, NZUsize!(Self::CHUNK_SIZE))
208            .await?;
209
210        let mut items = Vec::with_capacity(positions.len());
211        for i in 0..positions.len() {
212            let slice = &buf[i * Self::CHUNK_SIZE..(i + 1) * Self::CHUNK_SIZE];
213            items.push(A::decode(slice).map_err(Error::Codec)?);
214        }
215        Ok((items, hits))
216    }
217
218    /// Get an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise.
219    pub fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
220        let blob = self.manager.get(section).ok()??;
221        let offset = position.checked_mul(Self::CHUNK_SIZE_U64)?;
222        let remaining = blob.size().checked_sub(offset)?;
223        if remaining < Self::CHUNK_SIZE_U64 {
224            return None;
225        }
226        let mut buf = vec![0u8; Self::CHUNK_SIZE];
227        if !blob.try_read_sync_into(&mut buf, offset) {
228            return None;
229        }
230        A::decode(&buf[..]).ok()
231    }
232
233    /// Read the last item in a section, if any.
234    ///
235    /// Returns `Ok(None)` if the section is empty.
236    ///
237    /// # Errors
238    ///
239    /// - [Error::AlreadyPrunedToSection] if the section has been pruned.
240    /// - [Error::SectionOutOfRange] if the section doesn't exist.
241    pub async fn last(&self, section: u64) -> Result<Option<A>, Error> {
242        let blob = self
243            .manager
244            .get(section)?
245            .ok_or(Error::SectionOutOfRange(section))?;
246
247        let size = blob.size();
248        if size < Self::CHUNK_SIZE_U64 {
249            return Ok(None);
250        }
251
252        let last_position = (size / Self::CHUNK_SIZE_U64) - 1;
253        let offset = last_position * Self::CHUNK_SIZE_U64;
254        let buf = blob.read_at(offset, Self::CHUNK_SIZE).await?;
255        A::decode(buf.coalesce()).map_err(Error::Codec).map(Some)
256    }
257
258    /// Returns a stream of all items starting from the given section.
259    ///
260    /// Each item is returned as (section, position, item).
261    pub async fn replay(
262        &mut self,
263        start_section: u64,
264        start_position: u64,
265        buffer: NonZeroUsize,
266    ) -> Result<impl Stream<Item = Result<(u64, u64, A), Error>> + Send + '_, Error> {
267        // Pre-create readers from blobs. This validates replay setup but does not allocate
268        // `buffer` bytes per blob; page buffers are allocated later by `Replay::ensure`.
269        let mut blob_info = Vec::new();
270        for (&section, blob) in self.manager.sections_from(start_section) {
271            let blob_size = blob.size();
272            let mut replay = blob.replay(buffer).await?;
273            // For the first section, seek to the start position
274            let initial_position = if section == start_section {
275                let start = start_position
276                    .checked_mul(Self::CHUNK_SIZE_U64)
277                    .ok_or(Error::ItemOutOfRange(start_position))?;
278                if start > blob_size {
279                    return Err(Error::ItemOutOfRange(start_position));
280                }
281                replay.seek_to(start)?;
282                start_position
283            } else {
284                0
285            };
286            blob_info.push((section, replay, initial_position));
287        }
288
289        // Stream items as they are read to avoid occupying too much memory.
290        // Each blob is processed sequentially, yielding batches of items that are then
291        // flattened into individual stream elements.
292        Ok(
293            stream::iter(blob_info).flat_map(move |(section, replay, initial_position)| {
294                stream::unfold(
295                    ReplayState {
296                        section,
297                        replay,
298                        position: initial_position,
299                        done: false,
300                    },
301                    move |mut state| async move {
302                        if state.done {
303                            return None;
304                        }
305
306                        let mut batch: Vec<Result<(u64, u64, A), Error>> = Vec::new();
307                        loop {
308                            // Ensure we have enough data for one item
309                            match state.replay.ensure(Self::CHUNK_SIZE).await {
310                                Ok(true) => {}
311                                Ok(false) => {
312                                    // Reader exhausted - we're done with this blob
313                                    state.done = true;
314                                    return if batch.is_empty() {
315                                        None
316                                    } else {
317                                        Some((batch, state))
318                                    };
319                                }
320                                Err(err) => {
321                                    batch.push(Err(Error::Runtime(err)));
322                                    state.done = true;
323                                    return Some((batch, state));
324                                }
325                            }
326
327                            // Decode items from buffer
328                            while state.replay.remaining() >= Self::CHUNK_SIZE {
329                                match A::read(&mut state.replay) {
330                                    Ok(item) => {
331                                        batch.push(Ok((state.section, state.position, item)));
332                                        state.position += 1;
333                                    }
334                                    Err(err) => {
335                                        batch.push(Err(Error::Codec(err)));
336                                        state.done = true;
337                                        return Some((batch, state));
338                                    }
339                                }
340                            }
341
342                            // Return batch if we have items
343                            if !batch.is_empty() {
344                                return Some((batch, state));
345                            }
346                        }
347                    },
348                )
349                .flat_map(stream::iter)
350            }),
351        )
352    }
353
354    /// Sync the given `sections` to storage.
355    pub async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
356        self.manager.sync(sections).await
357    }
358
359    /// Start syncing the given `sections` to storage.
360    pub async fn start_sync(
361        &mut self,
362        sections: impl crate::Sections,
363    ) -> Result<Handle<()>, Error> {
364        self.manager.start_sync(sections).await
365    }
366
367    /// Sync all sections to storage.
368    pub async fn sync_all(&mut self) -> Result<(), Error> {
369        self.manager.sync_all().await
370    }
371
372    /// Prune all sections less than `min`. Returns true if any were pruned.
373    pub async fn prune(&mut self, min: u64) -> Result<bool, Error> {
374        self.manager.prune(min).await
375    }
376
377    /// Returns the oldest section number, if any blobs exist.
378    pub fn oldest_section(&self) -> Option<u64> {
379        self.manager.oldest_section()
380    }
381
382    /// Returns the newest section number, if any blobs exist.
383    pub fn newest_section(&self) -> Option<u64> {
384        self.manager.newest_section()
385    }
386
387    /// Returns an iterator over all section numbers.
388    pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
389        self.manager.sections()
390    }
391
392    /// Returns the number of items in the given section.
393    pub fn section_len(&self, section: u64) -> Result<u64, Error> {
394        let size = self.manager.size(section)?;
395        Ok(size / Self::CHUNK_SIZE_U64)
396    }
397
398    /// Returns the byte size of the given section.
399    pub fn size(&self, section: u64) -> Result<u64, Error> {
400        self.manager.size(section)
401    }
402
403    /// Rewind the journal to a specific section and byte offset.
404    ///
405    /// This truncates the section to the given size. All sections
406    /// after `section` are removed.
407    pub async fn rewind(&mut self, section: u64, offset: u64) -> Result<(), Error> {
408        self.manager.rewind(section, offset).await
409    }
410
411    /// Rewind only the given section to a specific byte offset.
412    ///
413    /// Unlike `rewind`, this does not affect other sections.
414    pub async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
415        self.manager.rewind_section(section, size).await
416    }
417
418    /// Remove all underlying blobs.
419    pub async fn destroy(self) -> Result<(), Error> {
420        self.manager.destroy().await
421    }
422
423    /// Clear all data, resetting the journal to an empty state.
424    ///
425    /// Unlike `destroy`, this keeps the journal alive so it can be reused.
426    pub async fn clear(&mut self) -> Result<(), Error> {
427        self.manager.clear().await
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use commonware_cryptography::{sha256::Digest, Hasher as _, Sha256};
435    use commonware_macros::test_traced;
436    use commonware_runtime::{
437        buffer::paged::CacheRef,
438        deterministic,
439        mocks::{fail_pending_syncs, release_pending_syncs, DelayedSyncContext, PendingSyncs},
440        BufferPooler, Error as RError, Runner, Spawner as _, Supervisor as _,
441    };
442    use commonware_utils::{NZUsize, NZU16};
443    use core::num::NonZeroU16;
444    use futures::{pin_mut, StreamExt};
445    use std::sync::{
446        atomic::{AtomicUsize, Ordering},
447        Arc,
448    };
449
450    const PAGE_SIZE: NonZeroU16 = NZU16!(44);
451    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
452
453    fn test_digest(value: u64) -> Digest {
454        Sha256::hash(&value.to_be_bytes())
455    }
456
457    fn test_cfg(pooler: &impl BufferPooler) -> Config {
458        Config {
459            partition: "test-partition".into(),
460            page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
461            write_buffer: NZUsize!(2048),
462        }
463    }
464
465    #[test_traced]
466    fn test_segmented_fixed_append_and_get() {
467        let executor = deterministic::Runner::default();
468        executor.start(|context| async move {
469            let cfg = test_cfg(&context);
470            let mut journal = Journal::init(context.child("storage"), cfg.clone())
471                .await
472                .expect("failed to init");
473
474            let pos0 = journal
475                .append(1, &test_digest(0))
476                .await
477                .expect("failed to append");
478            assert_eq!(pos0, 0);
479
480            let pos1 = journal
481                .append(1, &test_digest(1))
482                .await
483                .expect("failed to append");
484            assert_eq!(pos1, 1);
485
486            let pos2 = journal
487                .append(2, &test_digest(2))
488                .await
489                .expect("failed to append");
490            assert_eq!(pos2, 0);
491
492            let item0 = journal.get(1, 0).await.expect("failed to get");
493            assert_eq!(item0, test_digest(0));
494
495            let item1 = journal.get(1, 1).await.expect("failed to get");
496            assert_eq!(item1, test_digest(1));
497
498            let item2 = journal.get(2, 0).await.expect("failed to get");
499            assert_eq!(item2, test_digest(2));
500
501            let err = journal.get(1, 2).await;
502            assert!(matches!(err, Err(Error::ItemOutOfRange(2))));
503
504            let err = journal.get(3, 0).await;
505            assert!(matches!(err, Err(Error::SectionOutOfRange(3))));
506
507            journal.destroy().await.expect("failed to destroy");
508        });
509    }
510
511    #[test_traced]
512    fn test_segmented_fixed_replay() {
513        let executor = deterministic::Runner::default();
514        executor.start(|context| async move {
515            let cfg = test_cfg(&context);
516            let mut journal = Journal::init(context.child("first"), cfg.clone())
517                .await
518                .expect("failed to init");
519
520            for i in 0u64..10 {
521                journal
522                    .append(1, &test_digest(i))
523                    .await
524                    .expect("failed to append");
525            }
526            for i in 10u64..20 {
527                journal
528                    .append(2, &test_digest(i))
529                    .await
530                    .expect("failed to append");
531            }
532
533            journal.sync_all().await.expect("failed to sync");
534            drop(journal);
535
536            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
537                .await
538                .expect("failed to re-init");
539
540            let items = {
541                let stream = journal
542                    .replay(0, 0, NZUsize!(1024))
543                    .await
544                    .expect("failed to replay");
545                pin_mut!(stream);
546
547                let mut items = Vec::new();
548                while let Some(result) = stream.next().await {
549                    match result {
550                        Ok((section, pos, item)) => items.push((section, pos, item)),
551                        Err(err) => panic!("replay error: {err}"),
552                    }
553                }
554                items
555            };
556
557            assert_eq!(items.len(), 20);
558            for (i, item) in items.iter().enumerate().take(10) {
559                assert_eq!(item.0, 1);
560                assert_eq!(item.1, i as u64);
561                assert_eq!(item.2, test_digest(i as u64));
562            }
563            for (i, item) in items.iter().enumerate().skip(10).take(10) {
564                assert_eq!(item.0, 2);
565                assert_eq!(item.1, (i - 10) as u64);
566                assert_eq!(item.2, test_digest(i as u64));
567            }
568
569            journal.destroy().await.expect("failed to destroy");
570        });
571    }
572
573    #[test_traced]
574    fn test_segmented_fixed_replay_with_start_offset() {
575        // Test that replay with a non-zero start_position correctly skips items.
576        let executor = deterministic::Runner::default();
577        executor.start(|context| async move {
578            let cfg = test_cfg(&context);
579            let mut journal = Journal::init(context.child("first"), cfg.clone())
580                .await
581                .expect("failed to init");
582
583            // Append 10 items to section 1
584            for i in 0u64..10 {
585                journal
586                    .append(1, &test_digest(i))
587                    .await
588                    .expect("failed to append");
589            }
590            // Append 5 items to section 2
591            for i in 10u64..15 {
592                journal
593                    .append(2, &test_digest(i))
594                    .await
595                    .expect("failed to append");
596            }
597            journal.sync_all().await.expect("failed to sync");
598            drop(journal);
599
600            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
601                .await
602                .expect("failed to re-init");
603
604            // Replay from section 1, position 5 - should get items 5-9 from section 1 and all of section 2
605            {
606                let stream = journal
607                    .replay(1, 5, NZUsize!(1024))
608                    .await
609                    .expect("failed to replay");
610                pin_mut!(stream);
611
612                let mut items = Vec::new();
613                while let Some(result) = stream.next().await {
614                    let (section, pos, item) = result.expect("replay error");
615                    items.push((section, pos, item));
616                }
617
618                assert_eq!(
619                    items.len(),
620                    10,
621                    "Should have 5 items from section 1 + 5 from section 2"
622                );
623
624                // Check section 1 items (positions 5-9)
625                for (i, (section, pos, item)) in items.iter().enumerate().take(5) {
626                    assert_eq!(*section, 1);
627                    assert_eq!(*pos, (i + 5) as u64);
628                    assert_eq!(*item, test_digest((i + 5) as u64));
629                }
630
631                // Check section 2 items (positions 0-4)
632                for (i, (section, pos, item)) in items.iter().enumerate().skip(5) {
633                    assert_eq!(*section, 2);
634                    assert_eq!(*pos, (i - 5) as u64);
635                    assert_eq!(*item, test_digest((i + 5) as u64));
636                }
637            }
638
639            // Replay from section 1, position 9 - should get only item 9 from section 1 and all of section 2
640            {
641                let stream = journal
642                    .replay(1, 9, NZUsize!(1024))
643                    .await
644                    .expect("failed to replay");
645                pin_mut!(stream);
646
647                let mut items = Vec::new();
648                while let Some(result) = stream.next().await {
649                    let (section, pos, item) = result.expect("replay error");
650                    items.push((section, pos, item));
651                }
652
653                assert_eq!(
654                    items.len(),
655                    6,
656                    "Should have 1 item from section 1 + 5 from section 2"
657                );
658                assert_eq!(items[0], (1, 9, test_digest(9)));
659                for (i, (section, pos, item)) in items.iter().enumerate().skip(1) {
660                    assert_eq!(*section, 2);
661                    assert_eq!(*pos, (i - 1) as u64);
662                    assert_eq!(*item, test_digest((i + 9) as u64));
663                }
664            }
665
666            // Replay from section 2, position 3 - should get only items 3-4 from section 2
667            {
668                let stream = journal
669                    .replay(2, 3, NZUsize!(1024))
670                    .await
671                    .expect("failed to replay");
672                pin_mut!(stream);
673
674                let mut items = Vec::new();
675                while let Some(result) = stream.next().await {
676                    let (section, pos, item) = result.expect("replay error");
677                    items.push((section, pos, item));
678                }
679
680                assert_eq!(items.len(), 2, "Should have 2 items from section 2");
681                assert_eq!(items[0], (2, 3, test_digest(13)));
682                assert_eq!(items[1], (2, 4, test_digest(14)));
683            }
684
685            // Replay from position past the end should return ItemOutOfRange error
686            let result = journal.replay(1, 100, NZUsize!(1024)).await;
687            assert!(matches!(result, Err(Error::ItemOutOfRange(100))));
688            drop(result);
689
690            let result = journal.replay(1, u64::MAX, NZUsize!(1024)).await;
691            assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
692            drop(result);
693
694            journal.destroy().await.expect("failed to destroy");
695        });
696    }
697
698    #[test_traced]
699    fn test_segmented_fixed_prune() {
700        let executor = deterministic::Runner::default();
701        executor.start(|context| async move {
702            let cfg = test_cfg(&context);
703            let mut journal = Journal::init(context.child("storage"), cfg.clone())
704                .await
705                .expect("failed to init");
706
707            for section in 1u64..=5 {
708                journal
709                    .append(section, &test_digest(section))
710                    .await
711                    .expect("failed to append");
712            }
713            journal.sync_all().await.expect("failed to sync");
714
715            journal.prune(3).await.expect("failed to prune");
716
717            let err = journal.get(1, 0).await;
718            assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
719
720            let err = journal.get(2, 0).await;
721            assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
722
723            let item = journal.get(3, 0).await.expect("should exist");
724            assert_eq!(item, test_digest(3));
725
726            journal.destroy().await.expect("failed to destroy");
727        });
728    }
729
730    #[test_traced]
731    fn test_segmented_fixed_rewind() {
732        let executor = deterministic::Runner::default();
733        executor.start(|context| async move {
734            let cfg = test_cfg(&context);
735            let mut journal = Journal::init(context.child("storage"), cfg.clone())
736                .await
737                .expect("failed to init");
738
739            // Create sections 1, 2, 3
740            for section in 1u64..=3 {
741                journal
742                    .append(section, &test_digest(section))
743                    .await
744                    .expect("failed to append");
745            }
746            journal.sync_all().await.expect("failed to sync");
747
748            // Verify all sections exist
749            for section in 1u64..=3 {
750                let size = journal.size(section).expect("failed to get size");
751                assert!(size > 0, "section {section} should have data");
752            }
753
754            // Rewind to section 1 (should remove sections 2, 3)
755            let size = journal.size(1).expect("failed to get size");
756            journal.rewind(1, size).await.expect("failed to rewind");
757
758            // Verify section 1 still has data
759            let size = journal.size(1).expect("failed to get size");
760            assert!(size > 0, "section 1 should still have data");
761
762            // Verify sections 2, 3 are removed
763            for section in 2u64..=3 {
764                let size = journal.size(section).expect("failed to get size");
765                assert_eq!(size, 0, "section {section} should be removed");
766            }
767
768            // Verify data in section 1 is still readable
769            let item = journal.get(1, 0).await.expect("failed to get");
770            assert_eq!(item, test_digest(1));
771
772            journal.destroy().await.expect("failed to destroy");
773        });
774    }
775
776    #[test_traced]
777    fn test_segmented_fixed_rewind_max_section() {
778        let executor = deterministic::Runner::default();
779        executor.start(|context| async move {
780            let cfg = test_cfg(&context);
781            let mut journal = Journal::init(context.child("storage"), cfg.clone())
782                .await
783                .expect("failed to init");
784
785            // Append to the maximal section. `section + 1` has no representable successor.
786            journal
787                .append(u64::MAX, &test_digest(0))
788                .await
789                .expect("failed to append");
790            journal.sync_all().await.expect("failed to sync");
791
792            // Rewinding the maximal section removes no sections above it and must not panic.
793            let size = journal.size(u64::MAX).expect("failed to get size");
794            journal
795                .rewind(u64::MAX, size)
796                .await
797                .expect("failed to rewind");
798
799            // The section is intact and readable.
800            assert_eq!(journal.size(u64::MAX).expect("failed to get size"), size);
801            assert_eq!(journal.get(u64::MAX, 0).await.unwrap(), test_digest(0));
802
803            journal.destroy().await.expect("failed to destroy");
804        });
805    }
806
807    #[test_traced]
808    fn test_segmented_fixed_rewind_many_sections() {
809        let executor = deterministic::Runner::default();
810        executor.start(|context| async move {
811            let cfg = test_cfg(&context);
812            let mut journal = Journal::init(context.child("storage"), cfg.clone())
813                .await
814                .expect("failed to init");
815
816            // Create sections 1-10
817            for section in 1u64..=10 {
818                journal
819                    .append(section, &test_digest(section))
820                    .await
821                    .expect("failed to append");
822            }
823            journal.sync_all().await.expect("failed to sync");
824
825            // Rewind to section 5 (should remove sections 6-10)
826            let size = journal.size(5).expect("failed to get size");
827            journal.rewind(5, size).await.expect("failed to rewind");
828
829            // Verify sections 1-5 still have data
830            for section in 1u64..=5 {
831                let size = journal.size(section).expect("failed to get size");
832                assert!(size > 0, "section {section} should still have data");
833            }
834
835            // Verify sections 6-10 are removed
836            for section in 6u64..=10 {
837                let size = journal.size(section).expect("failed to get size");
838                assert_eq!(size, 0, "section {section} should be removed");
839            }
840
841            // Verify data integrity via replay
842            {
843                let stream = journal
844                    .replay(0, 0, NZUsize!(1024))
845                    .await
846                    .expect("failed to replay");
847                pin_mut!(stream);
848                let mut items = Vec::new();
849                while let Some(result) = stream.next().await {
850                    let (section, _, item) = result.expect("failed to read");
851                    items.push((section, item));
852                }
853                assert_eq!(items.len(), 5);
854                for (i, (section, item)) in items.iter().enumerate() {
855                    assert_eq!(*section, (i + 1) as u64);
856                    assert_eq!(*item, test_digest((i + 1) as u64));
857                }
858            }
859
860            journal.destroy().await.expect("failed to destroy");
861        });
862    }
863
864    #[test_traced]
865    fn test_segmented_fixed_rewind_persistence() {
866        let executor = deterministic::Runner::default();
867        executor.start(|context| async move {
868            let cfg = test_cfg(&context);
869
870            // Create sections 1-5
871            let mut journal = Journal::init(context.child("first"), cfg.clone())
872                .await
873                .expect("failed to init");
874            for section in 1u64..=5 {
875                journal
876                    .append(section, &test_digest(section))
877                    .await
878                    .expect("failed to append");
879            }
880            journal.sync_all().await.expect("failed to sync");
881
882            // Rewind to section 2
883            let size = journal.size(2).expect("failed to get size");
884            journal.rewind(2, size).await.expect("failed to rewind");
885            journal.sync_all().await.expect("failed to sync");
886            drop(journal);
887
888            // Re-init and verify only sections 1-2 exist
889            let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
890                .await
891                .expect("failed to re-init");
892
893            // Verify sections 1-2 have data
894            for section in 1u64..=2 {
895                let size = journal.size(section).expect("failed to get size");
896                assert!(size > 0, "section {section} should have data after restart");
897            }
898
899            // Verify sections 3-5 are gone
900            for section in 3u64..=5 {
901                let size = journal.size(section).expect("failed to get size");
902                assert_eq!(size, 0, "section {section} should be gone after restart");
903            }
904
905            // Verify data integrity
906            let item1 = journal.get(1, 0).await.expect("failed to get");
907            assert_eq!(item1, test_digest(1));
908            let item2 = journal.get(2, 0).await.expect("failed to get");
909            assert_eq!(item2, test_digest(2));
910
911            journal.destroy().await.expect("failed to destroy");
912        });
913    }
914
915    #[test_traced]
916    fn test_segmented_fixed_corruption_recovery() {
917        let executor = deterministic::Runner::default();
918        executor.start(|context| async move {
919            let cfg = test_cfg(&context);
920            let mut journal = Journal::init(context.child("first"), cfg.clone())
921                .await
922                .expect("failed to init");
923
924            for i in 0u64..5 {
925                journal
926                    .append(1, &test_digest(i))
927                    .await
928                    .expect("failed to append");
929            }
930            journal.sync_all().await.expect("failed to sync");
931            drop(journal);
932
933            let (blob, size) = context
934                .open(&cfg.partition, &1u64.to_be_bytes())
935                .await
936                .expect("failed to open blob");
937            blob.resize(size - 1).await.expect("failed to truncate");
938            blob.sync().await.expect("failed to sync");
939
940            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
941                .await
942                .expect("failed to re-init");
943
944            let count = {
945                let stream = journal
946                    .replay(0, 0, NZUsize!(1024))
947                    .await
948                    .expect("failed to replay");
949                pin_mut!(stream);
950
951                let mut count = 0;
952                while let Some(result) = stream.next().await {
953                    result.expect("should be ok");
954                    count += 1;
955                }
956                count
957            };
958            assert_eq!(count, 4);
959
960            journal.destroy().await.expect("failed to destroy");
961        });
962    }
963
964    #[test_traced]
965    fn test_segmented_fixed_persistence() {
966        let executor = deterministic::Runner::default();
967        executor.start(|context| async move {
968            let cfg = test_cfg(&context);
969
970            // Create and populate journal
971            let mut journal = Journal::init(context.child("first"), cfg.clone())
972                .await
973                .expect("failed to init");
974
975            for i in 0u64..5 {
976                journal
977                    .append(1, &test_digest(i))
978                    .await
979                    .expect("failed to append");
980            }
981            journal.sync_all().await.expect("failed to sync");
982            drop(journal);
983
984            // Reopen and verify data persisted
985            let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
986                .await
987                .expect("failed to re-init");
988
989            for i in 0u64..5 {
990                let item = journal.get(1, i).await.expect("failed to get");
991                assert_eq!(item, test_digest(i));
992            }
993
994            journal.destroy().await.expect("failed to destroy");
995        });
996    }
997
998    #[test_traced]
999    fn test_segmented_fixed_sync() {
1000        let executor = deterministic::Runner::default();
1001        executor.start(|context| async move {
1002            let cfg = test_cfg(&context);
1003            let mut journal = Journal::init(context.child("first"), cfg.clone())
1004                .await
1005                .expect("failed to init");
1006
1007            // One sub-page item per section stays buffered until synced.
1008            for section in 1u64..=3 {
1009                journal
1010                    .append(section, &test_digest(section))
1011                    .await
1012                    .expect("failed to append");
1013            }
1014
1015            // Sync sections 1 and 3; a nonexistent section (99) is skipped, not an error.
1016            journal
1017                .sync(&[1, 3, 99])
1018                .await
1019                .expect("failed to sync sections");
1020            drop(journal);
1021
1022            // Only the synced sections survive the unclean drop.
1023            let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
1024                .await
1025                .expect("failed to re-init");
1026            assert_eq!(
1027                journal.get(1, 0).await.expect("section 1 durable"),
1028                test_digest(1)
1029            );
1030            assert_eq!(
1031                journal.get(3, 0).await.expect("section 3 durable"),
1032                test_digest(3)
1033            );
1034            assert!(matches!(
1035                journal.get(2, 0).await,
1036                Err(Error::ItemOutOfRange(0)) | Err(Error::SectionOutOfRange(2))
1037            ));
1038
1039            journal.destroy().await.expect("failed to destroy");
1040        });
1041    }
1042
1043    #[test_traced]
1044    fn test_segmented_fixed_section_len() {
1045        let executor = deterministic::Runner::default();
1046        executor.start(|context| async move {
1047            let cfg = test_cfg(&context);
1048            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1049                .await
1050                .expect("failed to init");
1051
1052            assert_eq!(journal.section_len(1).unwrap(), 0);
1053
1054            for i in 0u64..5 {
1055                journal
1056                    .append(1, &test_digest(i))
1057                    .await
1058                    .expect("failed to append");
1059            }
1060
1061            assert_eq!(journal.section_len(1).unwrap(), 5);
1062            assert_eq!(journal.section_len(2).unwrap(), 0);
1063
1064            journal.destroy().await.expect("failed to destroy");
1065        });
1066    }
1067
1068    #[test_traced]
1069    fn test_segmented_fixed_non_contiguous_sections() {
1070        // Test that sections with gaps in numbering work correctly.
1071        // Sections 1, 5, 10 should all be independent and accessible.
1072        let executor = deterministic::Runner::default();
1073        executor.start(|context| async move {
1074            let cfg = test_cfg(&context);
1075            let mut journal = Journal::init(context.child("first"), cfg.clone())
1076                .await
1077                .expect("failed to init");
1078
1079            // Create sections with gaps: 1, 5, 10
1080            journal
1081                .append(1, &test_digest(100))
1082                .await
1083                .expect("failed to append");
1084            journal
1085                .append(5, &test_digest(500))
1086                .await
1087                .expect("failed to append");
1088            journal
1089                .append(10, &test_digest(1000))
1090                .await
1091                .expect("failed to append");
1092            journal.sync_all().await.expect("failed to sync");
1093
1094            // Verify random access to each section
1095            assert_eq!(journal.get(1, 0).await.unwrap(), test_digest(100));
1096            assert_eq!(journal.get(5, 0).await.unwrap(), test_digest(500));
1097            assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(1000));
1098
1099            // Verify non-existent sections return appropriate errors
1100            for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
1101                let result = journal.get(missing_section, 0).await;
1102                assert!(
1103                    matches!(result, Err(Error::SectionOutOfRange(_))),
1104                    "Expected SectionOutOfRange for section {}, got {:?}",
1105                    missing_section,
1106                    result
1107                );
1108            }
1109
1110            // Drop and reopen to test replay
1111            drop(journal);
1112            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1113                .await
1114                .expect("failed to re-init");
1115
1116            // Replay and verify all items in order
1117            {
1118                let stream = journal
1119                    .replay(0, 0, NZUsize!(1024))
1120                    .await
1121                    .expect("failed to replay");
1122                pin_mut!(stream);
1123
1124                let mut items = Vec::new();
1125                while let Some(result) = stream.next().await {
1126                    let (section, _, item) = result.expect("replay error");
1127                    items.push((section, item));
1128                }
1129
1130                assert_eq!(items.len(), 3, "Should have 3 items");
1131                assert_eq!(items[0], (1, test_digest(100)));
1132                assert_eq!(items[1], (5, test_digest(500)));
1133                assert_eq!(items[2], (10, test_digest(1000)));
1134            }
1135
1136            // Test replay starting from middle section (5)
1137            {
1138                let stream = journal
1139                    .replay(5, 0, NZUsize!(1024))
1140                    .await
1141                    .expect("failed to replay from section 5");
1142                pin_mut!(stream);
1143
1144                let mut items = Vec::new();
1145                while let Some(result) = stream.next().await {
1146                    let (section, _, item) = result.expect("replay error");
1147                    items.push((section, item));
1148                }
1149
1150                assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
1151                assert_eq!(items[0], (5, test_digest(500)));
1152                assert_eq!(items[1], (10, test_digest(1000)));
1153            }
1154
1155            journal.destroy().await.expect("failed to destroy");
1156        });
1157    }
1158
1159    #[test_traced]
1160    fn test_segmented_fixed_empty_section_in_middle() {
1161        // Test that replay correctly handles an empty section between sections with data.
1162        // Section 1 has data, section 2 is empty, section 3 has data.
1163        let executor = deterministic::Runner::default();
1164        executor.start(|context| async move {
1165            let cfg = test_cfg(&context);
1166            let mut journal = Journal::init(context.child("first"), cfg.clone())
1167                .await
1168                .expect("failed to init");
1169
1170            // Append to section 1
1171            journal
1172                .append(1, &test_digest(100))
1173                .await
1174                .expect("failed to append");
1175
1176            // Create section 2 but make it empty via rewind
1177            journal
1178                .append(2, &test_digest(200))
1179                .await
1180                .expect("failed to append");
1181            journal.sync(2).await.expect("failed to sync");
1182            journal
1183                .rewind_section(2, 0)
1184                .await
1185                .expect("failed to rewind");
1186
1187            // Append to section 3
1188            journal
1189                .append(3, &test_digest(300))
1190                .await
1191                .expect("failed to append");
1192
1193            journal.sync_all().await.expect("failed to sync");
1194
1195            // Verify section lengths
1196            assert_eq!(journal.section_len(1).unwrap(), 1);
1197            assert_eq!(journal.section_len(2).unwrap(), 0);
1198            assert_eq!(journal.section_len(3).unwrap(), 1);
1199
1200            // Drop and reopen to test replay
1201            drop(journal);
1202            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1203                .await
1204                .expect("failed to re-init");
1205
1206            // Replay all - should get items from sections 1 and 3, skipping empty section 2
1207            {
1208                let stream = journal
1209                    .replay(0, 0, NZUsize!(1024))
1210                    .await
1211                    .expect("failed to replay");
1212                pin_mut!(stream);
1213
1214                let mut items = Vec::new();
1215                while let Some(result) = stream.next().await {
1216                    let (section, _, item) = result.expect("replay error");
1217                    items.push((section, item));
1218                }
1219
1220                assert_eq!(
1221                    items.len(),
1222                    2,
1223                    "Should have 2 items (skipping empty section)"
1224                );
1225                assert_eq!(items[0], (1, test_digest(100)));
1226                assert_eq!(items[1], (3, test_digest(300)));
1227            }
1228
1229            // Replay starting from empty section 2 - should get only section 3
1230            {
1231                let stream = journal
1232                    .replay(2, 0, NZUsize!(1024))
1233                    .await
1234                    .expect("failed to replay from section 2");
1235                pin_mut!(stream);
1236
1237                let mut items = Vec::new();
1238                while let Some(result) = stream.next().await {
1239                    let (section, _, item) = result.expect("replay error");
1240                    items.push((section, item));
1241                }
1242
1243                assert_eq!(items.len(), 1, "Should have 1 item from section 3");
1244                assert_eq!(items[0], (3, test_digest(300)));
1245            }
1246
1247            journal.destroy().await.expect("failed to destroy");
1248        });
1249    }
1250
1251    #[test_traced]
1252    fn test_segmented_fixed_truncation_recovery_across_page_boundary() {
1253        // Test that truncating a single byte from a blob that has items straddling a page boundary
1254        // correctly recovers by removing the incomplete item.
1255        //
1256        // With PAGE_SIZE=44 and ITEM_SIZE=32:
1257        // - Item 0: bytes 0-31
1258        // - Item 1: bytes 32-63 (straddles page boundary at 44)
1259        // - Item 2: bytes 64-95 (straddles page boundary at 88)
1260        //
1261        // After 3 items we have 96 bytes = 2 full pages + 8 bytes. Truncating 1 byte leaves 95
1262        // bytes, which is not a multiple of 32. Recovery should truncate to 64 bytes (2 complete
1263        // items).
1264        let executor = deterministic::Runner::default();
1265        executor.start(|context| async move {
1266            let cfg = test_cfg(&context);
1267            let mut journal = Journal::init(context.child("first"), cfg.clone())
1268                .await
1269                .expect("failed to init");
1270
1271            // Append 3 items (just over 2 pages worth)
1272            for i in 0u64..3 {
1273                journal
1274                    .append(1, &test_digest(i))
1275                    .await
1276                    .expect("failed to append");
1277            }
1278            journal.sync_all().await.expect("failed to sync");
1279
1280            // Verify all 3 items are readable
1281            for i in 0u64..3 {
1282                let item = journal.get(1, i).await.expect("failed to get");
1283                assert_eq!(item, test_digest(i));
1284            }
1285            drop(journal);
1286
1287            // Truncate the blob by exactly 1 byte to simulate partial write
1288            let (blob, size) = context
1289                .open(&cfg.partition, &1u64.to_be_bytes())
1290                .await
1291                .expect("failed to open blob");
1292            blob.resize(size - 1).await.expect("failed to truncate");
1293            blob.sync().await.expect("failed to sync");
1294            drop(blob);
1295
1296            // Reopen journal - should recover by truncating last page due to failed checksum, and
1297            // end up with a correct blob size due to partial-item trimming.
1298            let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1299                .await
1300                .expect("failed to re-init");
1301
1302            // Verify section now has only 2 items
1303            assert_eq!(journal.section_len(1).unwrap(), 2);
1304
1305            // Verify size is the expected multiple of ITEM_SIZE (this would fail if we didn't trim
1306            // items and just relied on page-level checksum recovery).
1307            assert_eq!(journal.size(1).unwrap(), 64);
1308
1309            // Items 0 and 1 should still be readable
1310            let item0 = journal.get(1, 0).await.expect("failed to get item 0");
1311            assert_eq!(item0, test_digest(0));
1312            let item1 = journal.get(1, 1).await.expect("failed to get item 1");
1313            assert_eq!(item1, test_digest(1));
1314
1315            // Item 2 should return ItemOutOfRange
1316            let err = journal.get(1, 2).await;
1317            assert!(
1318                matches!(err, Err(Error::ItemOutOfRange(2))),
1319                "expected ItemOutOfRange(2), got {:?}",
1320                err
1321            );
1322
1323            journal.destroy().await.expect("failed to destroy");
1324        });
1325    }
1326
1327    #[test_traced]
1328    fn test_journal_clear() {
1329        let executor = deterministic::Runner::default();
1330        executor.start(|context| async move {
1331            let cfg = Config {
1332                partition: "clear-test".into(),
1333                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1334                write_buffer: NZUsize!(1024),
1335            };
1336
1337            let mut journal: Journal<_, Digest> =
1338                Journal::init(context.child("journal"), cfg.clone())
1339                    .await
1340                    .expect("Failed to initialize journal");
1341
1342            // Append items across multiple sections
1343            for section in 0..5u64 {
1344                for i in 0..10u64 {
1345                    journal
1346                        .append(section, &test_digest(section * 1000 + i))
1347                        .await
1348                        .expect("Failed to append");
1349                }
1350                journal.sync(section).await.expect("Failed to sync");
1351            }
1352
1353            // Verify we have data
1354            assert_eq!(journal.get(0, 0).await.unwrap(), test_digest(0));
1355            assert_eq!(journal.get(4, 0).await.unwrap(), test_digest(4000));
1356
1357            // Clear the journal
1358            journal.clear().await.expect("Failed to clear");
1359
1360            // After clear, all reads should fail
1361            for section in 0..5u64 {
1362                assert!(matches!(
1363                    journal.get(section, 0).await,
1364                    Err(Error::SectionOutOfRange(s)) if s == section
1365                ));
1366            }
1367
1368            // Append new data after clear
1369            for i in 0..5u64 {
1370                journal
1371                    .append(10, &test_digest(i * 100))
1372                    .await
1373                    .expect("Failed to append after clear");
1374            }
1375            journal.sync(10).await.expect("Failed to sync after clear");
1376
1377            // New data should be readable
1378            assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(0));
1379
1380            // Old sections should still be missing
1381            assert!(matches!(
1382                journal.get(0, 0).await,
1383                Err(Error::SectionOutOfRange(0))
1384            ));
1385
1386            journal.destroy().await.unwrap();
1387        });
1388    }
1389
1390    #[test_traced]
1391    fn test_last_missing_section_returns_error() {
1392        let executor = deterministic::Runner::default();
1393        executor.start(|context| async move {
1394            let cfg = test_cfg(&context);
1395            let journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
1396                .await
1397                .expect("failed to init");
1398
1399            assert!(matches!(
1400                journal.last(0).await,
1401                Err(Error::SectionOutOfRange(0))
1402            ));
1403            assert!(matches!(
1404                journal.last(99).await,
1405                Err(Error::SectionOutOfRange(99))
1406            ));
1407
1408            journal.destroy().await.unwrap();
1409        });
1410    }
1411
1412    #[test_traced]
1413    fn test_last_after_rewind_to_zero() {
1414        let executor = deterministic::Runner::default();
1415        executor.start(|context| async move {
1416            let cfg = test_cfg(&context);
1417            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1418                .await
1419                .expect("failed to init");
1420
1421            journal.append(0, &test_digest(0)).await.unwrap();
1422            journal.append(0, &test_digest(1)).await.unwrap();
1423            journal.sync(0).await.unwrap();
1424
1425            assert!(journal.last(0).await.unwrap().is_some());
1426
1427            journal.rewind(0, 0).await.unwrap();
1428            assert_eq!(journal.last(0).await.unwrap(), None);
1429
1430            journal.destroy().await.unwrap();
1431        });
1432    }
1433
1434    #[test_traced]
1435    fn test_last_pruned_section_returns_error() {
1436        let executor = deterministic::Runner::default();
1437        executor.start(|context| async move {
1438            let cfg = test_cfg(&context);
1439            let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
1440                .await
1441                .expect("failed to init");
1442
1443            journal.append(0, &test_digest(0)).await.unwrap();
1444            journal.append(1, &test_digest(1)).await.unwrap();
1445            journal.sync_all().await.unwrap();
1446
1447            journal.prune(1).await.unwrap();
1448
1449            assert!(matches!(
1450                journal.last(0).await,
1451                Err(Error::AlreadyPrunedToSection(1))
1452            ));
1453            assert!(journal.last(1).await.unwrap().is_some());
1454
1455            journal.destroy().await.unwrap();
1456        });
1457    }
1458
1459    #[test_traced]
1460    fn test_get_many_empty() {
1461        let executor = deterministic::Runner::default();
1462        executor.start(|context| async move {
1463            let cfg = test_cfg(&context);
1464            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
1465            journal.append(0, &test_digest(0)).await.unwrap();
1466            assert_eq!(journal.section_len(0).unwrap(), 1);
1467
1468            let mut buf = [];
1469            let (items, hits) = journal.get_many(0, &[], &mut buf).await.unwrap();
1470            assert!(items.is_empty());
1471            assert_eq!(hits, 0);
1472
1473            journal.destroy().await.unwrap();
1474        });
1475    }
1476
1477    #[test_traced]
1478    fn test_get_many_single_section() {
1479        let executor = deterministic::Runner::default();
1480        executor.start(|context| async move {
1481            let cfg = test_cfg(&context);
1482            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
1483
1484            for i in 0..5 {
1485                journal.append(0, &test_digest(i)).await.unwrap();
1486            }
1487            assert_eq!(journal.section_len(0).unwrap(), 5);
1488
1489            // Read all 5 items in one call. The reusable buffer is intentionally oversized:
1490            // get_many slices it to the exact length the batch needs.
1491            let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
1492            let mut buf = vec![0u8; 6 * chunk];
1493            let (items, _) = journal
1494                .get_many(0, &[0, 1, 2, 3, 4], &mut buf)
1495                .await
1496                .unwrap();
1497
1498            for (i, item) in items.iter().enumerate() {
1499                assert_eq!(*item, test_digest(i as u64));
1500            }
1501
1502            journal.destroy().await.unwrap();
1503        });
1504    }
1505
1506    #[test_traced]
1507    fn test_get_many_subset() {
1508        // Read a sparse subset of positions.
1509        let executor = deterministic::Runner::default();
1510        executor.start(|context| async move {
1511            let cfg = test_cfg(&context);
1512            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
1513
1514            for i in 0..10 {
1515                journal.append(0, &test_digest(i)).await.unwrap();
1516            }
1517            assert_eq!(journal.section_len(0).unwrap(), 10);
1518
1519            let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
1520            let positions = [1, 4, 7, 9];
1521            let mut buf = vec![0u8; positions.len() * chunk];
1522            let (items, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
1523
1524            for (i, &pos) in positions.iter().enumerate() {
1525                assert_eq!(items[i], test_digest(pos));
1526            }
1527
1528            journal.destroy().await.unwrap();
1529        });
1530    }
1531
1532    #[test_traced]
1533    fn test_get_many_bad_section() {
1534        let executor = deterministic::Runner::default();
1535        executor.start(|context| async move {
1536            let cfg = test_cfg(&context);
1537            let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
1538                .await
1539                .unwrap();
1540
1541            let mut buf = vec![0u8; 64];
1542            let err = journal.get_many(99, &[0], &mut buf).await.unwrap_err();
1543            assert!(matches!(err, Error::SectionOutOfRange(99)));
1544
1545            journal.destroy().await.unwrap();
1546        });
1547    }
1548
1549    #[test_traced]
1550    fn test_get_many_matches_get() {
1551        // Verify batch read matches individual reads.
1552        let executor = deterministic::Runner::default();
1553        executor.start(|context| async move {
1554            let cfg = test_cfg(&context);
1555            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
1556
1557            for i in 0..8 {
1558                journal.append(0, &test_digest(i)).await.unwrap();
1559            }
1560            assert_eq!(journal.section_len(0).unwrap(), 8);
1561            journal.sync_all().await.unwrap();
1562
1563            let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
1564            let positions: Vec<u64> = (0..8).collect();
1565            let mut buf = vec![0u8; positions.len() * chunk];
1566            let (batch, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
1567
1568            for pos in &positions {
1569                let single = journal.get(0, *pos).await.unwrap();
1570                assert_eq!(batch[*pos as usize], single);
1571            }
1572
1573            journal.destroy().await.unwrap();
1574        });
1575    }
1576
1577    #[test_traced]
1578    fn test_segmented_fixed_prune_waits_for_in_flight_start_sync() {
1579        let executor = deterministic::Runner::default();
1580        executor.start(|context| async move {
1581            let pending = PendingSyncs::default();
1582            let context = DelayedSyncContext {
1583                inner: context,
1584                pending: pending.clone(),
1585            };
1586            let cfg = test_cfg(&context);
1587            let mut journal = Journal::init(context.child("storage"), cfg)
1588                .await
1589                .expect("failed to init");
1590
1591            journal
1592                .append(1, &test_digest(0))
1593                .await
1594                .expect("failed to append");
1595            let handle = journal.start_sync(1).await.expect("failed to start sync");
1596            assert!(!pending.lock().is_empty());
1597
1598            let started = Arc::new(AtomicUsize::new(0));
1599            let completed = Arc::new(AtomicUsize::new(0));
1600            let started_clone = started.clone();
1601            let completed_clone = completed.clone();
1602            let waiter = context.inner.child("prune").spawn(|_| async move {
1603                started_clone.fetch_add(1, Ordering::Relaxed);
1604                assert!(journal.prune(2).await.expect("failed to prune"));
1605                completed_clone.fetch_add(1, Ordering::Relaxed);
1606                journal
1607            });
1608
1609            while started.load(Ordering::Relaxed) == 0 {
1610                commonware_runtime::reschedule().await;
1611            }
1612            commonware_runtime::reschedule().await;
1613            assert_eq!(
1614                completed.load(Ordering::Relaxed),
1615                0,
1616                "prune must wait for in-flight syncs on pruned sections"
1617            );
1618
1619            release_pending_syncs(&pending);
1620            handle
1621                .await
1622                .expect("sync handle should complete despite pruning");
1623            while completed.load(Ordering::Relaxed) == 0 {
1624                commonware_runtime::reschedule().await;
1625            }
1626            let journal = waiter.await.expect("prune task failed");
1627            assert_eq!(journal.oldest_section(), None);
1628        });
1629    }
1630
1631    #[test_traced]
1632    fn test_segmented_fixed_destroy_waits_for_in_flight_start_sync() {
1633        let executor = deterministic::Runner::default();
1634        executor.start(|context| async move {
1635            let pending = PendingSyncs::default();
1636            let context = DelayedSyncContext {
1637                inner: context,
1638                pending: pending.clone(),
1639            };
1640            let cfg = test_cfg(&context);
1641            let mut journal = Journal::init(context.child("storage"), cfg)
1642                .await
1643                .expect("failed to init");
1644
1645            journal
1646                .append(1, &test_digest(0))
1647                .await
1648                .expect("failed to append");
1649            let handle = journal.start_sync(1).await.expect("failed to start sync");
1650            assert!(!pending.lock().is_empty());
1651
1652            let started = Arc::new(AtomicUsize::new(0));
1653            let completed = Arc::new(AtomicUsize::new(0));
1654            let started_clone = started.clone();
1655            let completed_clone = completed.clone();
1656            let waiter = context.inner.child("destroy").spawn(|_| async move {
1657                started_clone.fetch_add(1, Ordering::Relaxed);
1658                journal.destroy().await.expect("failed to destroy");
1659                completed_clone.fetch_add(1, Ordering::Relaxed);
1660            });
1661
1662            while started.load(Ordering::Relaxed) == 0 {
1663                commonware_runtime::reschedule().await;
1664            }
1665            commonware_runtime::reschedule().await;
1666            assert_eq!(
1667                completed.load(Ordering::Relaxed),
1668                0,
1669                "destroy must wait for in-flight syncs"
1670            );
1671
1672            release_pending_syncs(&pending);
1673            handle
1674                .await
1675                .expect("sync handle should complete despite destruction");
1676            while completed.load(Ordering::Relaxed) == 0 {
1677                commonware_runtime::reschedule().await;
1678            }
1679            waiter.await.expect("destroy task failed");
1680        });
1681    }
1682
1683    #[test_traced]
1684    fn test_segmented_fixed_clear_waits_for_in_flight_start_sync() {
1685        let executor = deterministic::Runner::default();
1686        executor.start(|context| async move {
1687            let pending = PendingSyncs::default();
1688            let context = DelayedSyncContext {
1689                inner: context,
1690                pending: pending.clone(),
1691            };
1692            let cfg = test_cfg(&context);
1693            let mut journal = Journal::init(context.child("storage"), cfg)
1694                .await
1695                .expect("failed to init");
1696
1697            journal
1698                .append(1, &test_digest(0))
1699                .await
1700                .expect("failed to append");
1701            let handle = journal.start_sync(1).await.expect("failed to start sync");
1702            assert!(!pending.lock().is_empty());
1703
1704            let started = Arc::new(AtomicUsize::new(0));
1705            let completed = Arc::new(AtomicUsize::new(0));
1706            let started_clone = started.clone();
1707            let completed_clone = completed.clone();
1708            let waiter = context.inner.child("clear").spawn(|_| async move {
1709                started_clone.fetch_add(1, Ordering::Relaxed);
1710                journal.clear().await.expect("failed to clear");
1711                completed_clone.fetch_add(1, Ordering::Relaxed);
1712                journal
1713            });
1714
1715            while started.load(Ordering::Relaxed) == 0 {
1716                commonware_runtime::reschedule().await;
1717            }
1718            commonware_runtime::reschedule().await;
1719            assert_eq!(
1720                completed.load(Ordering::Relaxed),
1721                0,
1722                "clear must wait for in-flight syncs"
1723            );
1724
1725            release_pending_syncs(&pending);
1726            handle
1727                .await
1728                .expect("sync handle should complete despite clearing");
1729            while completed.load(Ordering::Relaxed) == 0 {
1730                commonware_runtime::reschedule().await;
1731            }
1732            let mut journal = waiter.await.expect("clear task failed");
1733
1734            // The journal must remain usable after clear.
1735            assert_eq!(journal.oldest_section(), None);
1736            let position = journal
1737                .append(1, &test_digest(1))
1738                .await
1739                .expect("failed to append after clear");
1740            assert_eq!(position, 0);
1741            journal.destroy().await.expect("failed to destroy");
1742        });
1743    }
1744
1745    #[test_traced]
1746    fn test_segmented_fixed_rewind_waits_for_in_flight_start_sync() {
1747        let executor = deterministic::Runner::default();
1748        executor.start(|context| async move {
1749            let pending = PendingSyncs::default();
1750            let context = DelayedSyncContext {
1751                inner: context,
1752                pending: pending.clone(),
1753            };
1754            let cfg = test_cfg(&context);
1755            let mut journal = Journal::init(context.child("storage"), cfg)
1756                .await
1757                .expect("failed to init");
1758
1759            journal
1760                .append(1, &test_digest(0))
1761                .await
1762                .expect("failed to append");
1763            journal
1764                .append(2, &test_digest(1))
1765                .await
1766                .expect("failed to append");
1767            let handle = journal.start_sync(2).await.expect("failed to start sync");
1768            assert!(!pending.lock().is_empty());
1769
1770            let size = journal.size(1).expect("failed to get size");
1771            let started = Arc::new(AtomicUsize::new(0));
1772            let completed = Arc::new(AtomicUsize::new(0));
1773            let started_clone = started.clone();
1774            let completed_clone = completed.clone();
1775            let waiter = context.inner.child("rewind").spawn(move |_| async move {
1776                started_clone.fetch_add(1, Ordering::Relaxed);
1777                journal.rewind(1, size).await.expect("failed to rewind");
1778                completed_clone.fetch_add(1, Ordering::Relaxed);
1779                journal
1780            });
1781
1782            while started.load(Ordering::Relaxed) == 0 {
1783                commonware_runtime::reschedule().await;
1784            }
1785            commonware_runtime::reschedule().await;
1786            assert_eq!(
1787                completed.load(Ordering::Relaxed),
1788                0,
1789                "rewind must wait for in-flight syncs on removed sections"
1790            );
1791
1792            release_pending_syncs(&pending);
1793            handle
1794                .await
1795                .expect("sync handle should complete despite rewind");
1796            while completed.load(Ordering::Relaxed) == 0 {
1797                commonware_runtime::reschedule().await;
1798            }
1799            let journal = waiter.await.expect("rewind task failed");
1800            assert_eq!(journal.size(2).expect("failed to get size"), 0);
1801            journal.destroy().await.expect("failed to destroy");
1802        });
1803    }
1804
1805    #[test_traced]
1806    fn test_segmented_fixed_prune_surfaces_failed_in_flight_start_sync() {
1807        let executor = deterministic::Runner::default();
1808        executor.start(|context| async move {
1809            let pending = PendingSyncs::default();
1810            let context = DelayedSyncContext {
1811                inner: context,
1812                pending: pending.clone(),
1813            };
1814            let cfg = test_cfg(&context);
1815            let mut journal = Journal::init(context.child("storage"), cfg)
1816                .await
1817                .expect("failed to init");
1818
1819            journal
1820                .append(1, &test_digest(0))
1821                .await
1822                .expect("failed to append");
1823            let handle = journal.start_sync(1).await.expect("failed to start sync");
1824            fail_pending_syncs(&pending);
1825
1826            let err = journal
1827                .prune(2)
1828                .await
1829                .expect_err("prune must surface a failed in-flight sync");
1830            assert!(matches!(err, Error::Runtime(RError::Io(_))));
1831
1832            let err = handle.await.expect_err("sync handle should fail");
1833            assert!(matches!(err, RError::Io(_)));
1834        });
1835    }
1836}