1use super::{
98 blobs::{Blob, Blobs, Partition, Replay as BlobReplay, Writable},
99 checkpoint::Checkpoint,
100};
101#[commonware_macros::stability(ALPHA)]
102use crate::{journal::authenticated, merkle};
103use crate::{
104 journal::{
105 contiguous::{metrics::Metrics, Many, Mutable},
106 Error,
107 },
108 Context,
109};
110use commonware_codec::{CodecFixedShared, DecodeExt as _, ReadExt as _};
111use commonware_runtime::{
112 buffer::paged::{CacheRef, Writer},
113 Blob as RBlob, Buf, IoBuf,
114};
115use commonware_utils::Cached;
116use futures::{future::try_join_all, Stream};
117use std::{
118 collections::BTreeMap,
119 future::Future,
120 marker::PhantomData,
121 num::{NonZeroU64, NonZeroUsize},
122 ops::Range,
123 sync::Arc,
124};
125use tracing::warn;
126
127commonware_utils::thread_local_cache!(static PROBE_SCRATCH: Vec<u8>);
131
132pub struct PreparedAppend<A> {
135 buf: Vec<u8>,
136 _marker: PhantomData<A>,
137}
138
139#[inline]
141fn first_in_blob(pruning_boundary: u64, blob: u64, items_per_blob: u64) -> Result<u64, Error> {
142 let start = super::blob_first_position(blob, items_per_blob)?;
143 Ok(pruning_boundary.max(start))
144}
145
146fn replay_stream<'a, B: RBlob, A: CodecFixedShared>(
152 blobs: &Blobs<'a, B>,
153 bounds: Range<u64>,
154 items_per_blob: NonZeroU64,
155 start_pos: u64,
156 buffer: NonZeroUsize,
157) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send + use<'a, B, A>, Error> {
158 if start_pos > bounds.end {
159 return Err(Error::ItemOutOfRange(start_pos));
160 }
161 if start_pos < bounds.start {
162 return Err(Error::ItemPruned(start_pos));
163 }
164
165 let mut states = Vec::new();
166 if start_pos < bounds.end {
167 let items_per_blob = items_per_blob.get();
168 let start_blob = super::position_to_blob(start_pos, items_per_blob);
169 let end_blob = super::position_to_blob(bounds.end - 1, items_per_blob);
170 let items_per_batch = (buffer.get() / A::SIZE).max(1);
171
172 for blob in start_blob..=end_blob {
173 let blob_first = first_in_blob(bounds.start, blob, items_per_blob)?;
176 let first_pos = if blob == start_blob {
177 start_pos
178 } else {
179 blob_first
180 };
181 let blob_end = super::blob_end_position(blob, items_per_blob, bounds.end);
182 let offset = (first_pos - blob_first)
183 .checked_mul(A::SIZE as u64)
184 .ok_or(Error::OffsetOverflow)?;
185 let blob = blobs
186 .get(blob)
187 .expect("positions in bounds map to a retained blob");
188
189 states.push(FixedReplayState::<B, A> {
190 replay: blob.replay_from(offset, buffer)?,
191 pos: first_pos,
192 end_pos: blob_end,
193 items_per_batch,
194 _marker: PhantomData,
195 });
196 }
197 }
198
199 Ok(super::replay_stream_from_states(states))
200}
201
202struct FixedReplayState<'a, B: RBlob, A> {
204 replay: BlobReplay<'a, B>,
206 pos: u64,
208 end_pos: u64,
210 items_per_batch: usize,
212 _marker: PhantomData<A>,
213}
214
215impl<B: RBlob, A: CodecFixedShared> super::ReplayBatchState for FixedReplayState<'_, B, A> {
216 type Item = A;
217
218 async fn next_batch(mut self) -> Option<(Vec<Result<(u64, A), Error>>, Self)> {
220 if self.pos == self.end_pos {
221 return None;
222 }
223
224 let mut batch = Vec::new();
227 match self.replay.ensure(A::SIZE).await {
228 Ok(true) => {}
229 Ok(false) => {
230 batch.push(Err(Error::Corruption(format!(
231 "blob ended before position {}",
232 self.pos
233 ))));
234 self.pos = self.end_pos;
235 return Some((batch, self));
236 }
237 Err(err) => {
238 batch.push(Err(err));
239 self.pos = self.end_pos;
240 return Some((batch, self));
241 }
242 }
243
244 let available = (self.replay.remaining() / A::SIZE) as u64;
247 let remaining = self.end_pos - self.pos;
248 let count = available.min(self.items_per_batch as u64).min(remaining) as usize;
249 let Some(next_pos) = self.pos.checked_add(count as u64) else {
250 batch.push(Err(Error::OffsetOverflow));
251 self.pos = self.end_pos;
252 return Some((batch, self));
253 };
254 batch.reserve(count);
255
256 let base = self.pos;
257 for i in 0..count {
258 match A::read(&mut self.replay) {
259 Ok(item) => batch.push(Ok((base + i as u64, item))),
260 Err(err) => {
261 batch.push(Err(Error::Codec(err)));
262 self.pos = self.end_pos;
263 return Some((batch, self));
264 }
265 }
266 }
267 self.pos = next_pos;
268 Some((batch, self))
269 }
270}
271
272enum BlobFill {
274 Full { len: u64 },
275 Short { len: u64 },
276 Overfull { len: u64, capacity: u64 },
277}
278
279struct RecoveredBounds {
282 pruning_boundary: u64,
284 size: u64,
286 recovery_watermark: u64,
288 repair: Option<u64>,
291}
292
293#[derive(Clone)]
295pub struct Config {
296 pub partition: String,
301
302 pub items_per_blob: NonZeroU64,
308
309 pub page_cache: CacheRef,
311
312 pub write_buffer: NonZeroUsize,
314}
315
316pub struct Journal<E: Context, A> {
327 blobs: Writable<E>,
329
330 checkpoint: Checkpoint<E>,
332
333 bounds: Range<u64>,
335
336 dirty_from_blob: Option<u64>,
338
339 items_per_blob: NonZeroU64,
341
342 metrics: Arc<Metrics<E>>,
344
345 _phantom: PhantomData<A>,
346}
347
348impl<E: Context, A: CodecFixedShared> Journal<E, A> {
349 pub const CHUNK_SIZE: NonZeroUsize = match NonZeroUsize::new(A::SIZE) {
352 Some(size) => size,
353 None => panic!("journal item size must be nonzero"),
354 };
355
356 pub const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE.get() as u64;
358
359 fn items_to_bytes(items: u64) -> Result<u64, Error> {
361 items
362 .checked_mul(Self::CHUNK_SIZE_U64)
363 .ok_or(Error::OffsetOverflow)
364 }
365
366 fn mark_dirty_from(&mut self, blob: u64) {
368 self.dirty_from_blob = Some(
369 self.dirty_from_blob
370 .map_or(blob, |existing| existing.min(blob)),
371 );
372 }
373
374 fn from_blobs(
376 blobs: Writable<E>,
377 checkpoint: Checkpoint<E>,
378 bounds: Range<u64>,
379 dirty_from_blob: Option<u64>,
380 items_per_blob: NonZeroU64,
381 metrics: Metrics<E>,
382 ) -> Self {
383 Self {
384 blobs,
385 checkpoint,
386 bounds,
387 dirty_from_blob,
388 items_per_blob,
389 metrics: Arc::new(metrics),
390 _phantom: PhantomData,
391 }
392 }
393
394 pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
399 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
400 Self::init_with_checkpoint(context, cfg, checkpoint).await
401 }
402
403 async fn init_with_checkpoint(
405 context: E,
406 cfg: Config,
407 mut checkpoint: Checkpoint<E>,
408 ) -> Result<Self, Error> {
409 if let Some(clear_target) = checkpoint.clear_target() {
412 return Self::complete_staged_clear(context, cfg, checkpoint, clear_target).await;
413 }
414
415 let blob_partition = Partition::select(&context, &cfg.partition).await?;
416 let partition = Partition::new(
417 context.child("blobs"),
418 blob_partition,
419 cfg.page_cache,
420 cfg.write_buffer,
421 );
422 let mut pending = partition.open_all().await?;
423
424 for (&blob, writer) in &mut pending {
429 let size = writer.size();
430 let valid_size = Self::items_to_bytes(size / Self::CHUNK_SIZE_U64)?;
431 if valid_size != size {
432 warn!(
433 blob,
434 invalid_size = size,
435 new_size = valid_size,
436 "trailing bytes detected: truncating"
437 );
438 writer.resize(valid_size).await.map_err(Error::Runtime)?;
439 writer.sync().await.map_err(Error::Runtime)?;
440 }
441 }
442
443 let RecoveredBounds {
444 pruning_boundary,
445 size,
446 recovery_watermark,
447 repair,
448 } = Self::recover_bounds(
449 &pending,
450 cfg.items_per_blob.get(),
451 checkpoint.boundary_hint(),
452 checkpoint.watermark(),
453 )?;
454
455 checkpoint
458 .persist(
459 cfg.items_per_blob.get(),
460 pruning_boundary,
461 recovery_watermark,
462 )
463 .await?;
464
465 let tail_blob = super::position_to_blob(size, cfg.items_per_blob.get());
469 if let Some(truncate_to) = repair {
470 while let Some((&newest, _)) = pending.last_key_value() {
471 if newest <= tail_blob {
472 break;
473 }
474 drop(pending.remove(&newest));
475 partition.remove(newest).await?;
476 }
477 if let Some(writer) = pending.get_mut(&tail_blob) {
478 if truncate_to < writer.size() {
479 writer.resize(truncate_to).await.map_err(Error::Runtime)?;
480 writer.sync().await.map_err(Error::Runtime)?;
481 }
482 }
483 }
484
485 let blobs = Writable::recover(partition, pending, tail_blob).await?;
487
488 let dirty_from_blob = (recovery_watermark < size)
491 .then(|| super::position_to_blob(recovery_watermark, cfg.items_per_blob.get()));
492
493 let metrics = Metrics::new(context);
494 metrics.update(size, pruning_boundary, cfg.items_per_blob.get());
495
496 Ok(Self::from_blobs(
497 blobs,
498 checkpoint,
499 pruning_boundary..size,
500 dirty_from_blob,
501 cfg.items_per_blob,
502 metrics,
503 ))
504 }
505
506 async fn complete_staged_clear(
509 context: E,
510 cfg: Config,
511 mut checkpoint: Checkpoint<E>,
512 clear_target: u64,
513 ) -> Result<Self, Error> {
514 warn!(clear_target, "crash repair: completing interrupted clear");
515 let new_partition = format!("{}-blobs", cfg.partition);
516 Partition::<E>::remove_all(&context, &cfg.partition).await?;
517 Partition::<E>::remove_all(&context, &new_partition).await?;
518 let partition = Partition::new(
519 context.child("blobs"),
520 new_partition,
521 cfg.page_cache,
522 cfg.write_buffer,
523 );
524 let tail_blob = super::position_to_blob(clear_target, cfg.items_per_blob.get());
525 let blobs = Writable::recover(partition, BTreeMap::new(), tail_blob).await?;
526 checkpoint
527 .finish_clear(cfg.items_per_blob.get(), clear_target)
528 .await?;
529
530 let metrics = Metrics::new(context);
531 metrics.update(clear_target, clear_target, cfg.items_per_blob.get());
532 Ok(Self::from_blobs(
533 blobs,
534 checkpoint,
535 clear_target..clear_target,
536 None,
537 cfg.items_per_blob,
538 metrics,
539 ))
540 }
541
542 fn recover_bounds(
548 pending: &BTreeMap<u64, Writer<E::Blob>>,
549 items_per_blob: u64,
550 boundary_hint: Option<u64>,
551 watermark_hint: Option<u64>,
552 ) -> Result<RecoveredBounds, Error> {
553 let pruning_boundary = Self::recover_pruning_boundary(
554 boundary_hint,
555 pending.keys().next().copied(),
556 items_per_blob,
557 )?;
558
559 let (size, repair) =
560 Self::recover_by_walking_lengths(pending, items_per_blob, pruning_boundary)?;
561
562 let recovery_watermark = match watermark_hint {
563 Some(watermark) if watermark > size => {
564 return Err(Error::Corruption(format!(
568 "recovery watermark {watermark} exceeds recoverable size {size}"
569 )));
570 }
571 Some(watermark) => watermark,
572 None if repair.is_some() => {
573 return Err(Error::Corruption(
576 "legacy journal has a short non-tail blob".into(),
577 ));
578 }
579 None => first_in_blob(
582 pruning_boundary,
583 super::position_to_blob(size, items_per_blob),
584 items_per_blob,
585 )?,
586 };
587
588 Ok(RecoveredBounds {
589 pruning_boundary,
590 size,
591 recovery_watermark,
592 repair,
593 })
594 }
595
596 fn recover_pruning_boundary(
602 boundary_hint: Option<u64>,
603 oldest_blob: Option<u64>,
604 items_per_blob: u64,
605 ) -> Result<u64, Error> {
606 let blob_boundary = match oldest_blob {
607 Some(oldest) => super::blob_first_position(oldest, items_per_blob)?,
608 None => 0,
609 };
610
611 let Some(boundary_hint) = boundary_hint else {
612 return Ok(blob_boundary);
613 };
614 if boundary_hint.is_multiple_of(items_per_blob) {
615 return Ok(blob_boundary);
616 }
617
618 let hint_blob = super::position_to_blob(boundary_hint, items_per_blob);
619 match oldest_blob {
620 Some(oldest_blob) if hint_blob == oldest_blob => Ok(boundary_hint),
621 Some(oldest_blob) if hint_blob < oldest_blob => {
622 warn!(
623 hint_blob,
624 oldest_blob, "crash repair: boundary hint stale, computing from blobs"
625 );
626 Ok(blob_boundary)
627 }
628 Some(oldest_blob) => {
629 Err(Error::Corruption(format!(
632 "boundary hint references blob {hint_blob} \
633 but oldest blob is blob {oldest_blob}"
634 )))
635 }
636 None => {
637 Err(Error::Corruption(format!(
641 "boundary hint references blob {hint_blob} but no blobs exist"
642 )))
643 }
644 }
645 }
646
647 fn classify_fill(
650 pending: &BTreeMap<u64, Writer<E::Blob>>,
651 items_per_blob: u64,
652 pruning_boundary: u64,
653 blob: u64,
654 ) -> Result<BlobFill, Error> {
655 let len = pending
656 .get(&blob)
657 .map_or(0, |writer| writer.size() / Self::CHUNK_SIZE_U64);
658 let start = super::blob_first_position(blob, items_per_blob)?;
661 let skipped = pruning_boundary.saturating_sub(start).min(items_per_blob);
662 let capacity = items_per_blob - skipped;
663 Ok(match len.cmp(&capacity) {
664 std::cmp::Ordering::Less => BlobFill::Short { len },
665 std::cmp::Ordering::Equal => BlobFill::Full { len },
666 std::cmp::Ordering::Greater => BlobFill::Overfull { len, capacity },
667 })
668 }
669
670 fn recover_by_walking_lengths(
677 pending: &BTreeMap<u64, Writer<E::Blob>>,
678 items_per_blob: u64,
679 pruning_boundary: u64,
680 ) -> Result<(u64, Option<u64>), Error> {
681 let oldest = pending.keys().next().copied();
682 let newest = pending.keys().next_back().copied();
683
684 let (Some(oldest), Some(newest)) = (oldest, newest) else {
685 return Ok((pruning_boundary, None));
686 };
687
688 let mut size = pruning_boundary;
689 for blob in oldest..=newest {
690 let fill = Self::classify_fill(pending, items_per_blob, pruning_boundary, blob)?;
691 match fill {
692 BlobFill::Full { len } => {
694 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
695 }
696 BlobFill::Short { len } if blob == newest => {
698 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
699 return Ok((size, None));
700 }
701 BlobFill::Short { len } => {
704 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
705 return Ok((size, Some(Self::items_to_bytes(len)?)));
706 }
707 BlobFill::Overfull { len, capacity } => {
708 return Err(Error::Corruption(format!(
709 "blob {blob} has too many items: expected at most {capacity}, got {len}"
710 )));
711 }
712 }
713 }
714
715 Ok((size, None))
716 }
717
718 #[commonware_macros::stability(ALPHA)]
726 pub async fn init_at_size(context: E, cfg: Config, size: u64) -> Result<Self, Error> {
727 Partition::select(&context, &cfg.partition).await?;
729 Self::init_at_size_cleared(context, cfg, size, || async { Ok(()) }).await
730 }
731
732 #[commonware_macros::stability(ALPHA)]
739 pub(in crate::journal::contiguous) async fn init_at_size_cleared<F, Fut>(
740 context: E,
741 cfg: Config,
742 size: u64,
743 clear_dependents: F,
744 ) -> Result<Self, Error>
745 where
746 F: FnOnce() -> Fut,
747 Fut: Future<Output = Result<(), Error>>,
748 {
749 if size == u64::MAX {
752 return Err(Error::SizeOverflow);
753 }
754
755 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
758 checkpoint.stage_clear(size).await?;
759 clear_dependents().await?;
760 Self::init_with_checkpoint(context, cfg, checkpoint).await
761 }
762
763 pub(in crate::journal::contiguous) async fn init_cleared<F, Fut>(
770 context: E,
771 cfg: Config,
772 clear_dependents: F,
773 ) -> Result<Self, Error>
774 where
775 F: FnOnce() -> Fut,
776 Fut: Future<Output = Result<(), Error>>,
777 {
778 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
779 if checkpoint.clear_target().is_some() {
780 clear_dependents().await?;
781 }
782 Self::init_with_checkpoint(context, cfg, checkpoint).await
783 }
784
785 async fn flush_dirty_blobs(&mut self) -> Result<(), Error> {
787 let Some(start_blob) = self.dirty_from_blob else {
788 return Ok(());
789 };
790 self.blobs.sync_from(start_blob).await
791 }
792
793 pub async fn commit(&mut self) -> Result<(), Error> {
799 let _timer = self.metrics.commit_timer();
800 self.metrics.commit_calls.inc();
801 self.flush_dirty_blobs().await?;
802 self.dirty_from_blob = None;
803 Ok(())
804 }
805
806 pub async fn sync(&mut self) -> Result<(), Error> {
811 let _timer = self.metrics.sync_timer();
812 self.metrics.sync_calls.inc();
813 self.flush_dirty_blobs().await?;
814 self.dirty_from_blob = None;
815 self.checkpoint
816 .persist(
817 self.items_per_blob.get(),
818 self.bounds.start,
819 self.bounds.end,
820 )
821 .await
822 }
823
824 pub async fn snapshot(&mut self) -> Result<Reader<'static, E, A>, Error> {
830 Ok(Reader {
831 blobs: self.blobs.snapshot().await?,
832 bounds: self.bounds.clone(),
833 items_per_blob: self.items_per_blob,
834 metrics: self.metrics.clone(),
835 _phantom: PhantomData,
836 })
837 }
838
839 pub(super) fn reader(&self) -> Reader<'_, E, A> {
841 Reader {
842 blobs: self.blobs.reader(),
843 bounds: self.bounds.clone(),
844 items_per_blob: self.items_per_blob,
845 metrics: self.metrics.clone(),
846 _phantom: PhantomData,
847 }
848 }
849
850 pub(super) fn recovery_watermark(&self) -> u64 {
852 self.checkpoint
853 .watermark()
854 .expect("recovery watermark must exist after init")
855 }
856
857 pub const fn size(&self) -> u64 {
860 self.bounds.end
861 }
862
863 pub async fn append(&mut self, item: &A) -> Result<u64, Error> {
869 let _timer = self.metrics.append_timer();
870 self.metrics.append_calls.inc();
871 self.append_many_inner(Many::Flat(std::slice::from_ref(item)))
872 .await
873 }
874
875 pub async fn append_many<'a>(&'a mut self, items: Many<'a, A>) -> Result<u64, Error> {
879 let _timer = self.metrics.append_many_timer();
880 self.metrics.append_many_calls.inc();
881 self.append_many_inner(items).await
882 }
883
884 async fn append_many_inner<'a>(&'a mut self, items: Many<'a, A>) -> Result<u64, Error> {
886 let prepared = self.prepare_append(items);
887 self.write_encoded(prepared).await
888 }
889
890 pub fn prepare_append(&self, items: Many<'_, A>) -> PreparedAppend<A> {
895 let mut buf = Vec::with_capacity(items.len() * A::SIZE);
898 match items {
899 Many::Flat(items) => {
900 for item in items {
901 item.write(&mut buf);
902 }
903 }
904 Many::Nested(nested_items) => {
905 for items in nested_items {
906 for item in *items {
907 item.write(&mut buf);
908 }
909 }
910 }
911 }
912 PreparedAppend {
913 buf,
914 _marker: PhantomData,
915 }
916 }
917
918 pub async fn append_prepared(&mut self, prepared: PreparedAppend<A>) -> Result<u64, Error> {
923 let _timer = self.metrics.append_prepared_timer();
924 self.metrics.append_prepared_calls.inc();
925 self.write_encoded(prepared).await
926 }
927
928 async fn write_encoded(&mut self, prepared: PreparedAppend<A>) -> Result<u64, Error> {
930 let items_buf = prepared.buf;
931 let items_count = items_buf.len() / A::SIZE;
932 if items_count == 0 {
933 return Err(Error::EmptyAppend);
934 }
935 let items_buf = IoBuf::from(items_buf);
936
937 self.bounds
940 .end
941 .checked_add(items_count as u64)
942 .ok_or(Error::SizeOverflow)?;
943
944 let first_dirty_blob = super::position_to_blob(self.bounds.end, self.items_per_blob.get());
945 self.mark_dirty_from(first_dirty_blob);
946 let mut written = 0;
947 while written < items_count {
948 let batch_count = super::batch_count_to_blob_boundary(
949 self.bounds.end,
950 items_count - written,
951 self.items_per_blob.get(),
952 );
953 let start = written * A::SIZE;
954 let end = start + batch_count * A::SIZE;
955 let new_size = self.bounds.end + batch_count as u64;
957
958 self.blobs
959 .tail_writer()
960 .append_owned(items_buf.slice(start..end))
961 .await
962 .map_err(Error::Runtime)?;
963 self.bounds.end = new_size;
964 written += batch_count;
965
966 if new_size.is_multiple_of(self.items_per_blob.get()) {
969 self.blobs.seal_tail().await?;
970 }
971 }
972
973 self.metrics.update(
974 self.bounds.end,
975 self.bounds.start,
976 self.items_per_blob.get(),
977 );
978 Ok(self.bounds.end - 1)
979 }
980
981 pub async fn rewind(&mut self, size: u64) -> Result<(), Error> {
994 match size.cmp(&self.bounds.end) {
995 std::cmp::Ordering::Greater => return Err(Error::InvalidRewind(size)),
996 std::cmp::Ordering::Equal => return Ok(()),
997 std::cmp::Ordering::Less => {}
998 }
999
1000 if size < self.bounds.start {
1001 return Err(Error::ItemPruned(size));
1002 }
1003
1004 let blob = super::position_to_blob(size, self.items_per_blob.get());
1005 let pos_in_blob = size - first_in_blob(self.bounds.start, blob, self.items_per_blob.get())?;
1006 let byte_offset = Self::items_to_bytes(pos_in_blob)?;
1007
1008 if self.checkpoint.lower_watermark(size) {
1010 self.checkpoint.sync().await?;
1011 }
1012
1013 if blob == self.blobs.tail_blob_index() {
1014 self.blobs.rewind_tail(byte_offset).await?;
1015 } else {
1016 self.blobs.rewind_into_sealed(blob, byte_offset).await?;
1017 }
1018
1019 self.bounds.end = size;
1020 self.mark_dirty_from(blob);
1021 self.metrics.update(
1022 self.bounds.end,
1023 self.bounds.start,
1024 self.items_per_blob.get(),
1025 );
1026
1027 Ok(())
1028 }
1029
1030 pub const fn pruning_boundary(&self) -> u64 {
1032 self.bounds.start
1033 }
1034
1035 pub async fn prune(&mut self, min_item_pos: u64) -> Result<bool, Error> {
1045 let target_blob = super::position_to_blob(min_item_pos, self.items_per_blob.get());
1048 let tail_blob = super::position_to_blob(self.bounds.end, self.items_per_blob.get());
1049 let min_blob = std::cmp::min(target_blob, tail_blob);
1050
1051 if min_blob <= self.blobs.oldest_blob_index() {
1052 return Ok(false);
1053 }
1054
1055 self.flush_dirty_blobs().await?;
1063 self.dirty_from_blob = None;
1064
1065 let new_boundary = super::blob_first_position(min_blob, self.items_per_blob.get())?;
1066 self.blobs.prune(min_blob).await?;
1067 self.bounds.start = new_boundary;
1068
1069 self.metrics.update(
1070 self.bounds.end,
1071 self.bounds.start,
1072 self.items_per_blob.get(),
1073 );
1074
1075 Ok(true)
1076 }
1077
1078 pub async fn destroy(self) -> Result<(), Error> {
1086 self.blobs.destroy().await?;
1087 self.checkpoint.destroy().await?;
1088 Ok(())
1089 }
1090
1091 pub(crate) async fn clear_to_size(&mut self, new_size: u64) -> Result<(), Error> {
1101 if new_size == u64::MAX {
1103 return Err(Error::SizeOverflow);
1104 }
1105
1106 self.checkpoint.stage_clear(new_size).await?;
1108
1109 self.blobs
1111 .clear(super::position_to_blob(new_size, self.items_per_blob.get()))
1112 .await?;
1113 self.bounds = new_size..new_size;
1114 self.dirty_from_blob = None;
1115
1116 self.checkpoint
1118 .finish_clear(self.items_per_blob.get(), new_size)
1119 .await?;
1120
1121 self.metrics.update(
1122 self.bounds.end,
1123 self.bounds.start,
1124 self.items_per_blob.get(),
1125 );
1126 Ok(())
1127 }
1128
1129 #[commonware_macros::stability(ALPHA)]
1136 pub(super) async fn stage_clear_intent(&mut self, new_size: u64) -> Result<(), Error> {
1137 if new_size == u64::MAX {
1139 return Err(Error::SizeOverflow);
1140 }
1141 self.checkpoint.stage_clear(new_size).await
1142 }
1143}
1144
1145pub struct Reader<'a, E: Context, A> {
1147 blobs: Blobs<'a, E::Blob>,
1148 bounds: Range<u64>,
1149 items_per_blob: NonZeroU64,
1150 metrics: Arc<Metrics<E>>,
1151 _phantom: PhantomData<A>,
1152}
1153
1154impl<E: Context, A: CodecFixedShared> Reader<'_, E, A> {
1155 const fn validate_readable(&self, pos: u64) -> Result<(), Error> {
1157 if pos >= self.bounds.end {
1158 return Err(Error::ItemOutOfRange(pos));
1159 }
1160 if pos < self.bounds.start {
1161 return Err(Error::ItemPruned(pos));
1162 }
1163 Ok(())
1164 }
1165
1166 fn locate_group(&self, group: &[u64]) -> Result<(u64, Vec<u64>), Error> {
1169 let items_per_blob = self.items_per_blob.get();
1170 let blob = super::position_to_blob(group[0], items_per_blob);
1171 let first_position = first_in_blob(self.bounds.start, blob, items_per_blob)?;
1172 let offsets = group
1173 .iter()
1174 .map(|&pos| Journal::<E, A>::items_to_bytes(pos - first_position))
1175 .collect::<Result<Vec<u64>, _>>()?;
1176 Ok((blob, offsets))
1177 }
1178
1179 pub(super) async fn read_many_inner(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1183 if positions.is_empty() {
1184 return Ok(Vec::new());
1185 }
1186 assert!(
1187 positions.is_sorted_by(|a, b| a < b),
1188 "positions must be strictly increasing"
1189 );
1190 for &pos in positions {
1191 self.validate_readable(pos)?;
1192 }
1193
1194 let items_per_blob = self.items_per_blob.get();
1195
1196 let mut result: Vec<A> = Vec::with_capacity(positions.len());
1201 let mut reusable_buf = vec![0u8; positions.len() * A::SIZE];
1202
1203 let mut reads = Vec::new();
1206 let mut remaining_buf = reusable_buf.as_mut_slice();
1207 for group in positions.chunk_by(|a, b| {
1208 super::position_to_blob(*a, items_per_blob)
1209 == super::position_to_blob(*b, items_per_blob)
1210 }) {
1211 let (blob_num, blob_offsets) = self.locate_group(group)?;
1212 let blob = self
1213 .blobs
1214 .get(blob_num)
1215 .expect("positions in bounds map to a retained blob");
1216 let (buf, rest) = remaining_buf.split_at_mut(group.len() * A::SIZE);
1217 remaining_buf = rest;
1218 reads.push(async move {
1219 blob.read_many_into(buf, &blob_offsets, Journal::<E, A>::CHUNK_SIZE)
1220 .await
1221 });
1222 }
1223 let hits: u64 = try_join_all(reads)
1224 .await?
1225 .into_iter()
1226 .map(|group_hits| group_hits as u64)
1227 .sum();
1228
1229 for slice in reusable_buf.chunks_exact(A::SIZE) {
1230 result.push(A::decode(slice).map_err(Error::Codec)?);
1231 }
1232
1233 self.metrics.cache_hits.inc_by(hits);
1234 self.metrics
1235 .cache_misses
1236 .inc_by(positions.len() as u64 - hits);
1237 self.metrics.items_read.inc_by(positions.len() as u64);
1238 Ok(result)
1239 }
1240
1241 fn locate(&self, pos: u64) -> Result<(Blob<'_, E::Blob>, u64), Error> {
1243 self.validate_readable(pos)?;
1244 let items_per_blob = self.items_per_blob.get();
1245 let blob = super::position_to_blob(pos, items_per_blob);
1246 let pos_in_blob = pos - first_in_blob(self.bounds.start, blob, items_per_blob)?;
1247 let offset = Journal::<E, A>::items_to_bytes(pos_in_blob)?;
1248 let blob = self
1249 .blobs
1250 .get(blob)
1251 .expect("position in bounds maps to a retained blob");
1252 Ok((blob, offset))
1253 }
1254
1255 pub(super) fn probe_items(&self, positions: &[u64]) -> Vec<Option<A>> {
1259 assert!(
1260 positions.is_sorted_by(|a, b| a < b),
1261 "positions must be strictly increasing"
1262 );
1263 let mut out: Vec<Option<A>> = (0..positions.len()).map(|_| None).collect();
1264
1265 let start = positions.partition_point(|&pos| pos < self.bounds.start);
1269 let end = positions.partition_point(|&pos| pos < self.bounds.end);
1270 let valid = &positions[start..end];
1271 if valid.is_empty() {
1272 return out;
1273 }
1274
1275 let items_per_blob = self.items_per_blob.get();
1278 let mut scratch =
1279 Cached::take(&PROBE_SCRATCH, || Ok::<_, ()>(Vec::new()), |_| Ok(())).unwrap();
1280 let need = valid.len() * A::SIZE;
1281 if scratch.len() < need {
1282 scratch.resize(need, 0);
1283 }
1284 let buf = &mut scratch[..need];
1285 let mut hits = 0u64;
1286 let mut group_base = start;
1287 for group in valid.chunk_by(|a, b| {
1288 super::position_to_blob(*a, items_per_blob)
1289 == super::position_to_blob(*b, items_per_blob)
1290 }) {
1291 let base = group_base;
1292 group_base += group.len();
1293 let Ok((blob_num, blob_offsets)) = self.locate_group(group) else {
1294 continue;
1295 };
1296 let Some(blob) = self.blobs.get(blob_num) else {
1297 continue;
1298 };
1299 let buf = &mut buf[..group.len() * A::SIZE];
1300 let misses =
1301 blob.try_read_many_sync_into(buf, &blob_offsets, Journal::<E, A>::CHUNK_SIZE);
1302 let mut misses = misses.into_iter().peekable();
1303 for (idx, slice) in buf.chunks_exact(A::SIZE).enumerate() {
1304 if misses.peek() == Some(&idx) {
1305 misses.next();
1306 continue;
1307 }
1308 if let Ok(item) = A::decode(slice) {
1311 out[base + idx] = Some(item);
1312 hits += 1;
1313 }
1314 }
1315 }
1316 self.metrics.cache_hits.inc_by(hits);
1317 self.metrics.items_read.inc_by(hits);
1318 out
1319 }
1320}
1321
1322impl<E: Context, A: CodecFixedShared> super::Contiguous for Reader<'_, E, A> {
1323 type Item = A;
1324
1325 fn bounds(&self) -> Range<u64> {
1326 self.bounds.clone()
1327 }
1328
1329 async fn read(&self, pos: u64) -> Result<A, Error> {
1330 self.metrics.read_calls.inc();
1331
1332 if let Some(item) = self.try_read_sync(pos) {
1334 return Ok(item);
1335 }
1336
1337 let _timer = self.metrics.read_timer();
1338 let (blob, offset) = self.locate(pos)?;
1339 self.metrics.cache_misses.inc();
1340 let bufs = blob.read_at(offset, A::SIZE).await?;
1341 let item = A::decode(bufs.coalesce()).map_err(Error::Codec)?;
1342 self.metrics.items_read.inc();
1343 Ok(item)
1344 }
1345
1346 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1347 if positions.is_empty() {
1348 return Ok(Vec::new());
1349 }
1350 let _timer = self.metrics.read_many_timer();
1351 self.metrics.read_many_calls.inc();
1352 self.read_many_inner(positions).await
1353 }
1354
1355 fn try_read_sync(&self, pos: u64) -> Option<A> {
1356 let mut buf = vec![0u8; A::SIZE];
1357 let item = match self.locate(pos) {
1358 Ok((blob, offset)) if blob.try_read_sync_into(&mut buf, offset) => {
1359 A::decode(&buf[..]).ok()
1360 }
1361 _ => None,
1362 };
1363 if item.is_some() {
1364 self.metrics.cache_hits.inc();
1365 self.metrics.items_read.inc();
1366 }
1367 item
1368 }
1369
1370 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
1371 self.probe_items(positions)
1372 }
1373
1374 async fn replay(
1375 &self,
1376 start_pos: u64,
1377 buffer: NonZeroUsize,
1378 ) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send, Error> {
1379 replay_stream(
1380 &self.blobs,
1381 self.bounds.clone(),
1382 self.items_per_blob,
1383 start_pos,
1384 buffer,
1385 )
1386 }
1387}
1388
1389impl<E: Context, A: CodecFixedShared> super::Contiguous for Journal<E, A> {
1390 type Item = A;
1391
1392 fn bounds(&self) -> Range<u64> {
1393 self.bounds.clone()
1394 }
1395
1396 async fn read(&self, pos: u64) -> Result<A, Error> {
1397 self.reader().read(pos).await
1398 }
1399
1400 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1401 self.reader().read_many(positions).await
1402 }
1403
1404 fn try_read_sync(&self, pos: u64) -> Option<A> {
1405 self.reader().try_read_sync(pos)
1406 }
1407
1408 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
1409 self.reader().probe_items(positions)
1410 }
1411
1412 async fn replay(
1413 &self,
1414 start_pos: u64,
1415 buffer: NonZeroUsize,
1416 ) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send, Error> {
1417 let blobs = self.blobs.reader();
1418 replay_stream(
1419 &blobs,
1420 self.bounds.clone(),
1421 self.items_per_blob,
1422 start_pos,
1423 buffer,
1424 )
1425 }
1426}
1427
1428impl<E: Context, A: CodecFixedShared> Mutable for Journal<E, A> {
1429 async fn append(&mut self, item: &Self::Item) -> Result<u64, Error> {
1430 Self::append(self, item).await
1431 }
1432
1433 async fn append_many<'a>(&'a mut self, items: Many<'a, Self::Item>) -> Result<u64, Error> {
1434 Self::append_many(self, items).await
1435 }
1436
1437 async fn prune(&mut self, min_position: u64) -> Result<bool, Error> {
1438 Self::prune(self, min_position).await
1439 }
1440
1441 async fn rewind(&mut self, size: u64) -> Result<(), Error> {
1442 Self::rewind(self, size).await
1443 }
1444
1445 async fn commit(&mut self) -> Result<(), Error> {
1446 Self::commit(self).await
1447 }
1448
1449 async fn sync(&mut self) -> Result<(), Error> {
1450 Self::sync(self).await
1451 }
1452
1453 async fn destroy(self) -> Result<(), Error> {
1454 Self::destroy(self).await
1455 }
1456}
1457
1458#[commonware_macros::stability(ALPHA)]
1459impl<E: Context, A: CodecFixedShared> authenticated::Inner<E> for Journal<E, A> {
1460 type Config = Config;
1461
1462 async fn init<
1463 F: merkle::Family,
1464 H: commonware_cryptography::Hasher,
1465 S: commonware_parallel::Strategy,
1466 >(
1467 context: E,
1468 merkle_cfg: merkle::full::Config<S>,
1469 journal_cfg: Self::Config,
1470 rewind_predicate: fn(&A) -> bool,
1471 bagging: merkle::Bagging,
1472 ) -> Result<authenticated::Journal<F, E, Self, H, S>, authenticated::Error<F>> {
1473 authenticated::Journal::<F, E, Self, H, S>::new(
1474 context,
1475 merkle_cfg,
1476 journal_cfg,
1477 rewind_predicate,
1478 bagging,
1479 )
1480 .await
1481 }
1482}
1483
1484#[cfg(test)]
1485mod tests {
1486 use super::*;
1487 use crate::journal::contiguous::Contiguous as _;
1488 use commonware_codec::FixedSize;
1489 use commonware_cryptography::{sha256::Digest, Hasher as _, Sha256};
1490 use commonware_macros::test_traced;
1491 use commonware_runtime::{
1492 buffer::paged::Writer,
1493 deterministic::{self, Context},
1494 Blob, BufferPooler, Error as RuntimeError, Metrics as _, Runner, Spawner as _, Storage,
1495 Supervisor as _,
1496 };
1497 use commonware_utils::{NZUsize, NZU16, NZU64};
1498 use futures::{pin_mut, StreamExt};
1499 use std::num::NonZeroU16;
1500
1501 const PAGE_SIZE: NonZeroU16 = NZU16!(44);
1502 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
1503
1504 fn test_digest(value: u64) -> Digest {
1506 Sha256::hash(&value.to_be_bytes())
1507 }
1508
1509 fn test_cfg(pooler: &impl BufferPooler, items_per_blob: NonZeroU64) -> Config {
1510 Config {
1511 partition: "test-partition".into(),
1512 items_per_blob,
1513 page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
1514 write_buffer: NZUsize!(2048),
1515 }
1516 }
1517
1518 fn blob_partition(cfg: &Config) -> String {
1519 format!("{}-blobs", cfg.partition)
1520 }
1521
1522 fn counter(buffer: &str, name: &str) -> u64 {
1524 buffer
1525 .lines()
1526 .find(|l| l.contains(name) && !l.starts_with('#'))
1527 .and_then(|l| l.split_whitespace().last())
1528 .and_then(|v| v.parse().ok())
1529 .expect("counter missing")
1530 }
1531
1532 impl<E: crate::Context, A: CodecFixedShared> Journal<E, A> {
1533 pub(crate) const fn test_oldest_blob(&self) -> Option<u64> {
1535 Some(self.blobs.oldest_blob_index())
1536 }
1537
1538 pub(crate) fn test_newest_blob(&self) -> Option<u64> {
1540 Some(self.blobs.tail_blob_index())
1541 }
1542
1543 pub(crate) async fn test_sync_blob(&mut self, blob: u64) -> Result<(), Error> {
1545 self.blobs.sync_blob(blob).await
1546 }
1547
1548 pub(crate) async fn test_set_recovery_watermark(
1550 &mut self,
1551 watermark: u64,
1552 ) -> Result<(), Error> {
1553 self.checkpoint.set_watermark(Some(watermark));
1554 self.checkpoint.sync().await
1555 }
1556
1557 pub(crate) async fn test_stage_clear(
1559 context: E,
1560 partition: &str,
1561 target: u64,
1562 ) -> Result<(), Error> {
1563 let mut checkpoint = Checkpoint::open(context, partition).await?;
1564 checkpoint.stage_clear(target).await
1565 }
1566 }
1567
1568 #[test_traced]
1569 fn test_fixed_init_marks_suffix_past_recovery_watermark_dirty() {
1570 let executor = deterministic::Runner::default();
1571 executor.start(|context| async move {
1572 let mut cfg = test_cfg(&context, NZU64!(10));
1573 cfg.partition = "init-adopted-fixed".into();
1574
1575 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1576 .await
1577 .unwrap();
1578 journal.append(&1).await.unwrap();
1579 journal.append(&2).await.unwrap();
1580 journal.sync().await.unwrap();
1581 journal.test_set_recovery_watermark(1).await.unwrap();
1584 drop(journal);
1585
1586 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
1587 .await
1588 .unwrap();
1589 assert_eq!(journal.size(), 2);
1590
1591 *context.storage_fault_config().write() = deterministic::FaultConfig {
1596 sync_rate: Some(1.0),
1597 ..Default::default()
1598 };
1599 assert!(
1600 journal.commit().await.is_err(),
1601 "commit() must sync recovered data beyond the persisted recovery watermark"
1602 );
1603 });
1604 }
1605
1606 async fn scan_partition(context: &Context, partition: &str) -> Vec<Vec<u8>> {
1607 match context.scan(partition).await {
1608 Ok(blobs) => blobs,
1609 Err(RuntimeError::PartitionMissing(_)) => Vec::new(),
1610 Err(err) => panic!("Failed to scan partition {partition}: {err}"),
1611 }
1612 }
1613
1614 #[test_traced]
1615 fn test_fixed_journal_init_conflicting_partitions() {
1616 let executor = deterministic::Runner::default();
1617 executor.start(|context| async move {
1618 let cfg = test_cfg(&context, NZU64!(2));
1619 let legacy_partition = cfg.partition.clone();
1620 let blobs_partition = blob_partition(&cfg);
1621
1622 let (legacy_blob, _) = context
1623 .open(&legacy_partition, &0u64.to_be_bytes())
1624 .await
1625 .expect("Failed to open legacy blob");
1626 legacy_blob
1627 .write_at_sync(0, vec![0u8; 1])
1628 .await
1629 .expect("Failed to write legacy blob");
1630
1631 let (new_blob, _) = context
1632 .open(&blobs_partition, &0u64.to_be_bytes())
1633 .await
1634 .expect("Failed to open new blob");
1635 new_blob
1636 .write_at_sync(0, vec![0u8; 1])
1637 .await
1638 .expect("Failed to write new blob");
1639
1640 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
1641 assert!(matches!(result, Err(Error::Corruption(_))));
1642 });
1643 }
1644
1645 #[test_traced]
1646 fn test_fixed_journal_init_prefers_legacy_partition() {
1647 let executor = deterministic::Runner::default();
1648 executor.start(|context| async move {
1649 let cfg = test_cfg(&context, NZU64!(2));
1650 let legacy_partition = cfg.partition.clone();
1651 let blobs_partition = blob_partition(&cfg);
1652
1653 let (legacy_blob, _) = context
1655 .open(&legacy_partition, &0u64.to_be_bytes())
1656 .await
1657 .expect("Failed to open legacy blob");
1658 legacy_blob
1659 .write_at_sync(0, vec![0u8; 1])
1660 .await
1661 .expect("Failed to write legacy blob");
1662
1663 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
1664 .await
1665 .expect("failed to initialize journal");
1666 journal.append(&test_digest(1)).await.unwrap();
1667 journal.sync().await.unwrap();
1668 drop(journal);
1669
1670 let legacy_blobs = scan_partition(&context, &legacy_partition).await;
1671 let new_blobs = scan_partition(&context, &blobs_partition).await;
1672 assert!(!legacy_blobs.is_empty());
1673 assert!(new_blobs.is_empty());
1674 });
1675 }
1676
1677 #[test_traced]
1678 fn test_fixed_journal_init_defaults_to_blobs_partition() {
1679 let executor = deterministic::Runner::default();
1680 executor.start(|context| async move {
1681 let cfg = test_cfg(&context, NZU64!(2));
1682 let legacy_partition = cfg.partition.clone();
1683 let blobs_partition = blob_partition(&cfg);
1684
1685 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
1686 .await
1687 .expect("failed to initialize journal");
1688 journal.append(&test_digest(1)).await.unwrap();
1689 journal.sync().await.unwrap();
1690 drop(journal);
1691
1692 let legacy_blobs = scan_partition(&context, &legacy_partition).await;
1693 let new_blobs = scan_partition(&context, &blobs_partition).await;
1694 assert!(legacy_blobs.is_empty());
1695 assert!(!new_blobs.is_empty());
1696 });
1697 }
1698
1699 #[test_traced]
1700 fn test_fixed_journal_append_and_prune() {
1701 let executor = deterministic::Runner::default();
1703
1704 executor.start(|context| async move {
1706 let cfg = test_cfg(&context, NZU64!(2));
1708 let mut journal = Journal::init(context.child("first"), cfg.clone())
1709 .await
1710 .expect("failed to initialize journal");
1711
1712 let mut pos = journal
1714 .append(&test_digest(0))
1715 .await
1716 .expect("failed to append data 0");
1717 assert_eq!(pos, 0);
1718
1719 journal.sync().await.expect("Failed to sync journal");
1721 drop(journal);
1722
1723 let cfg = test_cfg(&context, NZU64!(2));
1724 let mut journal = Journal::init(context.child("second"), cfg.clone())
1725 .await
1726 .expect("failed to re-initialize journal");
1727 assert_eq!(journal.size(), 1);
1728
1729 pos = journal
1731 .append(&test_digest(1))
1732 .await
1733 .expect("failed to append data 1");
1734 assert_eq!(pos, 1);
1735 pos = journal
1736 .append(&test_digest(2))
1737 .await
1738 .expect("failed to append data 2");
1739 assert_eq!(pos, 2);
1740
1741 let item0 = journal.read(0).await.expect("failed to read data 0");
1743 assert_eq!(item0, test_digest(0));
1744 let item1 = journal.read(1).await.expect("failed to read data 1");
1745 assert_eq!(item1, test_digest(1));
1746 let item2 = journal.read(2).await.expect("failed to read data 2");
1747 assert_eq!(item2, test_digest(2));
1748 let err = journal.read(3).await.expect_err("expected read to fail");
1749 assert!(matches!(err, Error::ItemOutOfRange(3)));
1750
1751 journal.sync().await.expect("failed to sync journal");
1753
1754 journal.prune(1).await.expect("failed to prune journal 1");
1756
1757 journal.prune(2).await.expect("failed to prune journal 2");
1759 assert_eq!(journal.bounds().start, 2);
1760
1761 let result0 = journal.read(0).await;
1763 assert!(matches!(result0, Err(Error::ItemPruned(0))));
1764 let result1 = journal.read(1).await;
1765 assert!(matches!(result1, Err(Error::ItemPruned(1))));
1766
1767 let result2 = journal.read(2).await.unwrap();
1769 assert_eq!(result2, test_digest(2));
1770
1771 for i in 3..10 {
1773 let pos = journal
1774 .append(&test_digest(i))
1775 .await
1776 .expect("failed to append data");
1777 assert_eq!(pos, i);
1778 }
1779
1780 journal.prune(0).await.expect("no-op pruning failed");
1782 assert_eq!(journal.test_oldest_blob(), Some(1));
1783 assert_eq!(journal.test_newest_blob(), Some(5));
1784 assert_eq!(journal.bounds().start, 2);
1785
1786 journal
1788 .prune(3 * cfg.items_per_blob.get())
1789 .await
1790 .expect("failed to prune journal 2");
1791 assert_eq!(journal.test_oldest_blob(), Some(3));
1792 assert_eq!(journal.test_newest_blob(), Some(5));
1793 assert_eq!(journal.bounds().start, 6);
1794
1795 journal
1797 .prune(10000)
1798 .await
1799 .expect("failed to max-prune journal");
1800 let size = journal.size();
1801 assert_eq!(size, 10);
1802 assert_eq!(journal.test_oldest_blob(), Some(5));
1803 assert_eq!(journal.test_newest_blob(), Some(5));
1804 let bounds = journal.bounds();
1807 assert!(bounds.is_empty());
1808 assert_eq!(bounds.start, size);
1810
1811 {
1813 let reader = journal.snapshot().await.unwrap();
1814 let result = reader.replay(0, NZUsize!(1024)).await;
1815 assert!(matches!(result, Err(Error::ItemPruned(0))));
1816 }
1817
1818 {
1820 let reader = journal.snapshot().await.unwrap();
1821 let res = reader.replay(0, NZUsize!(1024)).await;
1822 assert!(matches!(res, Err(Error::ItemPruned(_))));
1823
1824 let reader = journal.snapshot().await.unwrap();
1825 let stream = reader
1826 .replay(journal.bounds().start, NZUsize!(1024))
1827 .await
1828 .expect("failed to replay journal from pruning boundary");
1829 pin_mut!(stream);
1830 let mut items = Vec::new();
1831 while let Some(result) = stream.next().await {
1832 match result {
1833 Ok((pos, item)) => {
1834 assert_eq!(test_digest(pos), item);
1835 items.push(pos);
1836 }
1837 Err(err) => panic!("Failed to read item: {err}"),
1838 }
1839 }
1840 assert_eq!(items, Vec::<u64>::new());
1841 }
1842
1843 journal.destroy().await.unwrap();
1844 });
1845 }
1846
1847 #[test_traced]
1849 fn test_fixed_journal_append_a_lot_of_data() {
1850 let executor = deterministic::Runner::default();
1852 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(10000);
1853 executor.start(|context| async move {
1854 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
1855 let mut journal = Journal::init(context.child("first"), cfg.clone())
1856 .await
1857 .expect("failed to initialize journal");
1858 for i in 0u64..ITEMS_PER_BLOB.get() * 2 - 1 {
1860 journal
1861 .append(&test_digest(i))
1862 .await
1863 .expect("failed to append data");
1864 }
1865 journal.sync().await.expect("failed to sync journal");
1867 drop(journal);
1868 let journal = Journal::init(context.child("second"), cfg.clone())
1869 .await
1870 .expect("failed to re-initialize journal");
1871 for i in 0u64..10000 {
1872 let item: Digest = journal.read(i).await.expect("failed to read data");
1873 assert_eq!(item, test_digest(i));
1874 }
1875 journal.destroy().await.expect("failed to destroy journal");
1876 });
1877 }
1878
1879 #[test_traced]
1880 fn test_fixed_journal_replay() {
1881 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
1882 let executor = deterministic::Runner::default();
1884
1885 executor.start(|context| async move {
1887 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
1889 let mut journal = Journal::init(context.child("first"), cfg.clone())
1890 .await
1891 .expect("failed to initialize journal");
1892
1893 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
1895 let pos = journal
1896 .append(&test_digest(i))
1897 .await
1898 .expect("failed to append data");
1899 assert_eq!(pos, i);
1900 }
1901
1902 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
1904 let item: Digest = journal.read(i).await.expect("failed to read data");
1905 assert_eq!(item, test_digest(i), "i={i}");
1906 }
1907
1908 {
1910 let reader = journal.snapshot().await.unwrap();
1911 let stream = reader
1912 .replay(0, NZUsize!(1024))
1913 .await
1914 .expect("failed to replay journal");
1915 let mut items = Vec::new();
1916 pin_mut!(stream);
1917 while let Some(result) = stream.next().await {
1918 match result {
1919 Ok((pos, item)) => {
1920 assert_eq!(test_digest(pos), item, "pos={pos}, item={item:?}");
1921 items.push(pos);
1922 }
1923 Err(err) => panic!("Failed to read item: {err}"),
1924 }
1925 }
1926
1927 assert_eq!(
1929 items.len(),
1930 ITEMS_PER_BLOB.get() as usize * 100 + ITEMS_PER_BLOB.get() as usize / 2
1931 );
1932 items.sort();
1933 for (i, pos) in items.iter().enumerate() {
1934 assert_eq!(i as u64, *pos);
1935 }
1936 }
1937
1938 journal.sync().await.expect("Failed to sync journal");
1939 drop(journal);
1940
1941 let (blob, _) = context
1943 .open(&blob_partition(&cfg), &40u64.to_be_bytes())
1944 .await
1945 .expect("Failed to open blob");
1946 let bad_bytes = 123456789u32;
1948 blob.write_at_sync(1, bad_bytes.to_be_bytes().to_vec())
1949 .await
1950 .expect("Failed to write bad bytes");
1951
1952 let mut journal = Journal::init(context.child("second"), cfg.clone())
1954 .await
1955 .expect("Failed to re-initialize journal");
1956
1957 let err = journal
1959 .read(40 * ITEMS_PER_BLOB.get() + 1)
1960 .await
1961 .unwrap_err();
1962 assert!(matches!(err, Error::Runtime(_)));
1963
1964 {
1966 let mut error_found = false;
1967 let reader = journal.snapshot().await.unwrap();
1968 let stream = reader
1969 .replay(0, NZUsize!(1024))
1970 .await
1971 .expect("failed to replay journal");
1972 let mut items = Vec::new();
1973 pin_mut!(stream);
1974 while let Some(result) = stream.next().await {
1975 match result {
1976 Ok((pos, item)) => {
1977 assert_eq!(test_digest(pos), item);
1978 items.push(pos);
1979 }
1980 Err(err) => {
1981 error_found = true;
1982 assert!(matches!(err, Error::Runtime(_)));
1983 assert!(stream.next().await.is_none());
1984 break;
1985 }
1986 }
1987 }
1988 assert!(error_found); }
1990 });
1991 }
1992
1993 #[test_traced]
1994 fn test_fixed_replay_stops_after_error() {
1995 let executor = deterministic::Runner::default();
1996 executor.start(|context| async move {
1997 let cfg = test_cfg(&context, NZU64!(10));
1998 let mut journal = Journal::init(context.child("first"), cfg.clone())
1999 .await
2000 .unwrap();
2001
2002 for i in 0u64..30 {
2003 journal.append(&test_digest(i)).await.unwrap();
2004 }
2005 journal.sync().await.unwrap();
2006 drop(journal);
2007
2008 let (blob, _) = context
2009 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2010 .await
2011 .unwrap();
2012 blob.write_at_sync(1, 123456789u32.to_be_bytes().to_vec())
2013 .await
2014 .unwrap();
2015
2016 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2017 .await
2018 .unwrap();
2019 let reader = journal.snapshot().await.unwrap();
2020 let stream = reader.replay(0, NZUsize!(1024)).await.unwrap();
2021 pin_mut!(stream);
2022
2023 for i in 0u64..10 {
2024 let (pos, item) = stream.next().await.unwrap().unwrap();
2025 assert_eq!(pos, i);
2026 assert_eq!(item, test_digest(i));
2027 }
2028 assert!(matches!(
2029 stream.next().await.unwrap(),
2030 Err(Error::Runtime(_))
2031 ));
2032 assert!(stream.next().await.is_none());
2033
2034 journal.destroy().await.unwrap();
2035 });
2036 }
2037
2038 #[test_traced]
2039 fn test_fixed_journal_replay_with_missing_historical_blob() {
2040 let executor = deterministic::Runner::default();
2041 executor.start(|context| async move {
2042 let cfg = test_cfg(&context, NZU64!(2));
2043 let mut journal = Journal::init(context.child("first"), cfg.clone())
2044 .await
2045 .unwrap();
2046 for i in 0u64..5 {
2047 journal.append(&test_digest(i)).await.unwrap();
2048 }
2049 journal.sync().await.unwrap();
2050 drop(journal);
2051
2052 context
2055 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
2056 .await
2057 .unwrap();
2058
2059 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2060 assert!(matches!(result, Err(Error::Corruption(_))));
2061 });
2062 }
2063
2064 #[test_traced]
2065 fn test_fixed_journal_partial_replay() {
2066 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
2067 const START_POS: u64 = 53;
2070
2071 let executor = deterministic::Runner::default();
2073 executor.start(|context| async move {
2075 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
2077 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2078 .await
2079 .expect("failed to initialize journal");
2080
2081 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
2083 let pos = journal
2084 .append(&test_digest(i))
2085 .await
2086 .expect("failed to append data");
2087 assert_eq!(pos, i);
2088 }
2089
2090 {
2092 let reader = journal.snapshot().await.unwrap();
2093 let stream = reader
2094 .replay(START_POS, NZUsize!(1024))
2095 .await
2096 .expect("failed to replay journal");
2097 let mut items = Vec::new();
2098 pin_mut!(stream);
2099 while let Some(result) = stream.next().await {
2100 match result {
2101 Ok((pos, item)) => {
2102 assert!(pos >= START_POS, "pos={pos}, expected >= {START_POS}");
2103 assert_eq!(
2104 test_digest(pos),
2105 item,
2106 "Item at position {pos} did not match expected digest"
2107 );
2108 items.push(pos);
2109 }
2110 Err(err) => panic!("Failed to read item: {err}"),
2111 }
2112 }
2113
2114 assert_eq!(
2116 items.len(),
2117 ITEMS_PER_BLOB.get() as usize * 100 + ITEMS_PER_BLOB.get() as usize / 2
2118 - START_POS as usize
2119 );
2120 items.sort();
2121 for (i, pos) in items.iter().enumerate() {
2122 assert_eq!(i as u64, *pos - START_POS);
2123 }
2124 }
2125
2126 journal.destroy().await.unwrap();
2127 });
2128 }
2129
2130 #[test_traced]
2131 fn test_fixed_journal_rejects_corrupted_tail_blob() {
2132 let executor = deterministic::Runner::default();
2133 executor.start(|context| async move {
2134 let cfg = test_cfg(&context, NZU64!(3));
2135 let mut journal = Journal::init(context.child("first"), cfg.clone())
2136 .await
2137 .unwrap();
2138 for i in 0..5 {
2139 journal.append(&test_digest(i)).await.unwrap();
2140 }
2141 journal.sync().await.unwrap();
2142 drop(journal);
2143
2144 let (blob, size) = context
2147 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2148 .await
2149 .unwrap();
2150 blob.resize(size - 1).await.unwrap();
2151 blob.sync().await.unwrap();
2152
2153 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2154 assert!(matches!(result, Err(Error::Corruption(_))));
2155 });
2156 }
2157
2158 #[test_traced]
2163 fn test_fixed_journal_crash_during_recovery_repair() {
2164 let executor = deterministic::Runner::default();
2165 executor.start(|context| async move {
2166 let cfg = test_cfg(&context, NZU64!(5));
2167 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2168 .await
2169 .unwrap();
2170
2171 for i in 0..15u64 {
2173 journal.append(&test_digest(i)).await.unwrap();
2174 }
2175 journal.sync().await.unwrap();
2176 assert_eq!(journal.recovery_watermark(), 15);
2177
2178 journal
2182 .checkpoint
2183 .persist(cfg.items_per_blob.get(), 0, 9)
2184 .await
2185 .unwrap();
2186 drop(journal);
2187
2188 {
2191 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
2192 let (blob, blob_size) = context
2193 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2194 .await
2195 .expect("failed to open blob 1");
2196 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
2197 .await
2198 .expect("failed to wrap blob 1");
2199 append
2200 .resize(4 * Digest::SIZE as u64)
2201 .await
2202 .expect("failed to shorten blob 1");
2203 append
2204 .sync()
2205 .await
2206 .expect("failed to sync shortened blob 1");
2207 }
2208
2209 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2212 .await
2213 .expect("init should succeed after crash during recovery repair");
2214 assert_eq!(journal.bounds(), 0..9);
2215 assert_eq!(journal.recovery_watermark(), 9);
2216 assert_eq!(journal.read(8).await.unwrap(), test_digest(8));
2217 assert!(matches!(
2218 journal.read(9).await,
2219 Err(Error::ItemOutOfRange(9))
2220 ));
2221 assert_eq!(
2222 journal.test_newest_blob(),
2223 Some(1),
2224 "stale blobs beyond the repair point should be removed"
2225 );
2226
2227 journal.destroy().await.unwrap();
2228 });
2229 }
2230
2231 #[test_traced]
2232 fn test_fixed_journal_recover_accepts_clean_short_tail() {
2233 let executor = deterministic::Runner::default();
2234 executor.start(|context| async move {
2235 let cfg = test_cfg(&context, NZU64!(5));
2236
2237 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2240 .await
2241 .unwrap();
2242 for i in 0..7 {
2243 journal.append(&test_digest(i)).await.unwrap();
2244 }
2245 journal.sync().await.unwrap();
2246 drop(journal);
2247
2248 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2250 .await
2251 .unwrap();
2252 assert_eq!(journal.size(), 7);
2253 for i in 0..7u64 {
2255 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
2256 }
2257 journal.destroy().await.unwrap();
2258 });
2259 }
2260
2261 #[test_traced]
2262 fn test_fixed_journal_recover_accepts_clean_empty_tail() {
2263 let executor = deterministic::Runner::default();
2264 executor.start(|context| async move {
2265 let cfg = test_cfg(&context, NZU64!(5));
2266
2267 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2270 .await
2271 .unwrap();
2272 for i in 0..5 {
2273 journal.append(&test_digest(i)).await.unwrap();
2274 }
2275 journal.sync().await.unwrap();
2276 drop(journal);
2277
2278 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2280 .await
2281 .unwrap();
2282 assert_eq!(journal.size(), 5);
2283 for i in 0..5u64 {
2284 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
2285 }
2286 assert_eq!(journal.test_newest_blob(), Some(1));
2287 journal.destroy().await.unwrap();
2288 });
2289 }
2290
2291 #[test_traced]
2292 fn test_fixed_journal_recover_sparse_blob_ids_repairs_at_gap() {
2293 let executor = deterministic::Runner::default();
2294 executor.start(|context| async move {
2295 let cfg = test_cfg(&context, NZU64!(1));
2296 let blob_partition = blob_partition(&cfg);
2297
2298 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2299 .await
2300 .unwrap();
2301 journal.append(&test_digest(0)).await.unwrap();
2302 journal.sync().await.unwrap();
2303 drop(journal);
2304
2305 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
2308 let (blob, blob_size) = context
2309 .open(&blob_partition, &u64::MAX.to_be_bytes())
2310 .await
2311 .unwrap();
2312 let mut append = Writer::new(blob, blob_size, 2048, cache_ref).await.unwrap();
2313 let extra = test_digest(999);
2314 append.append(extra.as_ref()).await.unwrap();
2315 append.sync().await.unwrap();
2316 drop(append);
2317
2318 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2319 .await
2320 .unwrap();
2321 assert_eq!(journal.bounds(), 0..1);
2322 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
2323 assert!(matches!(
2324 journal.read(1).await,
2325 Err(Error::ItemOutOfRange(1))
2326 ));
2327 assert_eq!(journal.test_newest_blob(), Some(1));
2328
2329 journal.destroy().await.unwrap();
2330 });
2331 }
2332
2333 #[test_traced]
2334 fn test_fixed_journal_recover_fallback_truncates_after_short_oldest_blob() {
2335 let executor = deterministic::Runner::default();
2336 executor.start(|context| async move {
2337 let cfg = test_cfg(&context, NZU64!(5));
2338 let mut journal =
2339 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
2340 .await
2341 .expect("failed to initialize journal at size");
2342
2343 for i in 0..8u64 {
2344 journal
2345 .append(&test_digest(100 + i))
2346 .await
2347 .expect("failed to append data");
2348 }
2349 journal.sync().await.expect("failed to sync journal");
2350 assert_eq!(journal.bounds(), 7..15);
2351
2352 {
2353 journal.checkpoint.set_watermark(Some(6));
2354 journal
2355 .checkpoint
2356 .sync()
2357 .await
2358 .expect("failed to sync lower recovery watermark");
2359 }
2360 drop(journal);
2361
2362 let (blob, size) = context
2363 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2364 .await
2365 .expect("failed to open oldest blob");
2366 blob.resize(size - 1).await.expect("failed to corrupt blob");
2367 blob.sync().await.expect("failed to sync blob");
2368
2369 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2370 .await
2371 .expect("failed to recover journal");
2372 assert_eq!(journal.bounds(), 7..9);
2373 assert_eq!(journal.read(7).await.unwrap(), test_digest(100));
2374 assert_eq!(journal.read(8).await.unwrap(), test_digest(101));
2375 assert!(matches!(
2376 journal.read(9).await,
2377 Err(Error::ItemOutOfRange(9))
2378 ));
2379 assert_eq!(journal.test_oldest_blob(), Some(1));
2380 assert_eq!(journal.test_newest_blob(), Some(1));
2381
2382 journal.destroy().await.unwrap();
2383 });
2384 }
2385
2386 #[test_traced]
2387 fn test_fixed_journal_stale_pruning_metadata_preserves_watermark() {
2388 let executor = deterministic::Runner::default();
2389 executor.start(|context| async move {
2390 let cfg = test_cfg(&context, NZU64!(5));
2391 let mut journal =
2392 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
2393 .await
2394 .expect("failed to initialize journal at size");
2395
2396 for i in 0..10u64 {
2397 journal
2398 .append(&test_digest(i))
2399 .await
2400 .expect("failed to append data");
2401 }
2402 journal.sync().await.expect("failed to sync journal");
2403 assert_eq!(journal.bounds(), 7..17);
2404
2405 {
2408 journal.checkpoint.set_watermark(Some(12));
2409 journal
2410 .checkpoint
2411 .sync()
2412 .await
2413 .expect("failed to sync recovery watermark");
2414 }
2415 drop(journal);
2416
2417 {
2420 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
2421 let (blob, blob_size) = context
2422 .open(&blob_partition(&cfg), &2u64.to_be_bytes())
2423 .await
2424 .expect("failed to open blob 2");
2425 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
2426 .await
2427 .expect("failed to wrap blob 2");
2428 append
2429 .resize(2 * Digest::SIZE as u64)
2430 .await
2431 .expect("failed to shorten anchored blob");
2432 append.sync().await.expect("failed to sync blob 2");
2433 }
2434
2435 context
2438 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
2439 .await
2440 .expect("failed to remove stale oldest blob");
2441
2442 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2443 .await
2444 .expect("failed to recover journal");
2445 assert_eq!(journal.bounds(), 10..12);
2446 assert_eq!(journal.recovery_watermark(), 12);
2447 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
2448 assert_eq!(journal.read(11).await.unwrap(), test_digest(4));
2449 assert!(matches!(
2450 journal.read(12).await,
2451 Err(Error::ItemOutOfRange(12))
2452 ));
2453
2454 journal.destroy().await.unwrap();
2455 });
2456 }
2457
2458 #[test_traced]
2459 fn test_fixed_journal_stale_pruning_metadata_without_watermark_walks_lengths() {
2460 let executor = deterministic::Runner::default();
2461 executor.start(|context| async move {
2462 let cfg = test_cfg(&context, NZU64!(5));
2463 let mut journal =
2464 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
2465 .await
2466 .expect("failed to initialize journal at size");
2467
2468 for i in 0..10u64 {
2469 journal
2470 .append(&test_digest(i))
2471 .await
2472 .expect("failed to append data");
2473 }
2474 journal.sync().await.expect("failed to sync journal");
2475 assert_eq!(journal.bounds(), 7..17);
2476
2477 {
2478 journal.checkpoint.set_watermark(None);
2479 journal
2480 .checkpoint
2481 .sync()
2482 .await
2483 .expect("failed to remove recovery watermark");
2484 }
2485 drop(journal);
2486
2487 context
2490 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
2491 .await
2492 .expect("failed to remove stale oldest blob");
2493
2494 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2495 .await
2496 .expect("failed to recover journal");
2497 assert_eq!(journal.bounds(), 10..17);
2498 assert_eq!(journal.recovery_watermark(), 15);
2500 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
2501 assert_eq!(journal.read(16).await.unwrap(), test_digest(9));
2502
2503 journal.sync().await.expect("failed to sync");
2505 assert_eq!(journal.recovery_watermark(), 17);
2506
2507 journal.destroy().await.unwrap();
2508 });
2509 }
2510
2511 #[test_traced]
2515 fn test_fixed_journal_boundary_hint_ahead_of_blobs_is_corruption() {
2516 let executor = deterministic::Runner::default();
2517 executor.start(|context| async move {
2518 let cfg = test_cfg(&context, NZU64!(5));
2519 let mut journal =
2520 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 3)
2521 .await
2522 .unwrap();
2523
2524 for i in 0..12u64 {
2526 journal.append(&test_digest(i)).await.unwrap();
2527 }
2528 journal.sync().await.unwrap();
2529 assert_eq!(journal.bounds(), 3..15);
2530
2531 {
2536 journal.checkpoint.set_boundary_hint(8);
2537 journal.checkpoint.set_watermark(Some(3));
2538 journal.checkpoint.sync().await.unwrap();
2539 }
2540 drop(journal);
2541
2542 context
2543 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
2544 .await
2545 .unwrap();
2546
2547 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2548 assert!(matches!(result, Err(Error::Corruption(_))));
2549 });
2550 }
2551
2552 #[test_traced]
2555 fn test_fixed_journal_boundary_hint_with_no_blobs_is_corruption() {
2556 let executor = deterministic::Runner::default();
2557 executor.start(|context| async move {
2558 let cfg = test_cfg(&context, NZU64!(5));
2559 let mut journal =
2560 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
2561 .await
2562 .unwrap();
2563
2564 for i in 0..3u64 {
2565 journal.append(&test_digest(i)).await.unwrap();
2566 }
2567 journal.sync().await.unwrap();
2568 drop(journal);
2569
2570 for name in scan_partition(&context, &blob_partition(&cfg)).await {
2572 context
2573 .remove(&blob_partition(&cfg), Some(&name))
2574 .await
2575 .unwrap();
2576 }
2577
2578 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2579 assert!(matches!(result, Err(Error::Corruption(_))));
2580 });
2581 }
2582
2583 #[test_traced]
2584 fn test_fixed_journal_legacy_recovery_installs_watermark() {
2585 let executor = deterministic::Runner::default();
2586 executor.start(|context| async move {
2587 let cfg = test_cfg(&context, NZU64!(5));
2588 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2589 .await
2590 .expect("failed to initialize journal");
2591
2592 for i in 0..12u64 {
2593 journal
2594 .append(&test_digest(i))
2595 .await
2596 .expect("failed to append data");
2597 }
2598 journal.sync().await.expect("failed to sync journal");
2599
2600 {
2601 journal.checkpoint.set_watermark(None);
2602 journal
2603 .checkpoint
2604 .sync()
2605 .await
2606 .expect("failed to remove recovery watermark");
2607 }
2608 drop(journal);
2609
2610 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2613 .await
2614 .expect("failed to recover legacy journal");
2615 assert_eq!(journal.bounds(), 0..12);
2616 assert_eq!(journal.recovery_watermark(), 10);
2617
2618 journal
2620 .sync()
2621 .await
2622 .expect("failed to sync after legacy recovery");
2623 assert_eq!(journal.recovery_watermark(), 12);
2624
2625 journal.destroy().await.unwrap();
2626 });
2627 }
2628
2629 #[test_traced]
2633 fn test_fixed_journal_legacy_upgrade_marks_recovered_blobs_dirty() {
2634 let executor = deterministic::Runner::default();
2635 executor.start(|context| async move {
2636 let cfg = test_cfg(&context, NZU64!(5));
2637 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2638 .await
2639 .unwrap();
2640
2641 for i in 0..7u64 {
2642 journal.append(&test_digest(i)).await.unwrap();
2643 }
2644 journal.sync().await.unwrap();
2645
2646 {
2648 journal.checkpoint.set_watermark(None);
2649 journal.checkpoint.sync().await.unwrap();
2650 }
2651 drop(journal);
2652
2653 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2654 .await
2655 .unwrap();
2656 assert_eq!(journal.size(), 7);
2657 assert_eq!(journal.recovery_watermark(), 5);
2659
2660 *context.storage_fault_config().write() = deterministic::FaultConfig {
2663 sync_rate: Some(1.0),
2664 ..Default::default()
2665 };
2666 assert!(
2667 journal.commit().await.is_err(),
2668 "commit must sync recovered data before the watermark can advance"
2669 );
2670 });
2671 }
2672
2673 #[test_traced]
2674 fn test_fixed_journal_commit_does_not_advance_recovery_watermark() {
2675 let executor = deterministic::Runner::default();
2676 executor.start(|context| async move {
2677 let cfg = test_cfg(&context, NZU64!(5));
2678 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
2679 .await
2680 .unwrap();
2681
2682 journal.append(&test_digest(0)).await.unwrap();
2683 journal.sync().await.unwrap();
2684 assert_eq!(journal.recovery_watermark(), 1);
2685
2686 journal.append(&test_digest(1)).await.unwrap();
2687 journal.commit().await.unwrap();
2688 assert_eq!(
2689 journal.recovery_watermark(),
2690 1,
2691 "commit must make dirty blobs durable without advancing the recovery watermark",
2692 );
2693
2694 journal.sync().await.unwrap();
2695 assert_eq!(journal.recovery_watermark(), 2);
2696 journal.destroy().await.unwrap();
2697 });
2698 }
2699
2700 #[test_traced]
2701 fn test_fixed_journal_prune_to_blob_boundary_removes_pruning_metadata() {
2702 let executor = deterministic::Runner::default();
2703 executor.start(|context| async move {
2704 let cfg = test_cfg(&context, NZU64!(5));
2705 let mut journal =
2706 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
2707 .await
2708 .expect("failed to initialize journal at size");
2709
2710 for i in 0..8u64 {
2711 journal
2712 .append(&test_digest(i))
2713 .await
2714 .expect("failed to append data");
2715 }
2716 journal.sync().await.expect("failed to sync journal");
2717 assert_eq!(journal.bounds(), 7..15);
2718
2719 journal.prune(10).await.expect("failed to prune journal");
2720 journal.sync().await.expect("failed to sync pruned journal");
2721 assert_eq!(journal.bounds(), 10..15);
2722 drop(journal);
2723
2724 let checkpoint = Checkpoint::open(context.child("metadata"), &cfg.partition)
2725 .await
2726 .expect("failed to reopen checkpoint");
2727 assert!(checkpoint.boundary_hint().is_none());
2728 drop(checkpoint);
2729
2730 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2731 .await
2732 .expect("failed to reopen journal");
2733 assert_eq!(journal.bounds(), 10..15);
2734 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
2735 journal.destroy().await.unwrap();
2736 });
2737 }
2738
2739 #[test_traced]
2740 fn test_fixed_journal_recover_rejects_overlong_blob() {
2741 let executor = deterministic::Runner::default();
2742 executor.start(|context| async move {
2743 let cfg = test_cfg(&context, NZU64!(5));
2744 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2745 .await
2746 .expect("failed to initialize journal");
2747
2748 for i in 0..5u64 {
2749 journal
2750 .append(&test_digest(i))
2751 .await
2752 .expect("failed to append data");
2753 }
2754 journal.sync().await.expect("failed to sync journal");
2755 drop(journal);
2756
2757 {
2760 let extra = test_digest(99);
2761 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
2762 let (blob, blob_size) = context
2763 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
2764 .await
2765 .expect("failed to open blob 0");
2766 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
2767 .await
2768 .expect("failed to wrap blob 0");
2769 append
2770 .append(extra.as_ref())
2771 .await
2772 .expect("failed to append extra item");
2773 append.sync().await.expect("failed to sync corrupted blob");
2774 }
2775
2776 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2777 assert!(matches!(result, Err(Error::Corruption(_))));
2778 });
2779 }
2780
2781 #[test_traced("DEBUG")]
2782 fn test_fixed_journal_recover_from_unwritten_data() {
2783 let executor = deterministic::Runner::default();
2784 executor.start(|context| async move {
2785 let cfg = test_cfg(&context, NZU64!(10));
2787 let mut journal = Journal::init(context.child("first"), cfg.clone())
2788 .await
2789 .expect("failed to initialize journal");
2790
2791 journal
2793 .append(&test_digest(0))
2794 .await
2795 .expect("failed to append data");
2796 assert_eq!(journal.size(), 1);
2797 journal.sync().await.expect("Failed to sync journal");
2798 drop(journal);
2799
2800 let (blob, size) = context
2803 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
2804 .await
2805 .expect("Failed to open blob");
2806 blob.write_at_sync(size, vec![0u8; PAGE_SIZE.get() as usize * 3])
2807 .await
2808 .expect("Failed to extend blob");
2809
2810 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2812 .await
2813 .expect("Failed to re-initialize journal");
2814
2815 assert_eq!(journal.size(), 1);
2818
2819 journal
2821 .append(&test_digest(1))
2822 .await
2823 .expect("failed to append data");
2824
2825 journal.destroy().await.unwrap();
2826 });
2827 }
2828
2829 #[test_traced]
2830 fn test_fixed_journal_rewinding() {
2831 let executor = deterministic::Runner::default();
2832 executor.start(|context| async move {
2833 let cfg = test_cfg(&context, NZU64!(2));
2835 let mut journal = Journal::init(context.child("first"), cfg.clone())
2836 .await
2837 .expect("failed to initialize journal");
2838 assert!(matches!(journal.rewind(0).await, Ok(())));
2839 assert!(matches!(
2840 journal.rewind(1).await,
2841 Err(Error::InvalidRewind(1))
2842 ));
2843
2844 journal
2846 .append(&test_digest(0))
2847 .await
2848 .expect("failed to append data 0");
2849 assert_eq!(journal.size(), 1);
2850 assert!(matches!(journal.rewind(1).await, Ok(()))); assert!(matches!(journal.rewind(0).await, Ok(())));
2852 assert_eq!(journal.size(), 0);
2853
2854 for i in 0..7 {
2856 let pos = journal
2857 .append(&test_digest(i))
2858 .await
2859 .expect("failed to append data");
2860 assert_eq!(pos, i);
2861 }
2862 assert_eq!(journal.size(), 7);
2863
2864 assert!(matches!(journal.rewind(4).await, Ok(())));
2866 assert_eq!(journal.size(), 4);
2867
2868 assert!(matches!(journal.rewind(0).await, Ok(())));
2870 assert_eq!(journal.size(), 0);
2871
2872 for _ in 0..10 {
2874 for i in 0..100 {
2875 journal
2876 .append(&test_digest(i))
2877 .await
2878 .expect("failed to append data");
2879 }
2880 journal.rewind(journal.size() - 49).await.unwrap();
2881 }
2882 const ITEMS_REMAINING: u64 = 10 * (100 - 49);
2883 assert_eq!(journal.size(), ITEMS_REMAINING);
2884
2885 journal.sync().await.expect("Failed to sync journal");
2886 drop(journal);
2887
2888 let mut cfg = test_cfg(&context, NZU64!(3));
2890 cfg.partition = "test-partition-2".into();
2891 let mut journal = Journal::init(context.child("second"), cfg.clone())
2892 .await
2893 .expect("failed to initialize journal");
2894 for _ in 0..10 {
2895 for i in 0..100 {
2896 journal
2897 .append(&test_digest(i))
2898 .await
2899 .expect("failed to append data");
2900 }
2901 journal.rewind(journal.size() - 49).await.unwrap();
2902 }
2903 assert_eq!(journal.size(), ITEMS_REMAINING);
2904
2905 journal.sync().await.expect("Failed to sync journal");
2906 drop(journal);
2907
2908 let mut journal: Journal<_, Digest> =
2910 Journal::init(context.child("third"), cfg.clone())
2911 .await
2912 .expect("failed to re-initialize journal");
2913 assert_eq!(journal.size(), 10 * (100 - 49));
2914
2915 journal.prune(300).await.expect("pruning failed");
2917 assert_eq!(journal.size(), ITEMS_REMAINING);
2918 assert!(matches!(
2920 journal.rewind(299).await,
2921 Err(Error::ItemPruned(299))
2922 ));
2923 assert!(matches!(journal.rewind(300).await, Ok(())));
2926 let bounds = journal.bounds();
2927 assert_eq!(bounds.end, 300);
2928 assert!(bounds.is_empty());
2929
2930 journal.destroy().await.unwrap();
2931 });
2932 }
2933
2934 #[test_traced]
2935 fn test_fixed_journal_rewind_commit_reopen() {
2936 let executor = deterministic::Runner::default();
2937 executor.start(|context| async move {
2938 let cfg = test_cfg(&context, NZU64!(5));
2939 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2940 .await
2941 .expect("failed to initialize journal");
2942
2943 for i in 0..12u64 {
2944 journal
2945 .append(&test_digest(i))
2946 .await
2947 .expect("failed to append data");
2948 }
2949 journal.sync().await.expect("failed to sync journal");
2950
2951 journal.rewind(7).await.expect("failed to rewind journal");
2952 journal.commit().await.expect("failed to commit journal");
2953 drop(journal);
2954
2955 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2956 .await
2957 .expect("failed to re-initialize journal");
2958 assert_eq!(journal.bounds(), 0..7);
2959 for i in 0..7u64 {
2960 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
2961 }
2962 assert!(matches!(
2963 journal.read(7).await,
2964 Err(Error::ItemOutOfRange(7))
2965 ));
2966
2967 journal.destroy().await.unwrap();
2968 });
2969 }
2970
2971 #[test_traced]
2972 fn test_fixed_journal_rewind_persists_lower_watermark() {
2973 let executor = deterministic::Runner::default();
2974 executor.start(|context| async move {
2975 let cfg = test_cfg(&context, NZU64!(5));
2976 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2977 .await
2978 .expect("failed to initialize journal");
2979
2980 for i in 0..12u64 {
2981 journal
2982 .append(&test_digest(i))
2983 .await
2984 .expect("failed to append data");
2985 }
2986 journal.sync().await.expect("failed to sync journal");
2987 journal.rewind(7).await.expect("failed to rewind journal");
2988 drop(journal);
2989
2990 let checkpoint = Checkpoint::open(context.child("metadata"), &cfg.partition)
2991 .await
2992 .expect("failed to reopen checkpoint");
2993 let persisted_watermark = checkpoint
2994 .watermark()
2995 .expect("missing recovery watermark after rewind");
2996 assert_eq!(persisted_watermark, 7);
2997 drop(checkpoint);
2998
2999 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3000 .await
3001 .expect("failed to re-initialize journal");
3002 journal.destroy().await.unwrap();
3003 });
3004 }
3005
3006 #[test_traced]
3007 fn test_fixed_journal_recover_after_watermark_lowered_before_rewind() {
3008 let executor = deterministic::Runner::default();
3009 executor.start(|context| async move {
3010 let cfg = test_cfg(&context, NZU64!(5));
3011 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3012 .await
3013 .expect("failed to initialize journal");
3014
3015 for i in 0..12u64 {
3016 journal
3017 .append(&test_digest(i))
3018 .await
3019 .expect("failed to append data");
3020 }
3021 journal.sync().await.expect("failed to sync journal");
3022
3023 {
3024 journal.checkpoint.set_watermark(Some(7));
3025 journal
3026 .checkpoint
3027 .sync()
3028 .await
3029 .expect("failed to lower recovery watermark");
3030 }
3031 drop(journal);
3032
3033 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3034 .await
3035 .expect("failed to recover journal");
3036 assert_eq!(journal.bounds(), 0..12);
3037 assert_eq!(journal.recovery_watermark(), 7);
3038 assert_eq!(journal.read(11).await.unwrap(), test_digest(11));
3039 journal.destroy().await.unwrap();
3040 });
3041 }
3042
3043 #[test_traced]
3044 fn test_fixed_journal_rewind_append_commit_reopen() {
3045 let executor = deterministic::Runner::default();
3046 executor.start(|context| async move {
3047 let cfg = test_cfg(&context, NZU64!(5));
3048 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3049 .await
3050 .expect("failed to initialize journal");
3051
3052 for i in 0..12u64 {
3053 journal
3054 .append(&test_digest(i))
3055 .await
3056 .expect("failed to append data");
3057 }
3058 journal.sync().await.expect("failed to sync journal");
3059
3060 journal.rewind(7).await.expect("failed to rewind journal");
3061 for i in 0..3u64 {
3062 journal
3063 .append(&test_digest(100 + i))
3064 .await
3065 .expect("failed to append data");
3066 }
3067 journal.commit().await.expect("failed to commit journal");
3068 drop(journal);
3069
3070 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3071 .await
3072 .expect("failed to re-initialize journal");
3073 assert_eq!(journal.bounds(), 0..10);
3074 assert_eq!(journal.recovery_watermark(), 7);
3075 for i in 0..7u64 {
3076 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3077 }
3078 for i in 0..3u64 {
3079 assert_eq!(journal.read(7 + i).await.unwrap(), test_digest(100 + i));
3080 }
3081 assert!(matches!(
3082 journal.read(10).await,
3083 Err(Error::ItemOutOfRange(10))
3084 ));
3085
3086 journal.destroy().await.unwrap();
3087 });
3088 }
3089
3090 #[test_traced]
3091 fn test_fixed_recovery_handles_multiple_empty_data_tail_blobs() {
3092 let executor = deterministic::Runner::default();
3093 executor.start(|context| async move {
3094 let cfg = test_cfg(&context, NZU64!(1));
3095 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
3096 .await
3097 .unwrap();
3098
3099 assert_eq!(journal.append(&test_digest(10)).await.unwrap(), 0);
3102 journal.sync().await.unwrap();
3103 assert_eq!(journal.append(&test_digest(20)).await.unwrap(), 1);
3104 assert_eq!(journal.append(&test_digest(30)).await.unwrap(), 2);
3105 drop(journal);
3106
3107 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3108 assert!(
3109 blobs.len() > 2,
3110 "expected multiple empty trailing blobs, got {}",
3111 blobs.len()
3112 );
3113
3114 let journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3115 .await
3116 .unwrap();
3117 assert_eq!(journal.bounds(), 0..1);
3118 assert_eq!(journal.read(0).await.unwrap(), test_digest(10));
3119 drop(journal);
3120
3121 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3124 assert_eq!(blobs.len(), 2);
3125
3126 let mut journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3127 .await
3128 .unwrap();
3129 assert_eq!(journal.append(&test_digest(42)).await.unwrap(), 1);
3130 assert_eq!(journal.read(1).await.unwrap(), test_digest(42));
3131 journal.destroy().await.unwrap();
3132 });
3133 }
3134
3135 #[test_traced]
3136 fn test_fixed_recovery_handles_empty_data_with_no_durable_items() {
3137 let executor = deterministic::Runner::default();
3138 executor.start(|context| async move {
3139 let cfg = test_cfg(&context, NZU64!(1));
3140 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
3141 .await
3142 .unwrap();
3143
3144 assert_eq!(journal.append(&test_digest(10)).await.unwrap(), 0);
3147 assert_eq!(journal.append(&test_digest(20)).await.unwrap(), 1);
3148 drop(journal);
3149
3150 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3151 assert!(
3152 blobs.len() > 1,
3153 "expected multiple empty blobs, got {}",
3154 blobs.len()
3155 );
3156
3157 let journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3158 .await
3159 .unwrap();
3160 assert_eq!(journal.bounds(), 0..0);
3161 drop(journal);
3162
3163 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3165 assert_eq!(blobs.len(), 1);
3166
3167 let mut journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3168 .await
3169 .unwrap();
3170 assert_eq!(journal.append(&test_digest(42)).await.unwrap(), 0);
3171 assert_eq!(journal.read(0).await.unwrap(), test_digest(42));
3172 journal.destroy().await.unwrap();
3173 });
3174 }
3175
3176 #[test_traced]
3183 fn test_fixed_recovery_partial_sync_loop_keeps_contiguous_prefix() {
3184 let executor = deterministic::Runner::default();
3185 executor.start(|context| async move {
3186 let cfg = test_cfg(&context, NZU64!(10));
3187 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3188 .await
3189 .unwrap();
3190
3191 for i in 0..25u64 {
3194 journal.append(&test_digest(i)).await.unwrap();
3195 }
3196
3197 {
3200 journal.test_sync_blob(0).await.unwrap();
3201 journal.test_sync_blob(1).await.unwrap();
3202 }
3203 drop(journal);
3204
3205 let names = scan_partition(&context, &blob_partition(&cfg)).await;
3208 assert_eq!(names.len(), 3);
3209 for (blob, name) in names.iter().enumerate() {
3210 let (_blob, size) = context.open(&blob_partition(&cfg), name).await.unwrap();
3211 if blob < 2 {
3212 assert!(size > 0, "blob {blob} should be durable");
3213 } else {
3214 assert_eq!(size, 0, "blob {blob} should be empty");
3215 }
3216 }
3217
3218 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3220 .await
3221 .unwrap();
3222 assert_eq!(journal.bounds(), 0..20);
3223 for i in 0..20u64 {
3224 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3225 }
3226 assert!(matches!(
3227 journal.read(20).await,
3228 Err(Error::ItemOutOfRange(20))
3229 ));
3230
3231 assert_eq!(journal.append(&test_digest(999)).await.unwrap(), 20);
3233 assert_eq!(journal.read(20).await.unwrap(), test_digest(999));
3234
3235 journal.destroy().await.unwrap();
3236 });
3237 }
3238
3239 #[test_traced]
3248 fn test_fixed_recovery_rolls_back_durable_blob_after_gap() {
3249 let executor = deterministic::Runner::default();
3250 executor.start(|context| async move {
3251 let cfg = test_cfg(&context, NZU64!(10));
3252 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3253 .await
3254 .unwrap();
3255
3256 for i in 0..10u64 {
3258 journal.append(&test_digest(i)).await.unwrap();
3259 }
3260 journal.sync().await.unwrap();
3261
3262 for i in 10..28u64 {
3265 journal.append(&test_digest(i)).await.unwrap();
3266 }
3267 {
3268 journal.test_sync_blob(2).await.unwrap();
3269 }
3270 drop(journal);
3271
3272 let names = scan_partition(&context, &blob_partition(&cfg)).await;
3274 assert_eq!(names.len(), 3);
3275 let mut sizes = Vec::new();
3276 for name in &names {
3277 let (_blob, size) = context.open(&blob_partition(&cfg), name).await.unwrap();
3278 sizes.push(size);
3279 }
3280 assert!(sizes[0] > 0, "blob 0 should be durable");
3281 assert_eq!(sizes[1], 0, "blob 1 should be the gap");
3282 assert!(sizes[2] > 0, "blob 2 should be incidentally durable");
3283
3284 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3287 .await
3288 .unwrap();
3289 assert_eq!(journal.bounds(), 0..10);
3290 for i in 0..10u64 {
3291 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3292 }
3293 assert!(matches!(
3294 journal.read(10).await,
3295 Err(Error::ItemOutOfRange(10))
3296 ));
3297
3298 let names = scan_partition(&context, &blob_partition(&cfg)).await;
3300 assert_eq!(names.len(), 2);
3301
3302 assert_eq!(journal.append(&test_digest(999)).await.unwrap(), 10);
3304 assert_eq!(journal.read(10).await.unwrap(), test_digest(999));
3305
3306 journal.destroy().await.unwrap();
3307 });
3308 }
3309
3310 #[test_traced]
3315 fn test_fixed_recovery_prune_crash_retains_unsynced_tail() {
3316 let executor = deterministic::Runner::default();
3317 executor.start(|context| async move {
3318 let cfg = test_cfg(&context, NZU64!(10));
3319 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3320 .await
3321 .unwrap();
3322
3323 for i in 0..10u64 {
3326 journal.append(&test_digest(i)).await.unwrap();
3327 }
3328 journal.sync().await.unwrap();
3329 for i in 10..25u64 {
3330 journal.append(&test_digest(i)).await.unwrap();
3331 }
3332
3333 assert!(journal.prune(10).await.unwrap());
3335 drop(journal);
3336
3337 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3338 .await
3339 .unwrap();
3340 assert_eq!(journal.bounds(), 10..25);
3341 for i in 10..25u64 {
3342 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3343 }
3344 journal.destroy().await.unwrap();
3345 });
3346 }
3347
3348 #[test_traced]
3356 fn test_fixed_recovery_empty_oldest_blob_orphaned_newer_blob() {
3357 let executor = deterministic::Runner::default();
3358 executor.start(|context| async move {
3359 let cfg = test_cfg(&context, NZU64!(10));
3360
3361 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3363 .await
3364 .unwrap();
3365 for i in 0..20u64 {
3366 journal.append(&test_digest(i)).await.unwrap();
3367 }
3368 journal.sync().await.unwrap();
3369 drop(journal);
3370
3371 let (blob0, size0) = context
3374 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
3375 .await
3376 .unwrap();
3377 assert!(size0 > 0);
3378 blob0.resize(0).await.unwrap();
3379 blob0.sync().await.unwrap();
3380
3381 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
3382 assert!(matches!(result, Err(Error::Corruption(_))));
3383 });
3384 }
3385
3386 #[test_traced]
3392 fn test_single_item_per_blob() {
3393 let executor = deterministic::Runner::default();
3394 executor.start(|context| async move {
3395 let cfg = Config {
3396 partition: "single-item-per-blob".into(),
3397 items_per_blob: NZU64!(1),
3398 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3399 write_buffer: NZUsize!(2048),
3400 };
3401
3402 let mut journal = Journal::init(context.child("first"), cfg.clone())
3404 .await
3405 .expect("failed to initialize journal");
3406
3407 let bounds = journal.bounds();
3409 assert_eq!(bounds.end, 0);
3410 assert!(bounds.is_empty());
3411
3412 let pos = journal
3414 .append(&test_digest(0))
3415 .await
3416 .expect("failed to append");
3417 assert_eq!(pos, 0);
3418 assert_eq!(journal.size(), 1);
3419
3420 journal.sync().await.expect("failed to sync");
3422
3423 let value = journal
3425 .read(journal.size() - 1)
3426 .await
3427 .expect("failed to read");
3428 assert_eq!(value, test_digest(0));
3429
3430 for i in 1..10u64 {
3432 let pos = journal
3433 .append(&test_digest(i))
3434 .await
3435 .expect("failed to append");
3436 assert_eq!(pos, i);
3437 assert_eq!(journal.size(), i + 1);
3438
3439 let value = journal
3441 .read(journal.size() - 1)
3442 .await
3443 .expect("failed to read");
3444 assert_eq!(value, test_digest(i));
3445 }
3446
3447 for i in 0..10u64 {
3449 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3450 }
3451
3452 journal.sync().await.expect("failed to sync");
3453
3454 journal.prune(5).await.expect("failed to prune");
3457
3458 assert_eq!(journal.size(), 10);
3460
3461 assert_eq!(journal.bounds().start, 5);
3463
3464 let value = journal
3466 .read(journal.size() - 1)
3467 .await
3468 .expect("failed to read");
3469 assert_eq!(value, test_digest(9));
3470
3471 for i in 0..5 {
3473 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
3474 }
3475
3476 for i in 5..10u64 {
3478 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3479 }
3480
3481 for i in 10..15u64 {
3483 let pos = journal
3484 .append(&test_digest(i))
3485 .await
3486 .expect("failed to append");
3487 assert_eq!(pos, i);
3488
3489 let value = journal
3491 .read(journal.size() - 1)
3492 .await
3493 .expect("failed to read");
3494 assert_eq!(value, test_digest(i));
3495 }
3496
3497 journal.sync().await.expect("failed to sync");
3498 drop(journal);
3499
3500 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3502 .await
3503 .expect("failed to re-initialize journal");
3504
3505 assert_eq!(journal.size(), 15);
3507
3508 assert_eq!(journal.bounds().start, 5);
3510
3511 let value = journal
3513 .read(journal.size() - 1)
3514 .await
3515 .expect("failed to read");
3516 assert_eq!(value, test_digest(14));
3517
3518 for i in 5..15u64 {
3520 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3521 }
3522
3523 journal.destroy().await.expect("failed to destroy journal");
3524
3525 let mut journal = Journal::init(context.child("third"), cfg.clone())
3528 .await
3529 .expect("failed to initialize journal");
3530
3531 for i in 0..10u64 {
3533 journal.append(&test_digest(i + 100)).await.unwrap();
3534 }
3535
3536 journal.prune(5).await.unwrap();
3538 let bounds = journal.bounds();
3539 assert_eq!(bounds.end, 10);
3540 assert_eq!(bounds.start, 5);
3541
3542 journal.sync().await.unwrap();
3544 drop(journal);
3545
3546 let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
3548 .await
3549 .expect("failed to re-initialize journal");
3550
3551 let bounds = journal.bounds();
3553 assert_eq!(bounds.end, 10);
3554 assert_eq!(bounds.start, 5);
3555
3556 let value = journal.read(journal.size() - 1).await.unwrap();
3558 assert_eq!(value, test_digest(109));
3559
3560 for i in 5..10u64 {
3562 assert_eq!(journal.read(i).await.unwrap(), test_digest(i + 100));
3563 }
3564
3565 journal.destroy().await.expect("failed to destroy journal");
3566
3567 let mut journal = Journal::init(context.child("storage"), cfg.clone())
3569 .await
3570 .expect("failed to initialize journal");
3571
3572 for i in 0..5u64 {
3573 journal.append(&test_digest(i + 200)).await.unwrap();
3574 }
3575 journal.sync().await.unwrap();
3576
3577 journal.prune(5).await.unwrap();
3579 let bounds = journal.bounds();
3580 assert_eq!(bounds.end, 5); assert!(bounds.is_empty()); let result = journal.read(journal.size() - 1).await;
3585 assert!(matches!(result, Err(Error::ItemPruned(4))));
3586
3587 journal.append(&test_digest(205)).await.unwrap();
3589 assert_eq!(journal.bounds().start, 5);
3590 assert_eq!(
3591 journal.read(journal.size() - 1).await.unwrap(),
3592 test_digest(205)
3593 );
3594
3595 journal.destroy().await.expect("failed to destroy journal");
3596 });
3597 }
3598
3599 #[test_traced]
3600 fn test_fixed_journal_init_at_size_zero() {
3601 let executor = deterministic::Runner::default();
3602 executor.start(|context| async move {
3603 let cfg = test_cfg(&context, NZU64!(5));
3604 let mut journal =
3605 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 0)
3606 .await
3607 .unwrap();
3608
3609 let bounds = journal.bounds();
3610 assert_eq!(bounds.end, 0);
3611 assert!(bounds.is_empty());
3612
3613 let pos = journal.append(&test_digest(100)).await.unwrap();
3615 assert_eq!(pos, 0);
3616 assert_eq!(journal.size(), 1);
3617 assert_eq!(journal.read(0).await.unwrap(), test_digest(100));
3618
3619 journal.destroy().await.unwrap();
3620 });
3621 }
3622
3623 #[test_traced]
3624 fn test_fixed_journal_init_at_max_size_rejected() {
3625 let executor = deterministic::Runner::default();
3626 executor.start(|context| async move {
3627 let mut cfg = test_cfg(&context, NZU64!(1));
3628 cfg.partition = "max-size-rejected".into();
3629
3630 assert!(matches!(
3632 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg, u64::MAX).await,
3633 Err(Error::SizeOverflow)
3634 ));
3635 });
3636 }
3637
3638 #[test_traced]
3639 fn test_fixed_journal_append_size_overflow() {
3640 let executor = deterministic::Runner::default();
3641 executor.start(|context| async move {
3642 let mut cfg = test_cfg(&context, NZU64!(1));
3643 cfg.partition = "append-size-overflow".into();
3644
3645 let mut journal =
3647 Journal::<_, Digest>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
3648 .await
3649 .unwrap();
3650
3651 assert_eq!(journal.append(&test_digest(7)).await.unwrap(), u64::MAX - 1);
3653 assert_eq!(journal.size(), u64::MAX);
3654
3655 assert!(matches!(
3658 journal.append(&test_digest(8)).await,
3659 Err(Error::SizeOverflow)
3660 ));
3661
3662 journal.destroy().await.unwrap();
3663 });
3664 }
3665
3666 #[test_traced]
3667 fn test_fixed_journal_replay_near_max_size() {
3668 let executor = deterministic::Runner::default();
3669 executor.start(|context| async move {
3670 let mut cfg = test_cfg(&context, NZU64!(10));
3671 cfg.partition = "replay-near-max-size".into();
3672
3673 let mut journal =
3674 Journal::<_, Digest>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
3675 .await
3676 .unwrap();
3677 let expected = test_digest(7);
3678 assert_eq!(journal.append(&expected).await.unwrap(), u64::MAX - 1);
3679
3680 {
3681 let reader = journal.snapshot().await.unwrap();
3682 let stream = reader.replay(u64::MAX - 1, NZUsize!(1024)).await.unwrap();
3683 pin_mut!(stream);
3684 let (pos, item) = stream.next().await.unwrap().unwrap();
3685 assert_eq!(pos, u64::MAX - 1);
3686 assert_eq!(item, expected);
3687 assert!(stream.next().await.is_none());
3688 }
3689
3690 journal.destroy().await.unwrap();
3691 });
3692 }
3693
3694 #[test_traced]
3695 fn test_fixed_journal_init_at_size_blob_boundary() {
3696 let executor = deterministic::Runner::default();
3697 executor.start(|context| async move {
3698 let cfg = test_cfg(&context, NZU64!(5));
3699
3700 let mut journal =
3702 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10)
3703 .await
3704 .unwrap();
3705
3706 let bounds = journal.bounds();
3707 assert_eq!(bounds.end, 10);
3708 assert!(bounds.is_empty());
3709
3710 let pos = journal.append(&test_digest(1000)).await.unwrap();
3712 assert_eq!(pos, 10);
3713 assert_eq!(journal.size(), 11);
3714 assert_eq!(journal.read(10).await.unwrap(), test_digest(1000));
3715
3716 let pos = journal.append(&test_digest(1001)).await.unwrap();
3718 assert_eq!(pos, 11);
3719 assert_eq!(journal.read(11).await.unwrap(), test_digest(1001));
3720
3721 journal.destroy().await.unwrap();
3722 });
3723 }
3724
3725 #[test_traced]
3726 fn test_fixed_journal_init_at_size_mid_blob() {
3727 let executor = deterministic::Runner::default();
3728 executor.start(|context| async move {
3729 let cfg = test_cfg(&context, NZU64!(5));
3730
3731 let mut journal =
3733 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7)
3734 .await
3735 .unwrap();
3736
3737 let bounds = journal.bounds();
3738 assert_eq!(bounds.end, 7);
3739 assert!(bounds.is_empty());
3741
3742 assert!(matches!(journal.read(5).await, Err(Error::ItemPruned(5))));
3744 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
3745
3746 let pos = journal.append(&test_digest(700)).await.unwrap();
3748 assert_eq!(pos, 7);
3749 assert_eq!(journal.size(), 8);
3750 assert_eq!(journal.read(7).await.unwrap(), test_digest(700));
3751 assert_eq!(journal.bounds().start, 7);
3753
3754 journal.destroy().await.unwrap();
3755 });
3756 }
3757
3758 #[test_traced]
3759 fn test_fixed_journal_append_many_after_mid_blob_start() {
3760 let executor = deterministic::Runner::default();
3761 executor.start(|context| async move {
3762 let cfg = test_cfg(&context, NZU64!(100));
3763 let mut journal =
3764 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 150)
3765 .await
3766 .unwrap();
3767
3768 let items: Vec<_> = (0..100u64).map(|i| test_digest(1500 + i)).collect();
3769 let last = journal.append_many(Many::Flat(&items)).await.unwrap();
3770 assert_eq!(last, 249);
3771 assert_eq!(journal.bounds(), 150..250);
3772
3773 for (position, index) in [(150, 0), (199, 49), (200, 50), (249, 99)] {
3774 assert_eq!(
3775 journal.read(position).await.unwrap(),
3776 items[index],
3777 "item at position {position} did not match"
3778 );
3779 }
3780
3781 journal.sync().await.unwrap();
3782 drop(journal);
3783
3784 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3785 .await
3786 .unwrap();
3787 assert_eq!(journal.bounds(), 150..250);
3788 for (position, index) in [(150, 0), (199, 49), (200, 50), (249, 99)] {
3789 assert_eq!(
3790 journal.read(position).await.unwrap(),
3791 items[index],
3792 "item at position {position} did not match after reopen"
3793 );
3794 }
3795
3796 journal.destroy().await.unwrap();
3797 });
3798 }
3799
3800 #[test_traced]
3801 fn test_fixed_journal_init_at_size_persistence() {
3802 let executor = deterministic::Runner::default();
3803 executor.start(|context| async move {
3804 let cfg = test_cfg(&context, NZU64!(5));
3805
3806 let mut journal =
3808 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15)
3809 .await
3810 .unwrap();
3811
3812 for i in 0..5u64 {
3814 let pos = journal.append(&test_digest(1500 + i)).await.unwrap();
3815 assert_eq!(pos, 15 + i);
3816 }
3817
3818 assert_eq!(journal.size(), 20);
3819
3820 journal.sync().await.unwrap();
3822 drop(journal);
3823
3824 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3825 .await
3826 .unwrap();
3827
3828 let bounds = journal.bounds();
3830 assert_eq!(bounds.end, 20);
3831 assert_eq!(bounds.start, 15);
3832
3833 for i in 0..5u64 {
3835 assert_eq!(journal.read(15 + i).await.unwrap(), test_digest(1500 + i));
3836 }
3837
3838 let pos = journal.append(&test_digest(9999)).await.unwrap();
3840 assert_eq!(pos, 20);
3841 assert_eq!(journal.read(20).await.unwrap(), test_digest(9999));
3842
3843 journal.destroy().await.unwrap();
3844 });
3845 }
3846
3847 #[test_traced]
3848 fn test_fixed_journal_init_at_size_persistence_without_data() {
3849 let executor = deterministic::Runner::default();
3850 executor.start(|context| async move {
3851 let cfg = test_cfg(&context, NZU64!(5));
3852
3853 let journal =
3855 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15)
3856 .await
3857 .unwrap();
3858
3859 let bounds = journal.bounds();
3860 assert_eq!(bounds.end, 15);
3861 assert!(bounds.is_empty());
3862
3863 drop(journal);
3865
3866 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3868 .await
3869 .unwrap();
3870
3871 let bounds = journal.bounds();
3872 assert_eq!(bounds.end, 15);
3873 assert!(bounds.is_empty());
3874
3875 let pos = journal.append(&test_digest(1500)).await.unwrap();
3877 assert_eq!(pos, 15);
3878 assert_eq!(journal.read(15).await.unwrap(), test_digest(1500));
3879
3880 journal.destroy().await.unwrap();
3881 });
3882 }
3883
3884 #[test_traced]
3885 fn test_fixed_journal_init_at_size_large_offset() {
3886 let executor = deterministic::Runner::default();
3887 executor.start(|context| async move {
3888 let cfg = test_cfg(&context, NZU64!(5));
3889
3890 let mut journal =
3892 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 1000)
3893 .await
3894 .unwrap();
3895
3896 let bounds = journal.bounds();
3897 assert_eq!(bounds.end, 1000);
3898 assert!(bounds.is_empty());
3899
3900 let pos = journal.append(&test_digest(100000)).await.unwrap();
3902 assert_eq!(pos, 1000);
3903 assert_eq!(journal.read(1000).await.unwrap(), test_digest(100000));
3904
3905 journal.destroy().await.unwrap();
3906 });
3907 }
3908
3909 #[test_traced]
3910 fn test_fixed_journal_init_at_size_prune_and_append() {
3911 let executor = deterministic::Runner::default();
3912 executor.start(|context| async move {
3913 let cfg = test_cfg(&context, NZU64!(5));
3914
3915 let mut journal =
3917 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 20)
3918 .await
3919 .unwrap();
3920
3921 for i in 0..10u64 {
3923 journal.append(&test_digest(2000 + i)).await.unwrap();
3924 }
3925
3926 assert_eq!(journal.size(), 30);
3927
3928 journal.prune(25).await.unwrap();
3930
3931 let bounds = journal.bounds();
3932 assert_eq!(bounds.end, 30);
3933 assert_eq!(bounds.start, 25);
3934
3935 for i in 25..30u64 {
3937 assert_eq!(journal.read(i).await.unwrap(), test_digest(2000 + (i - 20)));
3938 }
3939
3940 let pos = journal.append(&test_digest(3000)).await.unwrap();
3942 assert_eq!(pos, 30);
3943
3944 journal.destroy().await.unwrap();
3945 });
3946 }
3947
3948 #[test_traced]
3949 fn test_fixed_journal_clear_to_size() {
3950 let executor = deterministic::Runner::default();
3951 executor.start(|context| async move {
3952 let cfg = test_cfg(&context, NZU64!(10));
3953 let mut journal = Journal::init(context.child("journal"), cfg.clone())
3954 .await
3955 .expect("failed to initialize journal");
3956
3957 for i in 0..25u64 {
3959 journal.append(&test_digest(i)).await.unwrap();
3960 }
3961 assert_eq!(journal.size(), 25);
3962 journal.sync().await.unwrap();
3963
3964 journal.clear_to_size(100).await.unwrap();
3966 assert_eq!(journal.size(), 100);
3967
3968 for i in 0..25 {
3970 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
3971 }
3972
3973 drop(journal);
3975 let mut journal =
3976 Journal::<_, Digest>::init(context.child("journal_after_clear"), cfg.clone())
3977 .await
3978 .expect("failed to re-initialize journal after clear");
3979 assert_eq!(journal.size(), 100);
3980
3981 for i in 100..105u64 {
3983 let pos = journal.append(&test_digest(i)).await.unwrap();
3984 assert_eq!(pos, i);
3985 }
3986 assert_eq!(journal.size(), 105);
3987
3988 for i in 100..105u64 {
3990 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3991 }
3992
3993 journal.sync().await.unwrap();
3995 drop(journal);
3996
3997 let journal = Journal::<_, Digest>::init(context.child("journal_reopened"), cfg)
3998 .await
3999 .expect("failed to re-initialize journal");
4000
4001 assert_eq!(journal.size(), 105);
4002 for i in 100..105u64 {
4003 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4004 }
4005
4006 journal.destroy().await.unwrap();
4007 });
4008 }
4009
4010 #[test_traced]
4011 fn test_fixed_journal_clear_to_size_rejects_max() {
4012 let executor = deterministic::Runner::default();
4015 executor.start(|context| async move {
4016 let cfg = test_cfg(&context, NZU64!(10));
4017 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg)
4018 .await
4019 .unwrap();
4020 assert!(matches!(
4021 journal.clear_to_size(u64::MAX).await,
4022 Err(Error::SizeOverflow)
4023 ));
4024 assert!(matches!(
4025 journal.stage_clear_intent(u64::MAX).await,
4026 Err(Error::SizeOverflow)
4027 ));
4028 journal.destroy().await.unwrap();
4029 });
4030 }
4031
4032 #[test_traced]
4033 fn test_fixed_journal_sync_crash_meta_none_boundary_aligned() {
4034 let executor = deterministic::Runner::default();
4036 executor.start(|context| async move {
4037 let cfg = test_cfg(&context, NZU64!(5));
4038 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4039 .await
4040 .unwrap();
4041
4042 for i in 0..5u64 {
4043 journal.append(&test_digest(i)).await.unwrap();
4044 }
4045 journal.commit().await.unwrap();
4046 drop(journal);
4047
4048 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4049 .await
4050 .unwrap();
4051 let bounds = journal.bounds();
4052 assert_eq!(bounds.start, 0);
4053 assert_eq!(bounds.end, 5);
4054 journal.destroy().await.unwrap();
4055 });
4056 }
4057
4058 #[test_traced]
4059 fn test_fixed_journal_missing_metadata_with_short_blob_is_corruption() {
4060 let executor = deterministic::Runner::default();
4063 executor.start(|context| async move {
4064 let cfg = test_cfg(&context, NZU64!(5));
4065 let mut journal =
4066 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4067 .await
4068 .unwrap();
4069 for i in 0..3u64 {
4070 journal.append(&test_digest(i)).await.unwrap();
4071 }
4072 journal.sync().await.unwrap();
4073
4074 journal.checkpoint.clear();
4076 journal.checkpoint.sync().await.unwrap();
4077 drop(journal);
4078
4079 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
4080 assert!(matches!(result, Err(Error::Corruption(_))));
4081 });
4082 }
4083
4084 #[test_traced]
4085 fn test_fixed_journal_sync_crash_meta_mid_boundary_unchanged() {
4086 let executor = deterministic::Runner::default();
4088 executor.start(|context| async move {
4089 let cfg = test_cfg(&context, NZU64!(5));
4090 let mut journal =
4091 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4092 .await
4093 .unwrap();
4094 for i in 0..3u64 {
4095 journal.append(&test_digest(i)).await.unwrap();
4096 }
4097 journal.commit().await.unwrap();
4098 drop(journal);
4099
4100 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4101 .await
4102 .unwrap();
4103 let bounds = journal.bounds();
4104 assert_eq!(bounds.start, 7);
4105 assert_eq!(bounds.end, 10);
4106 journal.destroy().await.unwrap();
4107 });
4108 }
4109 #[test_traced]
4110 fn test_fixed_journal_sync_crash_meta_mid_to_aligned_becomes_stale() {
4111 let executor = deterministic::Runner::default();
4113 executor.start(|context| async move {
4114 let cfg = test_cfg(&context, NZU64!(5));
4115 let mut journal =
4116 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4117 .await
4118 .unwrap();
4119 for i in 0..10u64 {
4120 journal.append(&test_digest(i)).await.unwrap();
4121 }
4122 assert_eq!(journal.size(), 17);
4123 journal.prune(10).await.unwrap();
4124
4125 journal.commit().await.unwrap();
4126 drop(journal);
4127
4128 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4129 .await
4130 .unwrap();
4131 let bounds = journal.bounds();
4132 assert_eq!(bounds.start, 10);
4133 assert_eq!(bounds.end, 17);
4134 journal.destroy().await.unwrap();
4135 });
4136 }
4137
4138 #[test_traced]
4139 fn test_fixed_journal_prune_does_not_move_boundary_backwards() {
4140 let executor = deterministic::Runner::default();
4143 executor.start(|context| async move {
4144 let cfg = test_cfg(&context, NZU64!(5));
4145 let mut journal =
4147 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4148 .await
4149 .unwrap();
4150 for i in 0..5u64 {
4152 journal.append(&test_digest(i)).await.unwrap();
4153 }
4154 journal.prune(5).await.unwrap();
4156 assert_eq!(journal.bounds().start, 7);
4157 journal.destroy().await.unwrap();
4158 });
4159 }
4160
4161 #[test_traced]
4164 fn test_fixed_journal_commit_after_prune() {
4165 let executor = deterministic::Runner::default();
4166 executor.start(|context| async move {
4167 let cfg = test_cfg(&context, NZU64!(5));
4168 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
4169 .await
4170 .unwrap();
4171
4172 for i in 0..12 {
4173 journal.append(&test_digest(i)).await.unwrap();
4174 }
4175
4176 journal.prune(5).await.unwrap();
4177 journal
4178 .commit()
4179 .await
4180 .expect("commit should not try to sync pruned blobs");
4181 assert_eq!(journal.bounds(), 5..12);
4182 journal.destroy().await.unwrap();
4183 });
4184 }
4185
4186 #[test_traced]
4187 fn test_fixed_journal_replay_after_init_at_size_spanning_blobs() {
4188 let executor = deterministic::Runner::default();
4191 executor.start(|context| async move {
4192 let cfg = test_cfg(&context, NZU64!(5));
4193
4194 let mut journal =
4197 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7)
4198 .await
4199 .unwrap();
4200
4201 for i in 0..13u64 {
4203 let pos = journal.append(&test_digest(100 + i)).await.unwrap();
4204 assert_eq!(pos, 7 + i);
4205 }
4206 assert_eq!(journal.size(), 20);
4207 journal.sync().await.unwrap();
4208
4209 {
4211 let reader = journal.snapshot().await.unwrap();
4212 let stream = reader
4213 .replay(7, NZUsize!(1024))
4214 .await
4215 .expect("failed to replay");
4216 pin_mut!(stream);
4217 let mut items: Vec<(u64, Digest)> = Vec::new();
4218 while let Some(result) = stream.next().await {
4219 items.push(result.expect("replay item failed"));
4220 }
4221
4222 assert_eq!(items.len(), 13);
4224 for (i, (pos, item)) in items.iter().enumerate() {
4225 assert_eq!(*pos, 7 + i as u64);
4226 assert_eq!(*item, test_digest(100 + i as u64));
4227 }
4228 }
4229
4230 {
4232 let reader = journal.snapshot().await.unwrap();
4233 let stream = reader
4234 .replay(12, NZUsize!(1024))
4235 .await
4236 .expect("failed to replay from mid-stream");
4237 pin_mut!(stream);
4238 let mut items: Vec<(u64, Digest)> = Vec::new();
4239 while let Some(result) = stream.next().await {
4240 items.push(result.expect("replay item failed"));
4241 }
4242
4243 assert_eq!(items.len(), 8);
4245 for (i, (pos, item)) in items.iter().enumerate() {
4246 assert_eq!(*pos, 12 + i as u64);
4247 assert_eq!(*item, test_digest(100 + 5 + i as u64));
4248 }
4249 }
4250
4251 journal.destroy().await.unwrap();
4252 });
4253 }
4254
4255 #[test_traced]
4256 fn test_fixed_journal_rewind_error_before_bounds_start() {
4257 let executor = deterministic::Runner::default();
4259 executor.start(|context| async move {
4260 let cfg = test_cfg(&context, NZU64!(5));
4261
4262 let mut journal =
4263 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10)
4264 .await
4265 .unwrap();
4266
4267 for i in 0..3u64 {
4269 journal.append(&test_digest(i)).await.unwrap();
4270 }
4271 assert_eq!(journal.size(), 13);
4272
4273 journal.rewind(11).await.unwrap();
4275 assert_eq!(journal.size(), 11);
4276
4277 journal.rewind(10).await.unwrap();
4279 assert_eq!(journal.size(), 10);
4280
4281 let result = journal.rewind(9).await;
4283 assert!(matches!(result, Err(Error::ItemPruned(9))));
4284
4285 journal.destroy().await.unwrap();
4286 });
4287 }
4288
4289 #[test_traced]
4290 fn test_fixed_journal_init_at_size_crash_scenarios() {
4291 let executor = deterministic::Runner::default();
4292 executor.start(|context| async move {
4293 let cfg = test_cfg(&context, NZU64!(5));
4294
4295 let mut journal =
4297 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4298 .await
4299 .unwrap();
4300 for i in 0..5u64 {
4301 journal.append(&test_digest(i)).await.unwrap();
4302 }
4303 journal.sync().await.unwrap();
4304 drop(journal);
4305
4306 let blob_part = blob_partition(&cfg);
4309 let mut checkpoint = Checkpoint::open(context.child("intent_meta"), &cfg.partition)
4310 .await
4311 .unwrap();
4312 checkpoint.set_clear_target(12);
4313 checkpoint.sync().await.unwrap();
4314 drop(checkpoint);
4315 context.remove(&blob_part, None).await.unwrap();
4316
4317 let journal = Journal::<_, Digest>::init(
4319 context.child("crash").with_attribute("index", 1),
4320 cfg.clone(),
4321 )
4322 .await
4323 .expect("init failed after clear crash");
4324 let bounds = journal.bounds();
4325 assert_eq!(bounds.end, 12);
4326 assert_eq!(bounds.start, 12);
4327 drop(journal);
4328
4329 let mut checkpoint = Checkpoint::open(context.child("restore_meta"), &cfg.partition)
4331 .await
4332 .unwrap();
4333 checkpoint.set_boundary_hint(7);
4334 checkpoint.set_clear_target(2);
4335 checkpoint.sync().await.unwrap();
4336 drop(checkpoint);
4337
4338 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
4341 blob.sync().await.unwrap(); drop(blob);
4343
4344 let journal = Journal::<_, Digest>::init(
4346 context.child("crash").with_attribute("index", 2),
4347 cfg.clone(),
4348 )
4349 .await
4350 .expect("init failed after create crash");
4351
4352 let bounds = journal.bounds();
4353 assert_eq!(bounds.start, 2);
4354 assert_eq!(bounds.end, 2);
4355 journal.destroy().await.unwrap();
4356 });
4357 }
4358
4359 #[test_traced]
4360 fn test_fixed_journal_clear_to_size_crash_scenarios() {
4361 let executor = deterministic::Runner::default();
4362 executor.start(|context| async move {
4363 let cfg = test_cfg(&context, NZU64!(5));
4364
4365 let mut journal =
4368 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 12)
4369 .await
4370 .unwrap();
4371 journal.sync().await.unwrap();
4372 drop(journal);
4373
4374 let blob_part = blob_partition(&cfg);
4378 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
4379 .await
4380 .unwrap();
4381 checkpoint.set_clear_target(2);
4382 checkpoint.sync().await.unwrap();
4383 drop(checkpoint);
4384
4385 context.remove(&blob_part, None).await.unwrap();
4386
4387 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
4388 blob.sync().await.unwrap();
4389
4390 let journal = Journal::<_, Digest>::init(context.child("crash_clear"), cfg.clone())
4391 .await
4392 .expect("init failed after clear_to_size crash");
4393
4394 let bounds = journal.bounds();
4395 assert_eq!(bounds.start, 2);
4396 assert_eq!(bounds.end, 2);
4397 journal.destroy().await.unwrap();
4398 });
4399 }
4400
4401 #[test_traced]
4402 fn test_fixed_journal_clear_to_size_crash_after_intent_before_blobs() {
4403 let executor = deterministic::Runner::default();
4404 executor.start(|context| async move {
4405 let cfg = test_cfg(&context, NZU64!(5));
4406 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4407 .await
4408 .unwrap();
4409 for i in 0..12u64 {
4410 journal.append(&test_digest(i)).await.unwrap();
4411 }
4412 journal.sync().await.unwrap();
4413
4414 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
4415 .await
4416 .unwrap();
4417 checkpoint.set_clear_target(100);
4418 checkpoint.sync().await.unwrap();
4419 drop(checkpoint);
4420 drop(journal);
4421
4422 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4423 .await
4424 .expect("init failed after clear intent crash");
4425 assert_eq!(journal.bounds(), 100..100);
4426 let pos = journal.append(&test_digest(100)).await.unwrap();
4427 assert_eq!(pos, 100);
4428 journal.destroy().await.unwrap();
4429 });
4430 }
4431
4432 #[test_traced]
4433 fn test_fixed_journal_clear_intent_skips_corrupt_stale_blobs() {
4434 let executor = deterministic::Runner::default();
4435 executor.start(|context| async move {
4436 let cfg = test_cfg(&context, NZU64!(5));
4437 let blob_part = blob_partition(&cfg);
4438 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
4439 .await
4440 .unwrap();
4441 checkpoint.set_clear_target(12);
4442 checkpoint.sync().await.unwrap();
4443 drop(checkpoint);
4444
4445 let (blob, _) = context.open(&blob_part, b"not-u64").await.unwrap();
4448 blob.write_at_sync(0, vec![1, 2, 3]).await.unwrap();
4449 drop(blob);
4450
4451 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg.clone())
4452 .await
4453 .expect("clear intent should discard stale corrupt blobs before blob parsing");
4454 assert_eq!(journal.bounds(), 12..12);
4455 assert_eq!(journal.recovery_watermark(), 12);
4456 journal.destroy().await.unwrap();
4457 });
4458 }
4459
4460 #[test_traced]
4461 fn test_fixed_journal_clear_to_size_crash_after_mid_blob_intent_with_old_blobs_present() {
4462 let executor = deterministic::Runner::default();
4463 executor.start(|context| async move {
4464 let cfg = test_cfg(&context, NZU64!(10));
4465 let mut journal =
4466 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 10)
4467 .await
4468 .unwrap();
4469
4470 for i in 0..6u64 {
4471 let pos = journal.append(&test_digest(i)).await.unwrap();
4472 assert_eq!(pos, 10 + i);
4473 }
4474 journal.sync().await.unwrap();
4475
4476 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
4477 .await
4478 .unwrap();
4479 checkpoint.set_clear_target(15);
4480 checkpoint.sync().await.unwrap();
4481 drop(checkpoint);
4482 drop(journal);
4483
4484 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4485 .await
4486 .expect("init failed after mid-blob clear intent crash");
4487 assert_eq!(journal.bounds(), 15..15);
4488 drop(journal);
4489
4490 let mut journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
4491 .await
4492 .expect("init failed after completing mid-blob clear intent");
4493 assert_eq!(journal.bounds(), 15..15);
4494 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(14))));
4495 let pos = journal.append(&test_digest(100)).await.unwrap();
4496 assert_eq!(pos, 15);
4497 assert_eq!(journal.read(15).await.unwrap(), test_digest(100));
4498 journal.destroy().await.unwrap();
4499 });
4500 }
4501
4502 #[test_traced]
4503 fn test_fixed_journal_rejects_watermark_with_aligned_empty_tail() {
4504 let executor = deterministic::Runner::default();
4506 executor.start(|context| async move {
4507 let cfg = test_cfg(&context, NZU64!(5));
4508
4509 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4510 .await
4511 .unwrap();
4512 for i in 0..10u64 {
4513 journal.append(&test_digest(i)).await.unwrap();
4514 }
4515 journal.sync().await.unwrap();
4516 drop(journal);
4517
4518 let blob_part = blob_partition(&cfg);
4521 context.remove(&blob_part, None).await.unwrap();
4522 let (blob, _) = context.open(&blob_part, &1u64.to_be_bytes()).await.unwrap();
4523 blob.sync().await.unwrap();
4524
4525 let result = Journal::<_, Digest>::init(context.child("crash"), cfg.clone()).await;
4526 assert!(matches!(result, Err(Error::Corruption(_))));
4527 });
4528 }
4529
4530 #[test_traced]
4531 fn test_fixed_journal_rejects_far_watermark_with_aligned_empty_tail() {
4532 let executor = deterministic::Runner::default();
4534 executor.start(|context| async move {
4535 let cfg = test_cfg(&context, NZU64!(5));
4536
4537 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4538 .await
4539 .unwrap();
4540 for i in 0..10u64 {
4541 journal.append(&test_digest(i)).await.unwrap();
4542 }
4543 journal.sync().await.unwrap();
4544 drop(journal);
4545
4546 let blob_part = blob_partition(&cfg);
4549 context.remove(&blob_part, None).await.unwrap();
4550 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
4551 blob.sync().await.unwrap();
4552
4553 let result = Journal::<_, Digest>::init(context.child("crash"), cfg.clone()).await;
4554 assert!(matches!(result, Err(Error::Corruption(_))));
4555 });
4556 }
4557
4558 #[test_traced]
4559 fn test_read_many_empty() {
4560 let executor = deterministic::Runner::default();
4561 executor.start(|context| async move {
4562 let cfg = test_cfg(&context, NZU64!(10));
4563 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
4564 .await
4565 .unwrap();
4566
4567 let items = journal
4568 .snapshot()
4569 .await
4570 .unwrap()
4571 .read_many(&[])
4572 .await
4573 .unwrap();
4574 assert!(items.is_empty());
4575
4576 journal.destroy().await.unwrap();
4577 });
4578 }
4579
4580 #[test_traced]
4581 fn test_read_many_single_blob() {
4582 let executor = deterministic::Runner::default();
4584 executor.start(|context| async move {
4585 let cfg = test_cfg(&context, NZU64!(10));
4586 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4587
4588 for i in 0..5u64 {
4589 journal.append(&test_digest(i)).await.unwrap();
4590 }
4591 assert_eq!(journal.size(), 5);
4592
4593 let items = journal
4594 .snapshot()
4595 .await
4596 .unwrap()
4597 .read_many(&[0, 2, 4])
4598 .await
4599 .unwrap();
4600 assert_eq!(items, vec![test_digest(0), test_digest(2), test_digest(4)]);
4601
4602 journal.destroy().await.unwrap();
4603 });
4604 }
4605
4606 #[test_traced]
4607 #[should_panic(expected = "positions must be strictly increasing")]
4608 fn test_read_many_rejects_unsorted_positions() {
4609 let executor = deterministic::Runner::default();
4610 executor.start(|context| async move {
4611 let cfg = test_cfg(&context, NZU64!(10));
4612 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4613 for i in 0..5u64 {
4614 journal.append(&test_digest(i)).await.unwrap();
4615 }
4616
4617 let _ = journal.snapshot().await.unwrap().read_many(&[2, 1]).await;
4618 });
4619 }
4620
4621 #[test_traced]
4622 #[should_panic(expected = "positions must be strictly increasing")]
4623 fn test_read_many_rejects_duplicate_positions() {
4624 let executor = deterministic::Runner::default();
4626 executor.start(|context| async move {
4627 let cfg = test_cfg(&context, NZU64!(10));
4628 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4629 for i in 0..5u64 {
4630 journal.append(&test_digest(i)).await.unwrap();
4631 }
4632
4633 let _ = journal.snapshot().await.unwrap().read_many(&[1, 1]).await;
4634 });
4635 }
4636
4637 #[test_traced]
4638 fn test_read_many_across_blobs() {
4639 let executor = deterministic::Runner::default();
4641 executor.start(|context| async move {
4642 let cfg = test_cfg(&context, NZU64!(3));
4643 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4644
4645 for i in 0..9u64 {
4646 journal.append(&test_digest(i)).await.unwrap();
4647 }
4648 assert_eq!(journal.size(), 9);
4649 let items = journal
4652 .snapshot()
4653 .await
4654 .unwrap()
4655 .read_many(&[1, 4, 7])
4656 .await
4657 .unwrap();
4658 assert_eq!(items, vec![test_digest(1), test_digest(4), test_digest(7)]);
4659
4660 journal.destroy().await.unwrap();
4661 });
4662 }
4663
4664 #[test_traced]
4665 fn test_read_many_after_prune() {
4666 let executor = deterministic::Runner::default();
4668 executor.start(|context| async move {
4669 let cfg = test_cfg(&context, NZU64!(3));
4670 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4671
4672 for i in 0..9u64 {
4673 journal.append(&test_digest(i)).await.unwrap();
4674 }
4675 assert_eq!(journal.size(), 9);
4676 journal.sync().await.unwrap();
4677
4678 journal.prune(3).await.unwrap();
4680 assert_eq!(journal.bounds(), 3..9);
4681
4682 let items = journal
4683 .snapshot()
4684 .await
4685 .unwrap()
4686 .read_many(&[3, 5, 8])
4687 .await
4688 .unwrap();
4689 assert_eq!(items, vec![test_digest(3), test_digest(5), test_digest(8)]);
4690
4691 let err = journal
4693 .snapshot()
4694 .await
4695 .unwrap()
4696 .read_many(&[1])
4697 .await
4698 .unwrap_err();
4699 assert!(matches!(err, Error::ItemPruned(1)));
4700
4701 journal.destroy().await.unwrap();
4702 });
4703 }
4704
4705 #[test_traced]
4706 fn test_read_many_out_of_range() {
4707 let executor = deterministic::Runner::default();
4708 executor.start(|context| async move {
4709 let cfg = test_cfg(&context, NZU64!(10));
4710 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4711
4712 for i in 0..3u64 {
4713 journal.append(&test_digest(i)).await.unwrap();
4714 }
4715 assert_eq!(journal.size(), 3);
4716
4717 let err = journal
4718 .snapshot()
4719 .await
4720 .unwrap()
4721 .read_many(&[0, 5])
4722 .await
4723 .unwrap_err();
4724 assert!(matches!(err, Error::ItemOutOfRange(5)));
4725
4726 journal.destroy().await.unwrap();
4727 });
4728 }
4729
4730 #[test_traced]
4731 fn test_read_many_matches_read() {
4732 let executor = deterministic::Runner::default();
4734 executor.start(|context| async move {
4735 let cfg = test_cfg(&context, NZU64!(4));
4736 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4737
4738 for i in 0..20u64 {
4739 journal.append(&test_digest(i)).await.unwrap();
4740 }
4741 assert_eq!(journal.size(), 20);
4742 journal.sync().await.unwrap();
4743
4744 let positions: Vec<u64> = (0..20).collect();
4745 let reader = journal.snapshot().await.unwrap();
4746 let batch = reader.read_many(&positions).await.unwrap();
4747
4748 for &pos in &positions {
4749 let single = reader.read(pos).await.unwrap();
4750 assert_eq!(batch[pos as usize], single);
4751 }
4752 drop(reader);
4753
4754 journal.destroy().await.unwrap();
4755 });
4756 }
4757
4758 #[test_traced]
4759 fn test_try_read_many_sync_matches_read_many() {
4760 let executor = deterministic::Runner::default();
4763 executor.start(|context| async move {
4764 let cfg = test_cfg(&context, NZU64!(4));
4765 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4766
4767 for i in 0..20u64 {
4768 journal.append(&test_digest(i)).await.unwrap();
4769 }
4770 journal.sync().await.unwrap();
4771
4772 let positions: Vec<u64> = (0..20).collect();
4773 let reader = journal.snapshot().await.unwrap();
4774 let expected = reader.read_many(&positions).await.unwrap();
4775
4776 let served = reader.try_read_many_sync(&positions);
4779 assert_eq!(served.len(), positions.len());
4780 for (item, expected) in served.iter().zip(&expected) {
4781 if let Some(item) = item {
4782 assert_eq!(item, expected);
4783 }
4784 }
4785
4786 let tail: Vec<u64> = (16..20).collect();
4789 reader.read_many(&tail).await.unwrap();
4790 let served = reader.try_read_many_sync(&tail);
4791 for (item, pos) in served.iter().zip(&tail) {
4792 assert_eq!(
4793 item.as_ref().expect("warmed position is served"),
4794 &expected[*pos as usize]
4795 );
4796 }
4797
4798 assert!(reader.try_read_many_sync(&[0])[0].is_none());
4800
4801 let served = reader.try_read_many_sync(&[19, 20]);
4804 assert!(served[0].is_some());
4805 assert!(served[1].is_none());
4806 drop(served);
4807 drop(reader);
4808
4809 journal.rewind(18).await.unwrap();
4813 let reader = journal.snapshot().await.unwrap();
4814 reader.read_many(&[17]).await.unwrap();
4815 let served = reader.try_read_many_sync(&[17, 18]);
4816 assert!(served[0].is_some());
4817 assert!(served[1].is_none());
4818 drop(served);
4819 drop(reader);
4820
4821 journal.prune(8).await.unwrap();
4824 let reader = journal.snapshot().await.unwrap();
4825 reader.read_many(&[9]).await.unwrap();
4826 let served = reader.try_read_many_sync(&[3, 9]);
4827 assert!(served[0].is_none());
4828 assert!(served[1].is_some());
4829 drop(served);
4830 drop(reader);
4831
4832 journal.destroy().await.unwrap();
4833 });
4834 }
4835
4836 #[test_traced]
4837 fn test_probe_then_read_many_matches_read_many() {
4838 let executor = deterministic::Runner::default();
4841 executor.start(|context| async move {
4842 let cfg = test_cfg(&context, NZU64!(4));
4843 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
4844
4845 for i in 0..20u64 {
4846 journal.append(&test_digest(i)).await.unwrap();
4847 }
4848 journal.sync().await.unwrap();
4849
4850 let positions: Vec<u64> = (0..20).collect();
4851 let reader = journal.snapshot().await.unwrap();
4852 let expected: Vec<_> = (0..20).map(test_digest).collect();
4853 for _ in 0..2 {
4854 let mut served = reader.try_read_many_sync(&positions);
4855 let misses: Vec<u64> = positions
4856 .iter()
4857 .zip(&served)
4858 .filter_map(|(&pos, item)| item.is_none().then_some(pos))
4859 .collect();
4860 let mut fetched = reader.read_many(&misses).await.unwrap().into_iter();
4861 for item in served.iter_mut().filter(|item| item.is_none()) {
4862 *item = fetched.next();
4863 }
4864 let completed: Vec<_> = served.into_iter().map(Option::unwrap).collect();
4865 assert_eq!(completed, expected);
4866 }
4867 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
4868 drop(reader);
4869
4870 journal.destroy().await.unwrap();
4871 });
4872 }
4873
4874 #[test_traced]
4875 fn test_fixed_journal_metrics() {
4876 let executor = deterministic::Runner::default();
4877 executor.start(|context| async move {
4878 let cfg = test_cfg(&context, NZU64!(2));
4879 let mut journal =
4880 Journal::<_, Digest>::init(context.child("fixed_metrics"), cfg.clone())
4881 .await
4882 .unwrap();
4883
4884 let items: Vec<_> = (0..5).map(test_digest).collect();
4885 journal.append_many(Many::Flat(&items)).await.unwrap();
4886 journal.append(&test_digest(5)).await.unwrap();
4887 journal.commit().await.unwrap();
4888 journal.sync().await.unwrap();
4889 journal.snapshot().await.unwrap().read(0).await.unwrap();
4890 journal.snapshot().await.unwrap().try_read_sync(0).unwrap();
4891 journal
4892 .snapshot()
4893 .await
4894 .unwrap()
4895 .read_many(&[1, 2, 4])
4896 .await
4897 .unwrap();
4898 journal.prune(2).await.unwrap();
4899 journal.rewind(4).await.unwrap();
4900
4901 let buffer = context.encode();
4902 for expected in [
4903 "fixed_metrics_size 4",
4904 "fixed_metrics_pruning_boundary 2",
4905 "fixed_metrics_retained 2",
4906 "fixed_metrics_tail_items 2",
4907 "fixed_metrics_append_calls_total 1",
4908 "fixed_metrics_append_many_calls_total 1",
4909 "fixed_metrics_read_calls_total 1",
4910 "fixed_metrics_read_many_calls_total 1",
4911 "fixed_metrics_items_read_total 5",
4912 "fixed_metrics_commit_calls_total 1",
4913 "fixed_metrics_sync_calls_total 1",
4914 "fixed_metrics_append_duration_count 1",
4915 "fixed_metrics_append_many_duration_count 1",
4916 "fixed_metrics_read_duration_count 0",
4917 "fixed_metrics_read_many_duration_count 1",
4918 "fixed_metrics_commit_duration_count 1",
4919 "fixed_metrics_sync_duration_count 1",
4920 "fixed_metrics_cache_hits_total",
4921 "fixed_metrics_cache_misses_total",
4922 "fixed_metrics_blobs_tracked",
4923 ] {
4924 assert!(buffer.contains(expected), "{expected}\n{buffer}");
4925 }
4926
4927 journal.destroy().await.unwrap();
4928 });
4929 }
4930 #[test_traced]
4932 fn test_snapshot_frozen_across_roll() {
4933 let executor = deterministic::Runner::default();
4934 executor.start(|context| async move {
4935 let cfg = test_cfg(&context, NZU64!(5));
4936 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
4937 .await
4938 .unwrap();
4939 for i in 0..7u64 {
4940 journal.append(&test_digest(i)).await.unwrap();
4941 }
4942
4943 let snapshot = journal.snapshot().await.unwrap();
4944 assert_eq!(snapshot.bounds(), 0..7);
4945
4946 for i in 7..23u64 {
4949 journal.append(&test_digest(i)).await.unwrap();
4950 }
4951 assert_eq!(snapshot.bounds(), 0..7);
4952 for i in 0..7u64 {
4953 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
4954 }
4955 assert!(matches!(
4956 snapshot.read(7).await,
4957 Err(Error::ItemOutOfRange(7))
4958 ));
4959
4960 let fresh = journal.snapshot().await.unwrap();
4961 assert_eq!(fresh.bounds(), 0..23);
4962 assert_eq!(fresh.read(22).await.unwrap(), test_digest(22));
4963
4964 drop(snapshot);
4965 drop(fresh);
4966 journal.destroy().await.unwrap();
4967 });
4968 }
4969
4970 #[test_traced]
4973 fn test_prune_under_snapshot() {
4974 let executor = deterministic::Runner::default();
4975 executor.start(|context| async move {
4976 let cfg = test_cfg(&context, NZU64!(5));
4977 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
4978 .await
4979 .unwrap();
4980 for i in 0..17u64 {
4981 journal.append(&test_digest(i)).await.unwrap();
4982 }
4983 journal.sync().await.unwrap();
4984
4985 let snapshot = journal.snapshot().await.unwrap();
4986 assert!(journal.prune(12).await.unwrap());
4987
4988 assert_eq!(snapshot.bounds(), 0..17);
4990 for i in 0..17u64 {
4991 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
4992 }
4993
4994 let fresh = journal.snapshot().await.unwrap();
4995 assert_eq!(fresh.bounds(), 10..17);
4996 assert!(matches!(fresh.read(3).await, Err(Error::ItemPruned(3))));
4997
4998 drop(snapshot);
4999 drop(fresh);
5000 journal.destroy().await.unwrap();
5001 });
5002 }
5003
5004 #[test_traced]
5007 fn test_snapshots_readable_during_concurrent_appends() {
5008 let executor = deterministic::Runner::seeded(7);
5009 executor.start(|context| async move {
5010 let cfg = test_cfg(&context, NZU64!(5));
5011 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
5012 .await
5013 .unwrap();
5014
5015 let (mut tx, mut rx) =
5016 futures::channel::mpsc::channel::<Reader<'static, Context, Digest>>(8);
5017 let validator = context.child("validator").spawn(|_| async move {
5018 let mut validated = 0usize;
5019 while let Some(snapshot) = rx.next().await {
5020 let bounds = snapshot.bounds();
5021 for i in bounds.clone() {
5022 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
5023 }
5024 validated += (bounds.end - bounds.start) as usize;
5025 }
5026 validated
5027 });
5028
5029 for i in 0..40u64 {
5030 journal.append(&test_digest(i)).await.unwrap();
5031 if i % 7 == 0 {
5032 let snapshot = journal.snapshot().await.unwrap();
5033 if tx.try_send(snapshot).is_err() {
5034 break;
5035 }
5036 }
5037 }
5038 drop(tx);
5039 assert!(validator.await.unwrap() > 0);
5040
5041 journal.destroy().await.unwrap();
5042 });
5043 }
5044
5045 #[test_traced]
5048 fn test_replay_from_stale_snapshot() {
5049 let executor = deterministic::Runner::default();
5050 executor.start(|context| async move {
5051 let cfg = test_cfg(&context, NZU64!(5));
5052 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
5053 .await
5054 .unwrap();
5055 for i in 0..7u64 {
5056 journal.append(&test_digest(i)).await.unwrap();
5057 }
5058
5059 let snapshot = journal.snapshot().await.unwrap();
5061 assert_eq!(snapshot.bounds(), 0..7);
5062
5063 for i in 7..23u64 {
5065 journal.append(&test_digest(i)).await.unwrap();
5066 }
5067 assert!(journal.prune(12).await.unwrap());
5068
5069 {
5070 let stream = snapshot.replay(0, NZUsize!(1024)).await.unwrap();
5071 pin_mut!(stream);
5072 let mut expected = 0u64;
5073 while let Some(result) = stream.next().await {
5074 let (pos, item) = result.unwrap();
5075 assert_eq!(pos, expected);
5076 assert_eq!(item, test_digest(pos));
5077 expected += 1;
5078 }
5079 assert_eq!(expected, 7);
5080 }
5081
5082 drop(snapshot);
5083 journal.destroy().await.unwrap();
5084 });
5085 }
5086
5087 #[test_traced]
5088 fn test_read_many_sparse_sections_and_hit_accounting() {
5089 let executor = deterministic::Runner::default();
5093 executor.start(|context| async move {
5094 let mut cfg = test_cfg(&context, NZU64!(8));
5095 cfg.page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(16));
5098 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
5099
5100 for i in 0..50u64 {
5101 journal.append(&test_digest(i)).await.unwrap();
5102 }
5103 journal.sync().await.unwrap();
5104 journal.prune(11).await.unwrap();
5106
5107 let reader = journal.snapshot().await.unwrap();
5108
5109 let positions: Vec<u64> = vec![11, 12, 19, 20, 23, 31, 40, 47, 49];
5114 let expected_hits = positions
5115 .iter()
5116 .filter(|&&pos| reader.try_read_sync(pos).is_some())
5117 .count() as u64;
5118 let before = context.encode();
5119 let batch = reader.read_many(&positions).await.unwrap();
5120 let after = context.encode();
5121 assert_eq!(batch.len(), positions.len());
5122 assert_eq!(
5123 counter(&after, "cache_hits") - counter(&before, "cache_hits"),
5124 expected_hits,
5125 "batch read hit count should match the cached subset"
5126 );
5127 assert_eq!(
5128 counter(&after, "cache_misses") - counter(&before, "cache_misses"),
5129 positions.len() as u64 - expected_hits,
5130 "batch read miss count should cover the rest"
5131 );
5132 for (i, &pos) in positions.iter().enumerate() {
5133 let single = reader.read(pos).await.unwrap();
5134 assert_eq!(batch[i], single);
5135 assert_eq!(batch[i], test_digest(pos));
5136 }
5137
5138 let all: Vec<u64> = (11..50).collect();
5140 let batch = reader.read_many(&all).await.unwrap();
5141 for (i, &pos) in all.iter().enumerate() {
5142 assert_eq!(batch[i], reader.read(pos).await.unwrap());
5143 }
5144 drop(reader);
5145
5146 journal.destroy().await.unwrap();
5147 });
5148 }
5149
5150 #[test_traced]
5151 fn test_read_many_cold_blob_groups() {
5152 let executor = deterministic::Runner::default();
5157 executor.start(|context| async move {
5158 let mut cfg = test_cfg(&context, NZU64!(8));
5159 cfg.page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(32));
5160 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5161 .await
5162 .unwrap();
5163 for i in 0..40u64 {
5164 journal.append(&test_digest(i)).await.unwrap();
5165 }
5166 journal.sync().await.unwrap();
5167 drop(journal);
5168
5169 cfg.page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(32));
5173 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
5174 .await
5175 .unwrap();
5176 let reader = journal.snapshot().await.unwrap();
5177 let positions = [1, 5, 10, 14, 17, 22, 25, 30];
5178 for pos in positions {
5179 assert!(reader.try_read_sync(pos).is_none(), "position {pos}");
5180 }
5181
5182 let before = context.encode();
5184 let batch = reader.read_many(&positions).await.unwrap();
5185 let after = context.encode();
5186 assert_eq!(
5187 counter(&after, "second_cache_misses") - counter(&before, "second_cache_misses"),
5188 positions.len() as u64
5189 );
5190 assert_eq!(
5191 counter(&after, "second_cache_hits"),
5192 counter(&before, "second_cache_hits")
5193 );
5194 for (i, &pos) in positions.iter().enumerate() {
5195 assert_eq!(batch[i], test_digest(pos), "position {pos}");
5196 }
5197
5198 let before = context.encode();
5201 let batch = reader.read_many(&positions).await.unwrap();
5202 let after = context.encode();
5203 assert_eq!(
5204 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
5205 positions.len() as u64
5206 );
5207 for (i, &pos) in positions.iter().enumerate() {
5208 assert_eq!(batch[i], test_digest(pos), "position {pos}");
5209 }
5210 drop(reader);
5211
5212 journal.destroy().await.unwrap();
5213 });
5214 }
5215
5216 #[test_traced]
5217 fn test_fixed_journal_read_miss_timed() {
5218 let executor = deterministic::Runner::default();
5220 executor.start(|context| async move {
5221 let mut journal =
5222 Journal::<_, Digest>::init(context.child("miss"), test_cfg(&context, NZU64!(2)))
5223 .await
5224 .unwrap();
5225 for i in 0..20 {
5226 journal.append(&test_digest(i)).await.unwrap();
5227 }
5228 journal.sync().await.unwrap();
5229
5230 let reader = journal.snapshot().await.unwrap();
5232 let pos = (0..20)
5233 .find(|&pos| reader.try_read_sync(pos).is_none())
5234 .expect("some position should be cold");
5235 assert_eq!(reader.read(pos).await.unwrap(), test_digest(pos));
5236 drop(reader);
5237
5238 let buffer = context.encode();
5239 assert!(buffer.contains("miss_read_duration_count 1"), "{buffer}");
5240
5241 journal.destroy().await.unwrap();
5242 });
5243 }
5244}