1use super::{
135 blobs::{Blob, Blobs, Partition, Replay as BlobReplay, Writable},
136 checkpoint::Checkpoint,
137};
138#[commonware_macros::stability(ALPHA)]
139use crate::journal::authenticated;
140use crate::{
141 Context, SyncCompletion,
142 journal::{
143 Error,
144 contiguous::{Many, Mutable, metrics::Metrics},
145 durability::Barrier,
146 },
147};
148use commonware_codec::{CodecFixedShared, DecodeExt as _, ReadExt as _};
149use commonware_runtime::{
150 Blob as RBlob, Buf, Handle, IoBuf, ReadOptions,
151 buffer::paged::{CacheRef, Writer},
152};
153use commonware_utils::Cached;
154use futures::{FutureExt as _, Stream, future::try_join_all};
155use std::{
156 collections::BTreeMap,
157 future::Future,
158 marker::PhantomData,
159 num::{NonZeroU64, NonZeroUsize},
160 ops::Range,
161 sync::Arc,
162};
163use tracing::warn;
164
165commonware_utils::thread_local_cache!(static PROBE_SCRATCH: Vec<u8>);
169
170pub struct PreparedAppend<A> {
173 buf: Vec<u8>,
174 _marker: PhantomData<A>,
175}
176
177#[inline]
179fn first_in_blob(pruning_boundary: u64, blob: u64, items_per_blob: u64) -> Result<u64, Error> {
180 let start = super::blob_first_position(blob, items_per_blob)?;
181 Ok(pruning_boundary.max(start))
182}
183
184fn replay_stream<'a, B: RBlob, A: CodecFixedShared>(
190 blobs: &Blobs<'a, B>,
191 bounds: Range<u64>,
192 items_per_blob: NonZeroU64,
193 start_pos: u64,
194 buffer: NonZeroUsize,
195 read_options: ReadOptions,
196) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send + use<'a, B, A>, Error> {
197 if start_pos > bounds.end {
198 return Err(Error::ItemOutOfRange(start_pos));
199 }
200 if start_pos < bounds.start {
201 return Err(Error::ItemPruned(start_pos));
202 }
203
204 let mut states = Vec::new();
205 if start_pos < bounds.end {
206 let items_per_blob = items_per_blob.get();
207 let start_blob = super::position_to_blob(start_pos, items_per_blob);
208 let end_blob = super::position_to_blob(bounds.end - 1, items_per_blob);
209 let items_per_batch = (buffer.get() / A::SIZE).max(1);
210
211 for blob in start_blob..=end_blob {
212 let blob_first = first_in_blob(bounds.start, blob, items_per_blob)?;
215 let first_pos = if blob == start_blob {
216 start_pos
217 } else {
218 blob_first
219 };
220 let blob_end = super::blob_end_position(blob, items_per_blob, bounds.end);
221 let offset = (first_pos - blob_first)
222 .checked_mul(A::SIZE as u64)
223 .ok_or(Error::OffsetOverflow)?;
224 let blob = blobs
225 .get(blob)
226 .expect("positions in bounds map to a retained blob");
227
228 states.push(FixedReplayState::<B, A> {
229 replay: blob.replay_from(offset, buffer, read_options)?,
230 pos: first_pos,
231 end_pos: blob_end,
232 items_per_batch,
233 _marker: PhantomData,
234 });
235 }
236 }
237
238 Ok(super::replay_stream_from_states(states))
239}
240
241struct FixedReplayState<'a, B: RBlob, A> {
243 replay: BlobReplay<'a, B>,
245 pos: u64,
247 end_pos: u64,
249 items_per_batch: usize,
251 _marker: PhantomData<A>,
252}
253
254impl<B: RBlob, A: CodecFixedShared> super::ReplayBatchState for FixedReplayState<'_, B, A> {
255 type Item = A;
256
257 async fn next_batch(mut self) -> Option<(Vec<Result<(u64, A), Error>>, Self)> {
259 if self.pos == self.end_pos {
260 return None;
261 }
262
263 let mut batch = Vec::new();
266 match self.replay.ensure(A::SIZE).await {
267 Ok(true) => {}
268 Ok(false) => {
269 batch.push(Err(Error::Corruption(format!(
270 "blob ended before position {}",
271 self.pos
272 ))));
273 self.pos = self.end_pos;
274 return Some((batch, self));
275 }
276 Err(err) => {
277 batch.push(Err(err));
278 self.pos = self.end_pos;
279 return Some((batch, self));
280 }
281 }
282
283 let available = (self.replay.remaining() / A::SIZE) as u64;
286 let remaining = self.end_pos - self.pos;
287 let count = available.min(self.items_per_batch as u64).min(remaining) as usize;
288 let Some(next_pos) = self.pos.checked_add(count as u64) else {
289 batch.push(Err(Error::OffsetOverflow));
290 self.pos = self.end_pos;
291 return Some((batch, self));
292 };
293 batch.reserve(count);
294
295 let base = self.pos;
296 for i in 0..count {
297 match A::read(&mut self.replay) {
298 Ok(item) => batch.push(Ok((base + i as u64, item))),
299 Err(err) => {
300 batch.push(Err(Error::Codec(err)));
301 self.pos = self.end_pos;
302 return Some((batch, self));
303 }
304 }
305 }
306 self.pos = next_pos;
307 Some((batch, self))
308 }
309}
310
311enum BlobFill {
313 Full { len: u64 },
314 Short { len: u64 },
315 Overfull { len: u64, capacity: u64 },
316}
317
318struct RecoveredBounds {
321 size: u64,
323 recovery_watermark: u64,
325 repair: Option<u64>,
328}
329
330#[derive(Clone)]
332pub struct Config {
333 pub partition: String,
338
339 pub items_per_blob: NonZeroU64,
345
346 pub page_cache: CacheRef,
348
349 pub write_buffer: NonZeroUsize,
351
352 pub replay_buffer: NonZeroUsize,
354}
355
356pub(super) struct Inner<E: Context, A> {
358 blobs: Writable<E>,
360
361 checkpoint: Checkpoint<E>,
363
364 bounds: Range<u64>,
366
367 items_per_blob: NonZeroU64,
369
370 metrics: Arc<Metrics<E>>,
372
373 barrier: Barrier,
375
376 _phantom: PhantomData<A>,
377}
378
379impl<E: Context, A: CodecFixedShared> Inner<E, A> {
380 pub const CHUNK_SIZE: NonZeroUsize = match NonZeroUsize::new(A::SIZE) {
383 Some(size) => size,
384 None => panic!("journal item size must be nonzero"),
385 };
386
387 pub const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE.get() as u64;
389
390 fn items_to_bytes(items: u64) -> Result<u64, Error> {
392 items
393 .checked_mul(Self::CHUNK_SIZE_U64)
394 .ok_or(Error::OffsetOverflow)
395 }
396
397 fn from_blobs(
399 blobs: Writable<E>,
400 checkpoint: Checkpoint<E>,
401 bounds: Range<u64>,
402 items_per_blob: NonZeroU64,
403 metrics: Metrics<E>,
404 ) -> Self {
405 Self {
406 blobs,
407 barrier: Barrier::new(
408 checkpoint
409 .watermark()
410 .expect("recovery watermark must exist after init"),
411 ),
412 checkpoint,
413 bounds,
414 items_per_blob,
415 metrics: Arc::new(metrics),
416 _phantom: PhantomData,
417 }
418 }
419
420 pub(crate) async fn init(context: E, cfg: Config) -> Result<Self, Error> {
422 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
423 Self::init_with_checkpoint(context, cfg, checkpoint).await
424 }
425
426 async fn init_with_checkpoint(
428 context: E,
429 cfg: Config,
430 checkpoint: Checkpoint<E>,
431 ) -> Result<Self, Error> {
432 if let Some(clear_target) = checkpoint.clear_target() {
435 return Self::complete_staged_clear(context, cfg, checkpoint, clear_target).await;
436 }
437
438 let (blob_partition, names) = Partition::select(&context, &cfg.partition).await?;
443 let partition = Partition::new(
444 context.child("blobs"),
445 blob_partition,
446 cfg.page_cache,
447 cfg.write_buffer,
448 );
449 let mut pending = partition.open_many(names).await?;
450 let items_per_blob = cfg.items_per_blob.get();
451 let pruning_boundary = Self::recover_pruning_boundary(
452 checkpoint.boundary_hint(),
453 pending.keys().next().copied(),
454 items_per_blob,
455 )?;
456
457 let floor = checkpoint.watermark().unwrap_or(0);
467 let floor_blob = super::position_to_blob(floor, items_per_blob);
468 let suspects: Vec<u64> = pending.keys().rev().take(2).copied().collect();
469 for blob in suspects {
470 if blob < floor_blob {
471 continue;
472 }
473
474 let acknowledged = if blob == floor_blob {
477 Self::items_to_bytes(floor.saturating_sub(first_in_blob(
478 pruning_boundary,
479 blob,
480 items_per_blob,
481 )?))?
482 } else {
483 0
484 };
485 let writer = pending.get_mut(&blob).expect("suspect blob is present");
486 let recoverable = writer
487 .recoverable_prefix_len(acknowledged, cfg.replay_buffer, ReadOptions::default())
488 .await?;
489 let valid = Self::items_to_bytes(recoverable / Self::CHUNK_SIZE_U64)?;
490 if valid == writer.size() {
491 continue;
492 }
493
494 if valid < acknowledged {
497 return Err(Error::Corruption(format!(
498 "blob {blob} no longer backs acknowledged items: well-formed prefix {valid} \
499 of size {}",
500 writer.size()
501 )));
502 }
503 warn!(
504 blob,
505 valid,
506 size = writer.size(),
507 "truncating to recoverable item prefix"
508 );
509 writer.resize(valid).await?;
510 writer.sync().await?;
511 }
512
513 let RecoveredBounds {
514 size,
515 recovery_watermark,
516 repair,
517 } = Self::recover_bounds(
518 &pending,
519 items_per_blob,
520 pruning_boundary,
521 checkpoint.watermark(),
522 )?;
523
524 let checkpoint = checkpoint
527 .persist(
528 cfg.items_per_blob.get(),
529 pruning_boundary,
530 recovery_watermark,
531 )
532 .await?;
533
534 let tail_blob = super::position_to_blob(size, cfg.items_per_blob.get());
538 if let Some(truncate_to) = repair {
539 while let Some((&newest, _)) = pending.last_key_value() {
540 if newest <= tail_blob {
541 break;
542 }
543 drop(pending.remove(&newest));
544 partition.remove(newest).await?;
545 }
546 if let Some(writer) = pending.get_mut(&tail_blob)
547 && truncate_to < writer.size()
548 {
549 writer.resize(truncate_to).await?;
550 writer.sync().await?;
551 }
552 }
553
554 let blobs = Writable::recover(partition, pending, tail_blob).await?;
556
557 let metrics = Metrics::new(context);
558 metrics.update(size, pruning_boundary, cfg.items_per_blob.get());
559
560 Ok(Self::from_blobs(
561 blobs,
562 checkpoint,
563 pruning_boundary..size,
564 cfg.items_per_blob,
565 metrics,
566 ))
567 }
568
569 async fn complete_staged_clear(
572 context: E,
573 cfg: Config,
574 checkpoint: Checkpoint<E>,
575 clear_target: u64,
576 ) -> Result<Self, Error> {
577 warn!(clear_target, "crash repair: completing interrupted clear");
578 let new_partition = format!("{}-blobs", cfg.partition);
579 Partition::<E>::remove_all(&context, &cfg.partition).await?;
580 Partition::<E>::remove_all(&context, &new_partition).await?;
581 let partition = Partition::new(
582 context.child("blobs"),
583 new_partition,
584 cfg.page_cache,
585 cfg.write_buffer,
586 );
587 let tail_blob = super::position_to_blob(clear_target, cfg.items_per_blob.get());
588 let blobs = Writable::recover(partition, BTreeMap::new(), tail_blob).await?;
589 let checkpoint = checkpoint
590 .finish_clear(cfg.items_per_blob.get(), clear_target)
591 .await?;
592
593 let metrics = Metrics::new(context);
594 metrics.update(clear_target, clear_target, cfg.items_per_blob.get());
595 Ok(Self::from_blobs(
596 blobs,
597 checkpoint,
598 clear_target..clear_target,
599 cfg.items_per_blob,
600 metrics,
601 ))
602 }
603
604 fn recover_bounds(
611 pending: &BTreeMap<u64, Writer<E::Blob>>,
612 items_per_blob: u64,
613 pruning_boundary: u64,
614 watermark_hint: Option<u64>,
615 ) -> Result<RecoveredBounds, Error> {
616 let (size, repair) =
617 Self::recover_by_walking_lengths(pending, items_per_blob, pruning_boundary)?;
618
619 let recovery_watermark = match watermark_hint {
620 Some(watermark) if watermark > size => {
621 return Err(Error::Corruption(format!(
625 "recovery watermark {watermark} exceeds recoverable size {size}"
626 )));
627 }
628 Some(watermark) => watermark,
629 None if repair.is_some() => {
630 return Err(Error::Corruption(
633 "legacy journal has a short non-tail blob".into(),
634 ));
635 }
636 None => first_in_blob(
639 pruning_boundary,
640 super::position_to_blob(size, items_per_blob),
641 items_per_blob,
642 )?,
643 };
644
645 Ok(RecoveredBounds {
646 size,
647 recovery_watermark,
648 repair,
649 })
650 }
651
652 fn recover_pruning_boundary(
658 boundary_hint: Option<u64>,
659 oldest_blob: Option<u64>,
660 items_per_blob: u64,
661 ) -> Result<u64, Error> {
662 let blob_boundary = match oldest_blob {
663 Some(oldest) => super::blob_first_position(oldest, items_per_blob)?,
664 None => 0,
665 };
666
667 let Some(boundary_hint) = boundary_hint else {
668 return Ok(blob_boundary);
669 };
670 if boundary_hint.is_multiple_of(items_per_blob) {
671 return Ok(blob_boundary);
672 }
673
674 let hint_blob = super::position_to_blob(boundary_hint, items_per_blob);
675 match oldest_blob {
676 Some(oldest_blob) if hint_blob == oldest_blob => Ok(boundary_hint),
677 Some(oldest_blob) if hint_blob < oldest_blob => {
678 warn!(
679 hint_blob,
680 oldest_blob, "crash repair: boundary hint stale, computing from blobs"
681 );
682 Ok(blob_boundary)
683 }
684 Some(oldest_blob) => {
685 Err(Error::Corruption(format!(
688 "boundary hint references blob {hint_blob} \
689 but oldest blob is blob {oldest_blob}"
690 )))
691 }
692 None => {
693 Err(Error::Corruption(format!(
697 "boundary hint references blob {hint_blob} but no blobs exist"
698 )))
699 }
700 }
701 }
702
703 fn classify_fill(
706 pending: &BTreeMap<u64, Writer<E::Blob>>,
707 items_per_blob: u64,
708 pruning_boundary: u64,
709 blob: u64,
710 ) -> Result<BlobFill, Error> {
711 let len = pending
712 .get(&blob)
713 .map_or(0, |writer| writer.size() / Self::CHUNK_SIZE_U64);
714 let start = super::blob_first_position(blob, items_per_blob)?;
717 let skipped = pruning_boundary.saturating_sub(start).min(items_per_blob);
718 let capacity = items_per_blob - skipped;
719 Ok(match len.cmp(&capacity) {
720 std::cmp::Ordering::Less => BlobFill::Short { len },
721 std::cmp::Ordering::Equal => BlobFill::Full { len },
722 std::cmp::Ordering::Greater => BlobFill::Overfull { len, capacity },
723 })
724 }
725
726 fn recover_by_walking_lengths(
733 pending: &BTreeMap<u64, Writer<E::Blob>>,
734 items_per_blob: u64,
735 pruning_boundary: u64,
736 ) -> Result<(u64, Option<u64>), Error> {
737 let oldest = pending.keys().next().copied();
738 let newest = pending.keys().next_back().copied();
739
740 let (Some(oldest), Some(newest)) = (oldest, newest) else {
741 return Ok((pruning_boundary, None));
742 };
743
744 let mut size = pruning_boundary;
745 for blob in oldest..=newest {
746 let fill = Self::classify_fill(pending, items_per_blob, pruning_boundary, blob)?;
747 match fill {
748 BlobFill::Full { len } => {
750 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
751 }
752 BlobFill::Short { len } if blob == newest => {
754 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
755 return Ok((size, None));
756 }
757 BlobFill::Short { len } => {
760 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
761 return Ok((size, Some(Self::items_to_bytes(len)?)));
762 }
763 BlobFill::Overfull { len, capacity } => {
764 return Err(Error::Corruption(format!(
765 "blob {blob} has too many items: expected at most {capacity}, got {len}"
766 )));
767 }
768 }
769 }
770
771 Ok((size, None))
772 }
773
774 #[commonware_macros::stability(ALPHA)]
776 pub(crate) async fn init_at_size(context: E, cfg: Config, size: u64) -> Result<Self, Error> {
777 Partition::select(&context, &cfg.partition).await?;
779 Self::init_at_size_cleared(context, cfg, size, || async { Ok(()) }).await
780 }
781
782 #[commonware_macros::stability(ALPHA)]
789 pub(in crate::journal::contiguous) async fn init_at_size_cleared<F, Fut>(
790 context: E,
791 cfg: Config,
792 size: u64,
793 clear_dependents: F,
794 ) -> Result<Self, Error>
795 where
796 F: FnOnce() -> Fut,
797 Fut: Future<Output = Result<(), Error>>,
798 {
799 if size == u64::MAX {
802 return Err(Error::SizeOverflow);
803 }
804
805 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
808 let checkpoint = checkpoint.stage_clear(size).await?;
809 clear_dependents().await?;
810 Self::init_with_checkpoint(context, cfg, checkpoint).await
811 }
812
813 pub(in crate::journal::contiguous) async fn init_cleared<F, Fut>(
820 context: E,
821 cfg: Config,
822 clear_dependents: F,
823 ) -> Result<Self, Error>
824 where
825 F: FnOnce() -> Fut,
826 Fut: Future<Output = Result<(), Error>>,
827 {
828 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
829 if checkpoint.clear_target().is_some() {
830 clear_dependents().await?;
831 }
832 Self::init_with_checkpoint(context, cfg, checkpoint).await
833 }
834
835 pub(super) async fn start_data_sync(mut self: Box<Self>) -> (Box<Self>, Handle<()>) {
837 let handle = self.blobs.start_sync().await;
838 let completion: SyncCompletion = handle.boxed().shared();
839 self.barrier.record(self.bounds.end, completion.clone());
840 (self, Handle::from_future(completion))
841 }
842
843 pub(super) async fn start_watermark_sync(
845 mut self: Box<Self>,
846 size: u64,
847 ) -> Result<(Box<Self>, Handle<()>), Error> {
848 let size = size.min(self.barrier.boundary());
849 let (checkpoint, handle) = self.checkpoint.start_watermark_sync(size).await?;
850 self.checkpoint = checkpoint;
851 Ok((self, handle))
852 }
853
854 pub(crate) async fn start_sync(self: Box<Self>) -> Result<(Box<Self>, Handle<()>), Error> {
856 self.metrics.start_sync_calls.inc();
857 let (mut journal, data) = self.start_data_sync().await;
858 let size = journal.barrier.boundary();
859 let (journal, watermark) = journal.start_watermark_sync(size).await?;
860 let handle = Handle::from_future(async move {
861 data.await?;
862 watermark.await
863 });
864 Ok((journal, handle))
865 }
866
867 pub(crate) async fn commit(mut self: Box<Self>) -> Result<Box<Self>, Error> {
869 let _timer = self.metrics.commit_timer();
870 self.metrics.commit_calls.inc();
871 let size = self.bounds.end;
872 let handle = self.blobs.start_sync().await;
873 handle.await?;
874 self.barrier.mark_durable(size);
875 Ok(self)
876 }
877
878 pub(crate) async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
880 let _timer = self.metrics.sync_timer();
881 self.metrics.sync_calls.inc();
882 let size = self.bounds.end;
883 let handle = self.blobs.start_sync().await;
884 handle.await?;
885 self.barrier.mark_durable(size);
886 self.checkpoint = self
887 .checkpoint
888 .persist(self.items_per_blob.get(), self.bounds.start, size)
889 .await?;
890 Ok(self)
891 }
892
893 pub(crate) async fn snapshot(&mut self) -> Result<Reader<'static, E, A>, Error> {
895 Ok(Reader {
896 blobs: self.blobs.snapshot().await?,
897 bounds: self.bounds.clone(),
898 items_per_blob: self.items_per_blob,
899 metrics: self.metrics.clone(),
900 _phantom: PhantomData,
901 })
902 }
903
904 pub(super) fn reader(&self) -> Reader<'_, E, A> {
906 Reader {
907 blobs: self.blobs.reader(),
908 bounds: self.bounds.clone(),
909 items_per_blob: self.items_per_blob,
910 metrics: self.metrics.clone(),
911 _phantom: PhantomData,
912 }
913 }
914
915 pub(super) fn recovery_watermark(&self) -> u64 {
917 self.checkpoint
918 .watermark()
919 .expect("recovery watermark must exist after init")
920 }
921
922 pub const fn size(&self) -> u64 {
925 self.bounds.end
926 }
927
928 pub(crate) async fn append(&mut self, item: &A) -> Result<u64, Error> {
930 let _timer = self.metrics.append_timer();
931 self.metrics.append_calls.inc();
932 self.append_many_inner(Many::Flat(std::slice::from_ref(item)))
933 .await
934 }
935
936 pub(crate) async fn append_many<'a>(&'a mut self, items: Many<'a, A>) -> Result<u64, Error> {
938 let _timer = self.metrics.append_many_timer();
939 self.metrics.append_many_calls.inc();
940 self.append_many_inner(items).await
941 }
942
943 async fn append_many_inner<'a>(&'a mut self, items: Many<'a, A>) -> Result<u64, Error> {
945 let prepared = self.prepare_append(items);
946 self.write_encoded(prepared).await
947 }
948
949 pub(crate) fn prepare_append(&self, items: Many<'_, A>) -> PreparedAppend<A> {
951 let mut buf = Vec::with_capacity(items.len() * A::SIZE);
954 match items {
955 Many::Flat(items) => {
956 for item in items {
957 item.write(&mut buf);
958 }
959 }
960 Many::Nested(nested_items) => {
961 for items in nested_items {
962 for item in *items {
963 item.write(&mut buf);
964 }
965 }
966 }
967 }
968 PreparedAppend {
969 buf,
970 _marker: PhantomData,
971 }
972 }
973
974 pub(crate) async fn append_prepared(
976 &mut self,
977 prepared: PreparedAppend<A>,
978 ) -> Result<u64, Error> {
979 let _timer = self.metrics.append_prepared_timer();
980 self.metrics.append_prepared_calls.inc();
981 self.write_encoded(prepared).await
982 }
983
984 async fn write_encoded(&mut self, prepared: PreparedAppend<A>) -> Result<u64, Error> {
986 let items_buf = prepared.buf;
987 let items_count = items_buf.len() / A::SIZE;
988 if items_count == 0 {
989 return Err(Error::EmptyAppend);
990 }
991 let items_buf = IoBuf::from(items_buf);
992
993 self.bounds
996 .end
997 .checked_add(items_count as u64)
998 .ok_or(Error::SizeOverflow)?;
999
1000 let mut written = 0;
1001 while written < items_count {
1002 let batch_count = super::batch_count_to_blob_boundary(
1003 self.bounds.end,
1004 items_count - written,
1005 self.items_per_blob.get(),
1006 );
1007 let start = written * A::SIZE;
1008 let end = start + batch_count * A::SIZE;
1009 let new_size = self.bounds.end + batch_count as u64;
1011
1012 self.blobs
1013 .tail_writer()
1014 .append_owned(items_buf.slice(start..end))
1015 .await?;
1016 self.bounds.end = new_size;
1017 written += batch_count;
1018
1019 if new_size.is_multiple_of(self.items_per_blob.get()) {
1021 self.blobs.seal_tail().await?;
1022 }
1023 }
1024
1025 self.metrics.update(
1026 self.bounds.end,
1027 self.bounds.start,
1028 self.items_per_blob.get(),
1029 );
1030 Ok(self.bounds.end - 1)
1031 }
1032
1033 pub(crate) async fn rewind(mut self: Box<Self>, size: u64) -> Result<Box<Self>, Error> {
1035 match size.cmp(&self.bounds.end) {
1036 std::cmp::Ordering::Greater => return Err(Error::InvalidRewind(size)),
1037 std::cmp::Ordering::Equal => return Ok(self),
1038 std::cmp::Ordering::Less => {}
1039 }
1040
1041 if size < self.bounds.start {
1042 return Err(Error::ItemPruned(size));
1043 }
1044
1045 let blob = super::position_to_blob(size, self.items_per_blob.get());
1046 let pos_in_blob = size - first_in_blob(self.bounds.start, blob, self.items_per_blob.get())?;
1047 let byte_offset = Self::items_to_bytes(pos_in_blob)?;
1048
1049 if self.checkpoint.lower_watermark(size) {
1051 self.checkpoint = self.checkpoint.sync().await?;
1052 }
1053
1054 if blob == self.blobs.tail_blob_index() {
1055 self.blobs.rewind_tail(byte_offset).await?;
1056 } else {
1057 self.blobs.rewind_into_sealed(blob, byte_offset).await?;
1058 }
1059
1060 self.bounds.end = size;
1061 self.barrier.truncate(size);
1062 self.metrics.update(
1063 self.bounds.end,
1064 self.bounds.start,
1065 self.items_per_blob.get(),
1066 );
1067
1068 Ok(self)
1069 }
1070
1071 pub const fn pruning_boundary(&self) -> u64 {
1073 self.bounds.start
1074 }
1075
1076 pub(crate) async fn prune(
1078 mut self: Box<Self>,
1079 min_item_pos: u64,
1080 ) -> Result<(Box<Self>, bool), Error> {
1081 let target_blob = super::position_to_blob(min_item_pos, self.items_per_blob.get());
1084 let tail_blob = super::position_to_blob(self.bounds.end, self.items_per_blob.get());
1085 let min_blob = std::cmp::min(target_blob, tail_blob);
1086
1087 if min_blob <= self.blobs.oldest_blob_index() {
1088 return Ok((self, false));
1089 }
1090
1091 let sync = self.blobs.start_sync().await;
1099 sync.await?;
1100 self.barrier.mark_durable(self.bounds.end);
1101
1102 let new_boundary = super::blob_first_position(min_blob, self.items_per_blob.get())?;
1103 self.blobs.prune(min_blob).await?;
1104 self.bounds.start = new_boundary;
1105
1106 self.metrics.update(
1107 self.bounds.end,
1108 self.bounds.start,
1109 self.items_per_blob.get(),
1110 );
1111
1112 Ok((self, true))
1113 }
1114
1115 pub(crate) async fn destroy(self) -> Result<(), Error> {
1117 self.blobs.destroy().await?;
1118 self.checkpoint.destroy().await?;
1119 Ok(())
1120 }
1121
1122 pub(crate) async fn clear_to_size(
1132 mut self: Box<Self>,
1133 new_size: u64,
1134 ) -> Result<Box<Self>, Error> {
1135 if new_size == u64::MAX {
1137 return Err(Error::SizeOverflow);
1138 }
1139
1140 self.checkpoint = self.checkpoint.stage_clear(new_size).await?;
1142
1143 self.blobs
1145 .clear(super::position_to_blob(new_size, self.items_per_blob.get()))
1146 .await?;
1147 self.bounds = new_size..new_size;
1148 self.barrier = Barrier::new(new_size);
1149
1150 self.checkpoint = self
1152 .checkpoint
1153 .finish_clear(self.items_per_blob.get(), new_size)
1154 .await?;
1155
1156 self.metrics.update(
1157 self.bounds.end,
1158 self.bounds.start,
1159 self.items_per_blob.get(),
1160 );
1161 Ok(self)
1162 }
1163
1164 #[commonware_macros::stability(ALPHA)]
1171 pub(super) async fn stage_clear_intent(
1172 mut self: Box<Self>,
1173 new_size: u64,
1174 ) -> Result<Box<Self>, Error> {
1175 if new_size == u64::MAX {
1177 return Err(Error::SizeOverflow);
1178 }
1179 self.checkpoint = self.checkpoint.stage_clear(new_size).await?;
1180 Ok(self)
1181 }
1182}
1183
1184pub struct Journal<E: Context, A>(Box<Inner<E, A>>);
1198
1199impl<E: Context, A: CodecFixedShared> std::fmt::Debug for Journal<E, A> {
1200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1201 f.debug_struct("Journal")
1202 .field("bounds", &super::Contiguous::bounds(self))
1203 .finish_non_exhaustive()
1204 }
1205}
1206
1207impl<E: Context, A: CodecFixedShared> Journal<E, A> {
1208 pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
1215 Ok(Self(Box::new(Inner::init(context, cfg).await?)))
1216 }
1217
1218 #[commonware_macros::stability(ALPHA)]
1226 pub async fn init_at_size(context: E, cfg: Config, size: u64) -> Result<Self, Error> {
1227 Ok(Self(Box::new(
1228 Inner::init_at_size(context, cfg, size).await?,
1229 )))
1230 }
1231
1232 #[commonware_macros::stability(ALPHA)]
1234 pub(crate) async fn clear_to_size(mut self, new_size: u64) -> Result<Self, Error> {
1235 self.0 = self.0.clear_to_size(new_size).await?;
1236 Ok(self)
1237 }
1238
1239 pub async fn commit(mut self) -> Result<Self, Error> {
1245 self.0 = self.0.commit().await?;
1246 Ok(self)
1247 }
1248
1249 pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
1262 let (inner, handle) = self.0.start_sync().await?;
1263 self.0 = inner;
1264 Ok((self, handle))
1265 }
1266
1267 pub async fn sync(mut self) -> Result<Self, Error> {
1272 self.0 = self.0.sync().await?;
1273 Ok(self)
1274 }
1275
1276 pub async fn snapshot(mut self) -> Result<(Self, Reader<'static, E, A>), Error> {
1282 let reader = self.0.snapshot().await?;
1283 Ok((self, reader))
1284 }
1285
1286 pub fn size(&self) -> u64 {
1289 self.0.size()
1290 }
1291
1292 pub async fn append(mut self, item: &A) -> Result<(Self, u64), Error> {
1298 let position = self.0.append(item).await?;
1299 Ok((self, position))
1300 }
1301
1302 pub async fn append_many(mut self, items: Many<'_, A>) -> Result<(Self, u64), Error> {
1306 let position = self.0.append_many(items).await?;
1307 Ok((self, position))
1308 }
1309
1310 pub fn prepare_append(&self, items: Many<'_, A>) -> PreparedAppend<A> {
1315 self.0.prepare_append(items)
1316 }
1317
1318 pub async fn append_prepared(
1323 mut self,
1324 prepared: PreparedAppend<A>,
1325 ) -> Result<(Self, u64), Error> {
1326 let position = self.0.append_prepared(prepared).await?;
1327 Ok((self, position))
1328 }
1329
1330 pub async fn rewind(mut self, size: u64) -> Result<Self, Error> {
1345 self.0 = self.0.rewind(size).await?;
1346 Ok(self)
1347 }
1348
1349 pub fn pruning_boundary(&self) -> u64 {
1351 self.0.pruning_boundary()
1352 }
1353
1354 pub async fn prune(mut self, min_item_pos: u64) -> Result<(Self, bool), Error> {
1364 let (inner, pruned) = self.0.prune(min_item_pos).await?;
1365 self.0 = inner;
1366 Ok((self, pruned))
1367 }
1368
1369 pub async fn destroy(self) -> Result<(), Error> {
1377 self.0.destroy().await
1378 }
1379}
1380
1381pub struct Reader<'a, E: Context, A> {
1383 blobs: Blobs<'a, E::Blob>,
1384 bounds: Range<u64>,
1385 items_per_blob: NonZeroU64,
1386 metrics: Arc<Metrics<E>>,
1387 _phantom: PhantomData<A>,
1388}
1389
1390impl<E: Context, A: CodecFixedShared> Reader<'_, E, A> {
1391 const fn validate_readable(&self, pos: u64) -> Result<(), Error> {
1393 if pos >= self.bounds.end {
1394 return Err(Error::ItemOutOfRange(pos));
1395 }
1396 if pos < self.bounds.start {
1397 return Err(Error::ItemPruned(pos));
1398 }
1399 Ok(())
1400 }
1401
1402 fn locate_group(&self, group: &[u64]) -> Result<(u64, Vec<u64>), Error> {
1405 let items_per_blob = self.items_per_blob.get();
1406 let blob = super::position_to_blob(group[0], items_per_blob);
1407 let first_position = first_in_blob(self.bounds.start, blob, items_per_blob)?;
1408 let offsets = group
1409 .iter()
1410 .map(|&pos| Inner::<E, A>::items_to_bytes(pos - first_position))
1411 .collect::<Result<Vec<u64>, _>>()?;
1412 Ok((blob, offsets))
1413 }
1414
1415 pub(super) async fn read_many_inner(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1419 if positions.is_empty() {
1420 return Ok(Vec::new());
1421 }
1422 assert!(
1423 positions.is_sorted_by(|a, b| a < b),
1424 "positions must be strictly increasing"
1425 );
1426 for &pos in positions {
1427 self.validate_readable(pos)?;
1428 }
1429
1430 let items_per_blob = self.items_per_blob.get();
1431
1432 let mut result: Vec<A> = Vec::with_capacity(positions.len());
1437 let mut reusable_buf = vec![0u8; positions.len() * A::SIZE];
1438
1439 let mut reads = Vec::new();
1442 let mut remaining_buf = reusable_buf.as_mut_slice();
1443 for group in positions.chunk_by(|a, b| {
1444 super::position_to_blob(*a, items_per_blob)
1445 == super::position_to_blob(*b, items_per_blob)
1446 }) {
1447 let (blob_num, blob_offsets) = self.locate_group(group)?;
1448 let blob = self
1449 .blobs
1450 .get(blob_num)
1451 .expect("positions in bounds map to a retained blob");
1452 let (buf, rest) = remaining_buf.split_at_mut(group.len() * A::SIZE);
1453 remaining_buf = rest;
1454 reads.push(async move {
1455 blob.read_many_into(buf, &blob_offsets, Inner::<E, A>::CHUNK_SIZE)
1456 .await
1457 });
1458 }
1459 let hits: u64 = try_join_all(reads)
1460 .await?
1461 .into_iter()
1462 .map(|group_hits| group_hits as u64)
1463 .sum();
1464
1465 #[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
1466 for slice in reusable_buf.chunks_exact(A::SIZE) {
1467 result.push(A::decode(slice).map_err(Error::Codec)?);
1468 }
1469
1470 self.metrics.cache_hits.inc_by(hits);
1471 self.metrics
1472 .cache_misses
1473 .inc_by(positions.len() as u64 - hits);
1474 self.metrics.items_read.inc_by(positions.len() as u64);
1475 Ok(result)
1476 }
1477
1478 fn locate(&self, pos: u64) -> Result<(Blob<'_, E::Blob>, u64), Error> {
1480 self.validate_readable(pos)?;
1481 let items_per_blob = self.items_per_blob.get();
1482 let blob = super::position_to_blob(pos, items_per_blob);
1483 let pos_in_blob = pos - first_in_blob(self.bounds.start, blob, items_per_blob)?;
1484 let offset = Inner::<E, A>::items_to_bytes(pos_in_blob)?;
1485 let blob = self
1486 .blobs
1487 .get(blob)
1488 .expect("position in bounds maps to a retained blob");
1489 Ok((blob, offset))
1490 }
1491
1492 pub(super) fn probe_items(&self, positions: &[u64]) -> Vec<Option<A>> {
1496 assert!(
1497 positions.is_sorted_by(|a, b| a < b),
1498 "positions must be strictly increasing"
1499 );
1500 let mut out: Vec<Option<A>> = (0..positions.len()).map(|_| None).collect();
1501
1502 let start = positions.partition_point(|&pos| pos < self.bounds.start);
1506 let end = positions.partition_point(|&pos| pos < self.bounds.end);
1507 let valid = &positions[start..end];
1508 if valid.is_empty() {
1509 return out;
1510 }
1511
1512 let items_per_blob = self.items_per_blob.get();
1515 let mut scratch =
1516 Cached::take(&PROBE_SCRATCH, || Ok::<_, ()>(Vec::new()), |_| Ok(())).unwrap();
1517 let need = valid.len() * A::SIZE;
1518 if scratch.len() < need {
1519 scratch.resize(need, 0);
1520 }
1521 let buf = &mut scratch[..need];
1522 let mut hits = 0u64;
1523 let mut group_base = start;
1524 for group in valid.chunk_by(|a, b| {
1525 super::position_to_blob(*a, items_per_blob)
1526 == super::position_to_blob(*b, items_per_blob)
1527 }) {
1528 let base = group_base;
1529 group_base += group.len();
1530 let Ok((blob_num, blob_offsets)) = self.locate_group(group) else {
1531 continue;
1532 };
1533 let Some(blob) = self.blobs.get(blob_num) else {
1534 continue;
1535 };
1536 let buf = &mut buf[..group.len() * A::SIZE];
1537 let misses =
1538 blob.try_read_many_sync_into(buf, &blob_offsets, Inner::<E, A>::CHUNK_SIZE);
1539 let mut misses = misses.into_iter().peekable();
1540 #[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
1541 for (idx, slice) in buf.chunks_exact(A::SIZE).enumerate() {
1542 if misses.peek() == Some(&idx) {
1543 misses.next();
1544 continue;
1545 }
1546 if let Ok(item) = A::decode(slice) {
1549 out[base + idx] = Some(item);
1550 hits += 1;
1551 }
1552 }
1553 }
1554 self.metrics.cache_hits.inc_by(hits);
1555 self.metrics.items_read.inc_by(hits);
1556 out
1557 }
1558}
1559
1560impl<E: Context, A: CodecFixedShared> super::Contiguous for Reader<'_, E, A> {
1561 type Item = A;
1562
1563 fn bounds(&self) -> Range<u64> {
1564 self.bounds.clone()
1565 }
1566
1567 async fn read(&self, pos: u64) -> Result<A, Error> {
1568 self.metrics.read_calls.inc();
1569
1570 if let Some(item) = self.try_read_sync(pos) {
1572 return Ok(item);
1573 }
1574
1575 let _timer = self.metrics.read_timer();
1576 let (blob, offset) = self.locate(pos)?;
1577 self.metrics.cache_misses.inc();
1578 let bufs = blob.read_at(offset, A::SIZE).await?;
1579 let item = A::decode(bufs.coalesce()).map_err(Error::Codec)?;
1580 self.metrics.items_read.inc();
1581 Ok(item)
1582 }
1583
1584 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1585 if positions.is_empty() {
1586 return Ok(Vec::new());
1587 }
1588 let _timer = self.metrics.read_many_timer();
1589 self.metrics.read_many_calls.inc();
1590 self.read_many_inner(positions).await
1591 }
1592
1593 fn try_read_sync(&self, pos: u64) -> Option<A> {
1594 let mut buf = vec![0u8; A::SIZE];
1595 let item = match self.locate(pos) {
1596 Ok((blob, offset)) if blob.try_read_sync_into(&mut buf, offset) => {
1597 A::decode(&buf[..]).ok()
1598 }
1599 _ => None,
1600 };
1601 if item.is_some() {
1602 self.metrics.cache_hits.inc();
1603 self.metrics.items_read.inc();
1604 }
1605 item
1606 }
1607
1608 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
1609 self.probe_items(positions)
1610 }
1611
1612 async fn replay(
1613 &self,
1614 start_pos: u64,
1615 buffer: NonZeroUsize,
1616 read_options: ReadOptions,
1617 ) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send, Error> {
1618 replay_stream(
1619 &self.blobs,
1620 self.bounds.clone(),
1621 self.items_per_blob,
1622 start_pos,
1623 buffer,
1624 read_options,
1625 )
1626 }
1627}
1628
1629impl<E: Context, A: CodecFixedShared> super::Contiguous for Inner<E, A> {
1630 type Item = A;
1631
1632 fn bounds(&self) -> Range<u64> {
1633 self.bounds.clone()
1634 }
1635
1636 async fn read(&self, pos: u64) -> Result<A, Error> {
1637 self.reader().read(pos).await
1638 }
1639
1640 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1641 self.reader().read_many(positions).await
1642 }
1643
1644 fn try_read_sync(&self, pos: u64) -> Option<A> {
1645 self.reader().try_read_sync(pos)
1646 }
1647
1648 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
1649 self.reader().probe_items(positions)
1650 }
1651
1652 async fn replay(
1653 &self,
1654 start_pos: u64,
1655 buffer: NonZeroUsize,
1656 read_options: ReadOptions,
1657 ) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send, Error> {
1658 let blobs = self.blobs.reader();
1659 replay_stream(
1660 &blobs,
1661 self.bounds.clone(),
1662 self.items_per_blob,
1663 start_pos,
1664 buffer,
1665 read_options,
1666 )
1667 }
1668}
1669
1670impl<E: Context, A: CodecFixedShared> super::Contiguous for Journal<E, A> {
1671 type Item = A;
1672
1673 fn bounds(&self) -> Range<u64> {
1674 super::Contiguous::bounds(&*self.0)
1675 }
1676
1677 async fn read(&self, pos: u64) -> Result<A, Error> {
1678 super::Contiguous::read(&*self.0, pos).await
1679 }
1680
1681 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1682 super::Contiguous::read_many(&*self.0, positions).await
1683 }
1684
1685 fn try_read_sync(&self, pos: u64) -> Option<A> {
1686 super::Contiguous::try_read_sync(&*self.0, pos)
1687 }
1688
1689 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
1690 super::Contiguous::try_read_many_sync(&*self.0, positions)
1691 }
1692
1693 async fn replay(
1694 &self,
1695 start_pos: u64,
1696 buffer: NonZeroUsize,
1697 read_options: ReadOptions,
1698 ) -> Result<impl Stream<Item = Result<(u64, A), Error>> + Send, Error> {
1699 super::Contiguous::replay(&*self.0, start_pos, buffer, read_options).await
1700 }
1701}
1702
1703impl<E: Context, A: CodecFixedShared> Mutable for Journal<E, A> {
1704 async fn append(self, item: &Self::Item) -> Result<(Self, u64), Error> {
1705 Self::append(self, item).await
1706 }
1707
1708 async fn append_many(self, items: Many<'_, Self::Item>) -> Result<(Self, u64), Error> {
1709 Self::append_many(self, items).await
1710 }
1711
1712 async fn prune(self, min_position: u64) -> Result<(Self, bool), Error> {
1713 Self::prune(self, min_position).await
1714 }
1715
1716 async fn rewind(self, size: u64) -> Result<Self, Error> {
1717 Self::rewind(self, size).await
1718 }
1719
1720 async fn start_sync(self) -> Result<(Self, Handle<()>), Error> {
1721 Self::start_sync(self).await
1722 }
1723
1724 async fn commit(self) -> Result<Self, Error> {
1725 Self::commit(self).await
1726 }
1727
1728 async fn sync(self) -> Result<Self, Error> {
1729 Self::sync(self).await
1730 }
1731
1732 async fn destroy(self) -> Result<(), Error> {
1733 Self::destroy(self).await
1734 }
1735}
1736
1737#[commonware_macros::stability(ALPHA)]
1738impl<E: Context, A: CodecFixedShared> authenticated::Backing<E> for Journal<E, A> {
1739 type Config = Config;
1740
1741 async fn init(context: E, cfg: Self::Config) -> Result<Self, Error> {
1742 Self::init(context, cfg).await
1743 }
1744}
1745
1746#[cfg(test)]
1747mod tests {
1748 use super::*;
1749 use crate::journal::contiguous::Contiguous as _;
1750 use commonware_codec::FixedSize;
1751 use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
1752 use commonware_macros::test_traced;
1753 use commonware_runtime::{
1754 Blob, BufferPooler, Error as RuntimeError, Metrics as _, Runner, Spawner as _, Storage,
1755 Supervisor as _, WriteOptions,
1756 buffer::paged::{Writer, corrupt_page},
1757 deterministic::{self, Context},
1758 mocks::{
1759 DelayedSyncContext, PendingSyncs, RecordingContext, WriteFaultContext, WriteFaults,
1760 drive_pending_syncs, fail_pending_syncs, release_pending_syncs,
1761 },
1762 };
1763 use commonware_utils::{NZU16, NZU64, NZUsize, probability};
1764 use futures::{StreamExt, pin_mut};
1765 use std::num::NonZeroU16;
1766
1767 const PAGE_SIZE: NonZeroU16 = NZU16!(44);
1768 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
1769
1770 fn test_digest(value: u64) -> Digest {
1772 Sha256::hash(&[&value.to_be_bytes()])
1773 }
1774
1775 fn test_cfg(pooler: &impl BufferPooler, items_per_blob: NonZeroU64) -> Config {
1776 Config {
1777 partition: "test-partition".into(),
1778 items_per_blob,
1779 page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
1780 write_buffer: NZUsize!(2048),
1781 replay_buffer: NZUsize!(2048),
1782 }
1783 }
1784
1785 fn blob_partition(cfg: &Config) -> String {
1786 format!("{}-blobs", cfg.partition)
1787 }
1788
1789 #[test]
1790 fn test_start_sync_keeps_predecessor_sync() {
1791 let executor = deterministic::Runner::default();
1792 executor.start(|context| async move {
1793 let cfg = test_cfg(&context, NZU64!(3));
1794 let mut journal = Box::new(Inner::<_, u64>::init(context, cfg).await.unwrap());
1795
1796 journal
1798 .append_many(Many::Flat(&[1, 2, 3, 4]))
1799 .await
1800 .unwrap();
1801 assert!(journal.blobs.has_tail_predecessor_sync());
1802
1803 let (journal, handle) = journal.start_sync().await.unwrap();
1805 assert!(journal.blobs.has_tail_predecessor_sync());
1806 handle.await.unwrap();
1807 assert!(journal.blobs.has_tail_predecessor_sync());
1808
1809 journal.destroy().await.unwrap();
1810 });
1811 }
1812
1813 #[test_traced]
1814 fn test_start_sync_advances_watermark_lagged() {
1815 let executor = deterministic::Runner::default();
1816 executor.start(|context| async move {
1817 let pending = PendingSyncs::default();
1818 let cfg = test_cfg(&context, NZU64!(100));
1819 let make = |pending: PendingSyncs| {
1820 Inner::<_, u64>::init(
1821 DelayedSyncContext {
1822 inner: context.child("journal"),
1823 pending,
1824 },
1825 cfg.clone(),
1826 )
1827 };
1828 let mut journal = Box::new(make(pending.clone()).await.unwrap());
1829
1830 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
1832 let (mut journal, h1) = journal.start_sync().await.unwrap();
1833 assert_eq!(journal.recovery_watermark(), 0);
1834
1835 release_pending_syncs(&pending);
1836 h1.await.unwrap();
1837
1838 journal.append(&4).await.unwrap();
1841 let (journal, h2) = journal.start_sync().await.unwrap();
1842 assert_eq!(journal.recovery_watermark(), 3);
1843 drive_pending_syncs(&pending, h2).await.unwrap();
1844
1845 let (journal, h3) = drive_pending_syncs(&pending, journal.start_sync())
1847 .await
1848 .unwrap();
1849 assert_eq!(journal.recovery_watermark(), 4);
1850 drive_pending_syncs(&pending, h3).await.unwrap();
1851
1852 pending.unblock();
1854 drop(journal);
1855 let journal = make(pending.clone()).await.unwrap();
1856 assert_eq!(journal.recovery_watermark(), 4);
1857 assert_eq!(journal.bounds(), 0..4);
1858 journal.destroy().await.unwrap();
1859 });
1860 }
1861
1862 #[test_traced]
1863 fn test_start_sync_failure_blocks_watermark() {
1864 let executor = deterministic::Runner::default();
1865 executor.start(|context| async move {
1866 let pending = PendingSyncs::default();
1867 let cfg = test_cfg(&context, NZU64!(100));
1868 let mut journal = Box::new(
1869 Inner::<_, u64>::init(
1870 DelayedSyncContext {
1871 inner: context.child("journal"),
1872 pending: pending.clone(),
1873 },
1874 cfg,
1875 )
1876 .await
1877 .unwrap(),
1878 );
1879
1880 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
1881 let (journal, h1) = journal.start_sync().await.unwrap();
1882 fail_pending_syncs(&pending);
1883 assert!(h1.await.is_err());
1884
1885 let (journal, h2) = journal.start_sync().await.unwrap();
1888 assert_eq!(journal.recovery_watermark(), 0);
1889 assert!(h2.await.is_err());
1890 });
1891 }
1892
1893 #[test_traced]
1894 fn test_rewind_drains_parked_watermark_advance() {
1895 let executor = deterministic::Runner::default();
1896 executor.start(|context| async move {
1897 let pending = PendingSyncs::default();
1898 let cfg = test_cfg(&context, NZU64!(100));
1899 let make = |pending: PendingSyncs| {
1900 Inner::<_, u64>::init(
1901 DelayedSyncContext {
1902 inner: context.child("journal"),
1903 pending,
1904 },
1905 cfg.clone(),
1906 )
1907 };
1908 let mut journal = Box::new(make(pending.clone()).await.unwrap());
1909
1910 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
1911 let (mut journal, h1) = journal.start_sync().await.unwrap();
1912 release_pending_syncs(&pending);
1913 h1.await.unwrap();
1914
1915 journal.append(&4).await.unwrap();
1917 let (journal, h2) = journal.start_sync().await.unwrap();
1918 assert_eq!(journal.recovery_watermark(), 3);
1919
1920 let journal = drive_pending_syncs(&pending, journal.rewind(2))
1922 .await
1923 .unwrap();
1924 assert_eq!(journal.recovery_watermark(), 2);
1925 drop(h2);
1926
1927 pending.unblock();
1929 drop(journal);
1930 let journal = make(pending.clone()).await.unwrap();
1931 assert_eq!(journal.recovery_watermark(), 2);
1932 assert_eq!(journal.bounds(), 0..2);
1933 journal.destroy().await.unwrap();
1934 });
1935 }
1936
1937 #[test_traced]
1938 fn test_start_sync_watermark_advance_inline_failure() {
1939 let executor = deterministic::Runner::default();
1940 executor.start(|context| async move {
1941 let faults = WriteFaults::default();
1942 let cfg = test_cfg(&context, NZU64!(100));
1943 let make = |faults: WriteFaults| {
1944 Inner::<_, u64>::init(
1945 WriteFaultContext {
1946 inner: context.child("journal"),
1947 faults,
1948 },
1949 cfg.clone(),
1950 )
1951 };
1952 let mut journal = Box::new(make(faults.clone()).await.unwrap());
1953
1954 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
1957 let (mut journal, h1) = journal.start_sync().await.unwrap();
1958 h1.await.unwrap();
1959 journal.append(&4).await.unwrap();
1960 let journal = journal.commit().await.unwrap();
1961
1962 faults.arm();
1965 assert!(journal.start_sync().await.is_err());
1966 faults.disarm();
1967
1968 let journal = Box::new(make(faults).await.unwrap());
1971 assert_eq!(journal.bounds(), 0..4);
1972 let journal = journal.sync().await.unwrap();
1973 assert_eq!(journal.recovery_watermark(), 4);
1974 journal.destroy().await.unwrap();
1975 });
1976 }
1977
1978 #[test_traced]
1979 fn test_start_sync_watermark_advance_deferred_failure() {
1980 let executor = deterministic::Runner::default();
1981 executor.start(|context| async move {
1982 let pending = PendingSyncs::default();
1983 let cfg = test_cfg(&context, NZU64!(100));
1984 let mut journal = Box::new(
1985 Inner::<_, u64>::init(
1986 DelayedSyncContext {
1987 inner: context.child("journal"),
1988 pending: pending.clone(),
1989 },
1990 cfg,
1991 )
1992 .await
1993 .unwrap(),
1994 );
1995
1996 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
1997 let (journal, h1) = journal.start_sync().await.unwrap();
1998 release_pending_syncs(&pending);
1999 h1.await.unwrap();
2000
2001 let (mut journal, h2) = journal.start_sync().await.unwrap();
2004 fail_pending_syncs(&pending);
2005 assert!(h2.await.is_err());
2006
2007 journal.append(&4).await.unwrap();
2010 let (journal, h3) = journal.start_sync().await.unwrap();
2011 drive_pending_syncs(&pending, h3).await.unwrap();
2012
2013 let journal = drive_pending_syncs(&pending, journal.commit())
2017 .await
2018 .unwrap();
2019 assert!(drive_pending_syncs(&pending, journal.sync()).await.is_err());
2020 });
2021 }
2022
2023 #[test_traced]
2024 fn test_rewind_watermark_lowering_failure_keeps_blobs() {
2025 let executor = deterministic::Runner::default();
2026 executor.start(|context| async move {
2027 let faults = WriteFaults::default();
2028 let cfg = test_cfg(&context, NZU64!(100));
2029 let make = |faults: WriteFaults| {
2030 Inner::<_, u64>::init(
2031 WriteFaultContext {
2032 inner: context.child("journal"),
2033 faults,
2034 },
2035 cfg.clone(),
2036 )
2037 };
2038 let mut journal = Box::new(make(faults.clone()).await.unwrap());
2039 journal
2040 .append_many(Many::Flat(&[1, 2, 3, 4]))
2041 .await
2042 .unwrap();
2043 let journal = journal.sync().await.unwrap();
2044 assert_eq!(journal.recovery_watermark(), 4);
2045
2046 faults.arm();
2049 assert!(journal.rewind(2).await.is_err());
2050 faults.disarm();
2051
2052 let journal = make(faults).await.unwrap();
2053 assert_eq!(journal.recovery_watermark(), 4);
2054 assert_eq!(journal.bounds(), 0..4);
2055 journal.destroy().await.unwrap();
2056 });
2057 }
2058
2059 #[test_traced]
2060 fn test_rewind_truncates_durable_size() {
2061 let executor = deterministic::Runner::default();
2062 executor.start(|context| async move {
2063 let pending = PendingSyncs::default();
2064 let cfg = test_cfg(&context, NZU64!(100));
2065 let mut journal = Box::new(
2066 Inner::<_, u64>::init(
2067 DelayedSyncContext {
2068 inner: context.child("journal"),
2069 pending: pending.clone(),
2070 },
2071 cfg,
2072 )
2073 .await
2074 .unwrap(),
2075 );
2076 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2077 let journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
2078 assert_eq!(journal.recovery_watermark(), 3);
2079
2080 let mut journal = drive_pending_syncs(&pending, journal.rewind(2))
2084 .await
2085 .unwrap();
2086 journal.append(&9).await.unwrap();
2087 let (journal, handle) = journal.start_sync().await.unwrap();
2088 assert_eq!(journal.recovery_watermark(), 2);
2089
2090 pending.unblock();
2091 handle.await.unwrap();
2092 journal.destroy().await.unwrap();
2093 });
2094 }
2095
2096 #[test_traced]
2100 fn test_fixed_dropped_failed_start_sync_surfaces_after_rollover() {
2101 let executor = deterministic::Runner::default();
2102 executor.start(|context| async move {
2103 let cfg = test_cfg(&context, NZU64!(3));
2104 let mut journal = Box::new(
2105 Inner::<_, u64>::init(context.child("journal"), cfg)
2106 .await
2107 .unwrap(),
2108 );
2109
2110 journal.append(&0).await.unwrap();
2113 *context.storage_fault_config().write() = deterministic::FaultConfig {
2114 write_rate: Some(deterministic::WriteConfig {
2115 failure_rate: probability!(1.0),
2116 retention_rate: probability!(0.0),
2117 mode: deterministic::PartialWriteMode::Prefix,
2118 }),
2119 ..Default::default()
2120 };
2121 let (mut journal, handle) = journal.start_sync().await.unwrap();
2122 drop(handle);
2123 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
2124
2125 assert!(matches!(
2127 journal.append_many(Many::Flat(&[1, 2, 3])).await,
2128 Err(Error::Runtime(_))
2129 ));
2130 });
2131 }
2132
2133 fn counter(buffer: &str, name: &str) -> u64 {
2135 buffer
2136 .lines()
2137 .find(|l| l.contains(name) && !l.starts_with('#'))
2138 .and_then(|l| l.split_whitespace().last())
2139 .and_then(|v| v.parse().ok())
2140 .expect("counter missing")
2141 }
2142
2143 impl<E: crate::Context, A: CodecFixedShared> Inner<E, A> {
2144 pub(crate) const fn test_oldest_blob(&self) -> Option<u64> {
2146 Some(self.blobs.oldest_blob_index())
2147 }
2148
2149 pub(crate) fn test_newest_blob(&self) -> Option<u64> {
2151 Some(self.blobs.tail_blob_index())
2152 }
2153
2154 pub(crate) async fn test_sync_blob(&mut self, blob: u64) -> Result<(), Error> {
2156 self.blobs.sync_blob(blob).await
2157 }
2158
2159 pub(crate) async fn test_set_recovery_watermark(
2161 mut self: Box<Self>,
2162 watermark: u64,
2163 ) -> Result<Box<Self>, Error> {
2164 self.checkpoint.set_watermark(Some(watermark));
2165 self.checkpoint = self.checkpoint.sync().await?;
2166 Ok(self)
2167 }
2168
2169 pub(crate) async fn test_stage_clear(
2171 context: E,
2172 partition: &str,
2173 target: u64,
2174 ) -> Result<(), Error> {
2175 let checkpoint = Checkpoint::open(context, partition).await?;
2176 checkpoint.stage_clear(target).await?;
2177 Ok(())
2178 }
2179 }
2180
2181 impl<E: crate::Context, A: CodecFixedShared> Journal<E, A> {
2182 pub(crate) fn test_oldest_blob(&self) -> Option<u64> {
2184 self.0.test_oldest_blob()
2185 }
2186
2187 pub(crate) fn test_newest_blob(&self) -> Option<u64> {
2189 self.0.test_newest_blob()
2190 }
2191
2192 pub(crate) async fn test_sync_blob(&mut self, blob: u64) -> Result<(), Error> {
2194 self.0.test_sync_blob(blob).await
2195 }
2196
2197 pub(crate) async fn test_set_recovery_watermark(
2199 mut self,
2200 watermark: u64,
2201 ) -> Result<Self, Error> {
2202 self.0 = self.0.test_set_recovery_watermark(watermark).await?;
2203 Ok(self)
2204 }
2205
2206 pub(crate) async fn test_stage_clear(
2208 context: E,
2209 partition: &str,
2210 target: u64,
2211 ) -> Result<(), Error> {
2212 Inner::<E, A>::test_stage_clear(context, partition, target).await
2213 }
2214 }
2215
2216 #[test_traced]
2217 fn test_fixed_commit_syncs_recovered_tail_past_recovery_watermark() {
2218 let executor = deterministic::Runner::default();
2219 executor.start(|context| async move {
2220 let mut cfg = test_cfg(&context, NZU64!(10));
2221 cfg.partition = "init-adopted-fixed".into();
2222
2223 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2224 .await
2225 .unwrap();
2226 (journal, _) = journal.append(&1).await.unwrap();
2227 (journal, _) = journal.append(&2).await.unwrap();
2228 let journal = journal.sync().await.unwrap();
2229 let journal = journal.test_set_recovery_watermark(1).await.unwrap();
2232 drop(journal);
2233
2234 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
2235 .await
2236 .unwrap();
2237 assert_eq!(journal.size(), 2);
2238
2239 *context.storage_fault_config().write() = deterministic::FaultConfig {
2242 sync_rate: Some(probability!(1.0)),
2243 ..Default::default()
2244 };
2245 assert!(
2246 journal.commit().await.is_err(),
2247 "commit() must sync recovered data beyond the persisted recovery watermark"
2248 );
2249 });
2250 }
2251
2252 async fn scan_partition(context: &Context, partition: &str) -> Vec<Vec<u8>> {
2253 match context.scan(partition).await {
2254 Ok(blobs) => blobs,
2255 Err(RuntimeError::PartitionMissing(_)) => Vec::new(),
2256 Err(err) => panic!("Failed to scan partition {partition}: {err}"),
2257 }
2258 }
2259
2260 #[test_traced]
2261 fn test_fixed_journal_init_conflicting_partitions() {
2262 let executor = deterministic::Runner::default();
2263 executor.start(|context| async move {
2264 let cfg = test_cfg(&context, NZU64!(2));
2265 let legacy_partition = cfg.partition.clone();
2266 let blobs_partition = blob_partition(&cfg);
2267
2268 let (legacy_blob, _) = context
2269 .open(&legacy_partition, &0u64.to_be_bytes())
2270 .await
2271 .expect("Failed to open legacy blob");
2272 legacy_blob
2273 .write_at(0, vec![0u8; 1], WriteOptions::SYNC)
2274 .await
2275 .expect("Failed to write legacy blob");
2276
2277 let (new_blob, _) = context
2278 .open(&blobs_partition, &0u64.to_be_bytes())
2279 .await
2280 .expect("Failed to open new blob");
2281 new_blob
2282 .write_at(0, vec![0u8; 1], WriteOptions::SYNC)
2283 .await
2284 .expect("Failed to write new blob");
2285
2286 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2287 assert!(matches!(result, Err(Error::Corruption(_))));
2288 });
2289 }
2290
2291 #[test_traced]
2292 fn test_fixed_journal_init_prefers_legacy_partition() {
2293 let executor = deterministic::Runner::default();
2294 executor.start(|context| async move {
2295 let cfg = test_cfg(&context, NZU64!(2));
2296 let legacy_partition = cfg.partition.clone();
2297 let blobs_partition = blob_partition(&cfg);
2298
2299 let (legacy_blob, _) = context
2301 .open(&legacy_partition, &0u64.to_be_bytes())
2302 .await
2303 .expect("Failed to open legacy blob");
2304 legacy_blob
2305 .write_at(0, vec![0u8; 1], WriteOptions::SYNC)
2306 .await
2307 .expect("Failed to write legacy blob");
2308
2309 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2310 .await
2311 .expect("failed to initialize journal");
2312 (journal, _) = journal.append(&test_digest(1)).await.unwrap();
2313 let journal = journal.sync().await.unwrap();
2314 drop(journal);
2315
2316 let legacy_blobs = scan_partition(&context, &legacy_partition).await;
2317 let new_blobs = scan_partition(&context, &blobs_partition).await;
2318 assert!(!legacy_blobs.is_empty());
2319 assert!(new_blobs.is_empty());
2320 });
2321 }
2322
2323 #[test_traced]
2324 fn test_fixed_journal_init_defaults_to_blobs_partition() {
2325 let executor = deterministic::Runner::default();
2326 executor.start(|context| async move {
2327 let cfg = test_cfg(&context, NZU64!(2));
2328 let legacy_partition = cfg.partition.clone();
2329 let blobs_partition = blob_partition(&cfg);
2330
2331 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2332 .await
2333 .expect("failed to initialize journal");
2334 (journal, _) = journal.append(&test_digest(1)).await.unwrap();
2335 let journal = journal.sync().await.unwrap();
2336 drop(journal);
2337
2338 let legacy_blobs = scan_partition(&context, &legacy_partition).await;
2339 let new_blobs = scan_partition(&context, &blobs_partition).await;
2340 assert!(legacy_blobs.is_empty());
2341 assert!(!new_blobs.is_empty());
2342 });
2343 }
2344
2345 #[test_traced]
2346 fn test_fixed_journal_append_and_prune() {
2347 let executor = deterministic::Runner::default();
2349
2350 executor.start(|context| async move {
2352 let cfg = test_cfg(&context, NZU64!(2));
2354 let mut journal = Journal::init(context.child("first"), cfg.clone())
2355 .await
2356 .expect("failed to initialize journal");
2357
2358 let mut pos;
2360 (journal, pos) = journal
2361 .append(&test_digest(0))
2362 .await
2363 .expect("failed to append data 0");
2364 assert_eq!(pos, 0);
2365
2366 let journal = journal.sync().await.expect("Failed to sync journal");
2368 drop(journal);
2369
2370 let cfg = test_cfg(&context, NZU64!(2));
2371 let mut journal = Journal::init(context.child("second"), cfg.clone())
2372 .await
2373 .expect("failed to re-initialize journal");
2374 assert_eq!(journal.size(), 1);
2375
2376 (journal, pos) = journal
2378 .append(&test_digest(1))
2379 .await
2380 .expect("failed to append data 1");
2381 assert_eq!(pos, 1);
2382 (journal, pos) = journal
2383 .append(&test_digest(2))
2384 .await
2385 .expect("failed to append data 2");
2386 assert_eq!(pos, 2);
2387
2388 let item0 = journal.read(0).await.expect("failed to read data 0");
2390 assert_eq!(item0, test_digest(0));
2391 let item1 = journal.read(1).await.expect("failed to read data 1");
2392 assert_eq!(item1, test_digest(1));
2393 let item2 = journal.read(2).await.expect("failed to read data 2");
2394 assert_eq!(item2, test_digest(2));
2395 let err = journal.read(3).await.expect_err("expected read to fail");
2396 assert!(matches!(err, Error::ItemOutOfRange(3)));
2397
2398 journal = journal.sync().await.expect("failed to sync journal");
2400
2401 (journal, _) = journal.prune(1).await.expect("failed to prune journal 1");
2403
2404 (journal, _) = journal.prune(2).await.expect("failed to prune journal 2");
2406 assert_eq!(journal.bounds().start, 2);
2407
2408 let result0 = journal.read(0).await;
2410 assert!(matches!(result0, Err(Error::ItemPruned(0))));
2411 let result1 = journal.read(1).await;
2412 assert!(matches!(result1, Err(Error::ItemPruned(1))));
2413
2414 let result2 = journal.read(2).await.unwrap();
2416 assert_eq!(result2, test_digest(2));
2417
2418 for i in 3..10 {
2420 let pos;
2421 (journal, pos) = journal
2422 .append(&test_digest(i))
2423 .await
2424 .expect("failed to append data");
2425 assert_eq!(pos, i);
2426 }
2427
2428 (journal, _) = journal.prune(0).await.expect("no-op pruning failed");
2430 assert_eq!(journal.test_oldest_blob(), Some(1));
2431 assert_eq!(journal.test_newest_blob(), Some(5));
2432 assert_eq!(journal.bounds().start, 2);
2433
2434 (journal, _) = journal
2436 .prune(3 * cfg.items_per_blob.get())
2437 .await
2438 .expect("failed to prune journal 2");
2439 assert_eq!(journal.test_oldest_blob(), Some(3));
2440 assert_eq!(journal.test_newest_blob(), Some(5));
2441 assert_eq!(journal.bounds().start, 6);
2442
2443 (journal, _) = journal
2445 .prune(10000)
2446 .await
2447 .expect("failed to max-prune journal");
2448 let size = journal.size();
2449 assert_eq!(size, 10);
2450 assert_eq!(journal.test_oldest_blob(), Some(5));
2451 assert_eq!(journal.test_newest_blob(), Some(5));
2452 let bounds = journal.bounds();
2455 assert!(bounds.is_empty());
2456 assert_eq!(bounds.start, size);
2458
2459 {
2461 let reader;
2462 (journal, reader) = journal.snapshot().await.unwrap();
2463 let result = reader
2464 .replay(0, NZUsize!(1024), ReadOptions::default())
2465 .await;
2466 assert!(matches!(result, Err(Error::ItemPruned(0))));
2467 }
2468
2469 {
2471 let reader;
2472 (journal, reader) = journal.snapshot().await.unwrap();
2473 let res = reader
2474 .replay(0, NZUsize!(1024), ReadOptions::default())
2475 .await;
2476 assert!(matches!(res, Err(Error::ItemPruned(_))));
2477
2478 let reader;
2479 (journal, reader) = journal.snapshot().await.unwrap();
2480 let stream = reader
2481 .replay(
2482 journal.bounds().start,
2483 NZUsize!(1024),
2484 ReadOptions::default(),
2485 )
2486 .await
2487 .expect("failed to replay journal from pruning boundary");
2488 pin_mut!(stream);
2489 let mut items = Vec::new();
2490 while let Some(result) = stream.next().await {
2491 match result {
2492 Ok((pos, item)) => {
2493 assert_eq!(test_digest(pos), item);
2494 items.push(pos);
2495 }
2496 Err(err) => panic!("Failed to read item: {err}"),
2497 }
2498 }
2499 assert_eq!(items, Vec::<u64>::new());
2500 }
2501
2502 journal.destroy().await.unwrap();
2503 });
2504 }
2505
2506 #[test_traced]
2508 fn test_fixed_journal_append_a_lot_of_data() {
2509 let executor = deterministic::Runner::default();
2511 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(10000);
2512 executor.start(|context| async move {
2513 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
2514 let mut journal = Journal::init(context.child("first"), cfg.clone())
2515 .await
2516 .expect("failed to initialize journal");
2517 for i in 0u64..ITEMS_PER_BLOB.get() * 2 - 1 {
2519 (journal, _) = journal
2520 .append(&test_digest(i))
2521 .await
2522 .expect("failed to append data");
2523 }
2524 journal.sync().await.expect("failed to sync journal");
2526 let journal = Journal::init(context.child("second"), cfg.clone())
2527 .await
2528 .expect("failed to re-initialize journal");
2529 for i in 0u64..10000 {
2530 let item: Digest = journal.read(i).await.expect("failed to read data");
2531 assert_eq!(item, test_digest(i));
2532 }
2533 journal.destroy().await.expect("failed to destroy journal");
2534 });
2535 }
2536
2537 #[test_traced]
2538 fn test_fixed_journal_replay() {
2539 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
2540 let executor = deterministic::Runner::default();
2542
2543 executor.start(|context| async move {
2545 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
2547 let mut journal = Journal::init(context.child("first"), cfg.clone())
2548 .await
2549 .expect("failed to initialize journal");
2550
2551 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
2553 let pos;
2554 (journal, pos) = journal
2555 .append(&test_digest(i))
2556 .await
2557 .expect("failed to append data");
2558 assert_eq!(pos, i);
2559 }
2560
2561 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
2563 let item: Digest = journal.read(i).await.expect("failed to read data");
2564 assert_eq!(item, test_digest(i), "i={i}");
2565 }
2566
2567 {
2569 let reader;
2570 (journal, reader) = journal.snapshot().await.unwrap();
2571 let stream = reader
2572 .replay(0, NZUsize!(1024), ReadOptions::default())
2573 .await
2574 .expect("failed to replay journal");
2575 let mut items = Vec::new();
2576 pin_mut!(stream);
2577 while let Some(result) = stream.next().await {
2578 match result {
2579 Ok((pos, item)) => {
2580 assert_eq!(test_digest(pos), item, "pos={pos}, item={item:?}");
2581 items.push(pos);
2582 }
2583 Err(err) => panic!("Failed to read item: {err}"),
2584 }
2585 }
2586
2587 assert_eq!(
2589 items.len(),
2590 ITEMS_PER_BLOB.get() as usize * 100 + ITEMS_PER_BLOB.get() as usize / 2
2591 );
2592 items.sort();
2593 for (i, pos) in items.iter().enumerate() {
2594 assert_eq!(i as u64, *pos);
2595 }
2596 }
2597
2598 let journal = journal.sync().await.expect("Failed to sync journal");
2599 drop(journal);
2600
2601 let (blob, _) = context
2603 .open(&blob_partition(&cfg), &40u64.to_be_bytes())
2604 .await
2605 .expect("Failed to open blob");
2606 let bad_bytes = 123456789u32;
2608 blob.write_at(1, bad_bytes.to_be_bytes().to_vec(), WriteOptions::SYNC)
2609 .await
2610 .expect("Failed to write bad bytes");
2611
2612 let journal = Journal::init(context.child("second"), cfg.clone())
2614 .await
2615 .expect("Failed to re-initialize journal");
2616
2617 let err = journal
2619 .read(40 * ITEMS_PER_BLOB.get() + 1)
2620 .await
2621 .unwrap_err();
2622 assert!(matches!(err, Error::Runtime(_)));
2623
2624 {
2626 let mut error_found = false;
2627 let (_journal, reader) = journal.snapshot().await.unwrap();
2628 let stream = reader
2629 .replay(0, NZUsize!(1024), ReadOptions::default())
2630 .await
2631 .expect("failed to replay journal");
2632 let mut items = Vec::new();
2633 pin_mut!(stream);
2634 while let Some(result) = stream.next().await {
2635 match result {
2636 Ok((pos, item)) => {
2637 assert_eq!(test_digest(pos), item);
2638 items.push(pos);
2639 }
2640 Err(err) => {
2641 error_found = true;
2642 assert!(matches!(err, Error::Runtime(_)));
2643 assert!(stream.next().await.is_none());
2644 break;
2645 }
2646 }
2647 }
2648 assert!(error_found); }
2650 });
2651 }
2652
2653 #[test_traced]
2654 fn test_replay_and_writable_tip_request_dont_cache() {
2655 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(3);
2656
2657 let executor = deterministic::Runner::default();
2658 executor.start(|context| async move {
2659 let (context, recordings) = RecordingContext::new(context);
2660 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
2661 let page_cache = cfg.page_cache.clone();
2662 let mut journal = Journal::init(context.child("journal"), cfg)
2663 .await
2664 .expect("failed to initialize journal");
2665
2666 for i in 0..5 {
2667 (journal, _) = journal
2668 .append(&test_digest(i))
2669 .await
2670 .expect("failed to append");
2671 }
2672 journal = journal.sync().await.expect("failed to sync journal");
2673
2674 page_cache.clear();
2676 recordings.clear();
2677 {
2678 let stream = journal
2679 .replay(0, NZUsize!(56), ReadOptions::DONT_CACHE)
2680 .await
2681 .expect("failed to replay sealed history");
2682 pin_mut!(stream);
2683 let (position, item) = stream
2684 .next()
2685 .await
2686 .expect("missing sealed replay item")
2687 .expect("failed to replay sealed item");
2688 assert_eq!(position, 0);
2689 assert_eq!(item, test_digest(0));
2690
2691 let reads = recordings.snapshot().reads;
2692 assert!(!reads.is_empty());
2693 assert!(
2694 reads
2695 .iter()
2696 .all(|options| *options == ReadOptions::DONT_CACHE)
2697 );
2698 }
2699
2700 page_cache.clear();
2702 recordings.clear();
2703 {
2704 let stream = journal
2705 .replay(3, NZUsize!(56), ReadOptions::DONT_CACHE)
2706 .await
2707 .expect("failed to replay writable tip");
2708 pin_mut!(stream);
2709 let (position, item) = stream
2710 .next()
2711 .await
2712 .expect("missing writable replay item")
2713 .expect("failed to replay writable item");
2714 assert_eq!(position, 3);
2715 assert_eq!(item, test_digest(3));
2716
2717 let reads = recordings.snapshot().reads;
2718 assert!(!reads.is_empty());
2719 assert!(
2720 reads
2721 .iter()
2722 .all(|options| *options == ReadOptions::DONT_CACHE)
2723 );
2724 }
2725
2726 journal.destroy().await.expect("failed to destroy journal");
2727 });
2728 }
2729
2730 #[test_traced]
2731 fn test_fixed_replay_stops_after_error() {
2732 let executor = deterministic::Runner::default();
2733 executor.start(|context| async move {
2734 let cfg = test_cfg(&context, NZU64!(10));
2735 let mut journal = Journal::init(context.child("first"), cfg.clone())
2736 .await
2737 .unwrap();
2738
2739 for i in 0u64..30 {
2740 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2741 }
2742 let journal = journal.sync().await.unwrap();
2743 drop(journal);
2744
2745 let (blob, _) = context
2746 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2747 .await
2748 .unwrap();
2749 blob.write_at(1, 123456789u32.to_be_bytes().to_vec(), WriteOptions::SYNC)
2750 .await
2751 .unwrap();
2752
2753 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2754 .await
2755 .unwrap();
2756 let reader;
2757 (journal, reader) = journal.snapshot().await.unwrap();
2758 let stream = reader
2759 .replay(0, NZUsize!(1024), ReadOptions::default())
2760 .await
2761 .unwrap();
2762 pin_mut!(stream);
2763
2764 for i in 0u64..10 {
2765 let (pos, item) = stream.next().await.unwrap().unwrap();
2766 assert_eq!(pos, i);
2767 assert_eq!(item, test_digest(i));
2768 }
2769 assert!(matches!(
2770 stream.next().await.unwrap(),
2771 Err(Error::Runtime(_))
2772 ));
2773 assert!(stream.next().await.is_none());
2774
2775 journal.destroy().await.unwrap();
2776 });
2777 }
2778
2779 #[test_traced]
2780 fn test_fixed_journal_replay_with_missing_historical_blob() {
2781 let executor = deterministic::Runner::default();
2782 executor.start(|context| async move {
2783 let cfg = test_cfg(&context, NZU64!(2));
2784 let mut journal = Journal::init(context.child("first"), cfg.clone())
2785 .await
2786 .unwrap();
2787 for i in 0u64..5 {
2788 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2789 }
2790 let journal = journal.sync().await.unwrap();
2791 drop(journal);
2792
2793 context
2796 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
2797 .await
2798 .unwrap();
2799
2800 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2801 assert!(matches!(result, Err(Error::Corruption(_))));
2802 });
2803 }
2804
2805 #[test_traced]
2806 fn test_fixed_journal_partial_replay() {
2807 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
2808 const START_POS: u64 = 53;
2811
2812 let executor = deterministic::Runner::default();
2814 executor.start(|context| async move {
2816 let cfg = test_cfg(&context, ITEMS_PER_BLOB);
2818 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2819 .await
2820 .expect("failed to initialize journal");
2821
2822 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
2824 let pos;
2825 (journal, pos) = journal
2826 .append(&test_digest(i))
2827 .await
2828 .expect("failed to append data");
2829 assert_eq!(pos, i);
2830 }
2831
2832 {
2834 let reader;
2835 (journal, reader) = journal.snapshot().await.unwrap();
2836 let stream = reader
2837 .replay(START_POS, NZUsize!(1024), ReadOptions::default())
2838 .await
2839 .expect("failed to replay journal");
2840 let mut items = Vec::new();
2841 pin_mut!(stream);
2842 while let Some(result) = stream.next().await {
2843 match result {
2844 Ok((pos, item)) => {
2845 assert!(pos >= START_POS, "pos={pos}, expected >= {START_POS}");
2846 assert_eq!(
2847 test_digest(pos),
2848 item,
2849 "Item at position {pos} did not match expected digest"
2850 );
2851 items.push(pos);
2852 }
2853 Err(err) => panic!("Failed to read item: {err}"),
2854 }
2855 }
2856
2857 assert_eq!(
2859 items.len(),
2860 ITEMS_PER_BLOB.get() as usize * 100 + ITEMS_PER_BLOB.get() as usize / 2
2861 - START_POS as usize
2862 );
2863 items.sort();
2864 for (i, pos) in items.iter().enumerate() {
2865 assert_eq!(i as u64, *pos - START_POS);
2866 }
2867 }
2868
2869 journal.destroy().await.unwrap();
2870 });
2871 }
2872
2873 #[test_traced]
2874 fn test_fixed_journal_rejects_corrupted_tail_blob() {
2875 let executor = deterministic::Runner::default();
2876 executor.start(|context| async move {
2877 let cfg = test_cfg(&context, NZU64!(3));
2878 let mut journal = Journal::init(context.child("first"), cfg.clone())
2879 .await
2880 .unwrap();
2881 for i in 0..5 {
2882 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2883 }
2884 let journal = journal.sync().await.unwrap();
2885 drop(journal);
2886
2887 let (blob, size) = context
2890 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2891 .await
2892 .unwrap();
2893 blob.resize(size - 1).await.unwrap();
2894 blob.sync().await.unwrap();
2895
2896 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
2897 assert!(matches!(result, Err(Error::Corruption(_))));
2898 });
2899 }
2900
2901 #[test_traced]
2906 fn test_fixed_journal_crash_during_recovery_repair() {
2907 let executor = deterministic::Runner::default();
2908 executor.start(|context| async move {
2909 let cfg = test_cfg(&context, NZU64!(5));
2910 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2911 .await
2912 .unwrap();
2913
2914 for i in 0..15u64 {
2916 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2917 }
2918 let mut journal = journal.sync().await.unwrap();
2919 assert_eq!(journal.0.recovery_watermark(), 15);
2920
2921 journal.0.checkpoint = journal
2925 .0
2926 .checkpoint
2927 .persist(cfg.items_per_blob.get(), 0, 9)
2928 .await
2929 .unwrap();
2930 drop(journal);
2931
2932 {
2935 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
2936 let (blob, blob_size) = context
2937 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
2938 .await
2939 .expect("failed to open blob 1");
2940 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
2941 .await
2942 .expect("failed to wrap blob 1");
2943 append
2944 .resize(4 * Digest::SIZE as u64)
2945 .await
2946 .expect("failed to shorten blob 1");
2947 append
2948 .sync()
2949 .await
2950 .expect("failed to sync shortened blob 1");
2951 }
2952
2953 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2956 .await
2957 .expect("init should succeed after crash during recovery repair");
2958 assert_eq!(journal.bounds(), 0..9);
2959 assert_eq!(journal.0.recovery_watermark(), 9);
2960 assert_eq!(journal.read(8).await.unwrap(), test_digest(8));
2961 assert!(matches!(
2962 journal.read(9).await,
2963 Err(Error::ItemOutOfRange(9))
2964 ));
2965 assert_eq!(
2966 journal.test_newest_blob(),
2967 Some(1),
2968 "stale blobs beyond the repair point should be removed"
2969 );
2970
2971 journal.destroy().await.unwrap();
2972 });
2973 }
2974
2975 #[test_traced]
2976 fn test_fixed_journal_recover_accepts_clean_short_tail() {
2977 let executor = deterministic::Runner::default();
2978 executor.start(|context| async move {
2979 let cfg = test_cfg(&context, NZU64!(5));
2980
2981 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
2984 .await
2985 .unwrap();
2986 for i in 0..7 {
2987 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2988 }
2989 journal.sync().await.unwrap();
2990
2991 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2993 .await
2994 .unwrap();
2995 assert_eq!(journal.size(), 7);
2996 for i in 0..7u64 {
2998 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
2999 }
3000 journal.destroy().await.unwrap();
3001 });
3002 }
3003
3004 #[test_traced]
3005 fn test_fixed_journal_recover_accepts_clean_empty_tail() {
3006 let executor = deterministic::Runner::default();
3007 executor.start(|context| async move {
3008 let cfg = test_cfg(&context, NZU64!(5));
3009
3010 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3013 .await
3014 .unwrap();
3015 for i in 0..5 {
3016 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
3017 }
3018 journal.sync().await.unwrap();
3019
3020 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3022 .await
3023 .unwrap();
3024 assert_eq!(journal.size(), 5);
3025 for i in 0..5u64 {
3026 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3027 }
3028 assert_eq!(journal.test_newest_blob(), Some(1));
3029 journal.destroy().await.unwrap();
3030 });
3031 }
3032
3033 #[test_traced]
3034 fn test_fixed_journal_recover_sparse_blob_ids_repairs_at_gap() {
3035 let executor = deterministic::Runner::default();
3036 executor.start(|context| async move {
3037 let cfg = test_cfg(&context, NZU64!(1));
3038 let blob_partition = blob_partition(&cfg);
3039
3040 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3041 .await
3042 .unwrap();
3043 (journal, _) = journal.append(&test_digest(0)).await.unwrap();
3044 let journal = journal.sync().await.unwrap();
3045 drop(journal);
3046
3047 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
3050 let (blob, blob_size) = context
3051 .open(&blob_partition, &u64::MAX.to_be_bytes())
3052 .await
3053 .unwrap();
3054 let mut append = Writer::new(blob, blob_size, 2048, cache_ref).await.unwrap();
3055 let extra = test_digest(999);
3056 append.append(extra.as_ref()).await.unwrap();
3057 append.sync().await.unwrap();
3058 drop(append);
3059
3060 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3061 .await
3062 .unwrap();
3063 assert_eq!(journal.bounds(), 0..1);
3064 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
3065 assert!(matches!(
3066 journal.read(1).await,
3067 Err(Error::ItemOutOfRange(1))
3068 ));
3069 assert_eq!(journal.test_newest_blob(), Some(1));
3070
3071 journal.destroy().await.unwrap();
3072 });
3073 }
3074
3075 #[test_traced]
3076 fn test_fixed_journal_recover_fallback_truncates_after_short_oldest_blob() {
3077 let executor = deterministic::Runner::default();
3078 executor.start(|context| async move {
3079 let cfg = test_cfg(&context, NZU64!(5));
3080 let mut journal =
3081 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
3082 .await
3083 .expect("failed to initialize journal at size");
3084
3085 for i in 0..8u64 {
3086 (journal, _) = journal
3087 .append(&test_digest(100 + i))
3088 .await
3089 .expect("failed to append data");
3090 }
3091 let journal = journal.sync().await.expect("failed to sync journal");
3092 assert_eq!(journal.bounds(), 7..15);
3093
3094 let journal = journal
3095 .test_set_recovery_watermark(6)
3096 .await
3097 .expect("failed to sync lower recovery watermark");
3098 drop(journal);
3099
3100 let (blob, size) = context
3101 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
3102 .await
3103 .expect("failed to open oldest blob");
3104 blob.resize(size - 1).await.expect("failed to corrupt blob");
3105 blob.sync().await.expect("failed to sync blob");
3106
3107 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3108 .await
3109 .expect("failed to recover journal");
3110 assert_eq!(journal.bounds(), 7..9);
3111 assert_eq!(journal.read(7).await.unwrap(), test_digest(100));
3112 assert_eq!(journal.read(8).await.unwrap(), test_digest(101));
3113 assert!(matches!(
3114 journal.read(9).await,
3115 Err(Error::ItemOutOfRange(9))
3116 ));
3117 assert_eq!(journal.test_oldest_blob(), Some(1));
3118 assert_eq!(journal.test_newest_blob(), Some(1));
3119
3120 journal.destroy().await.unwrap();
3121 });
3122 }
3123
3124 #[test_traced]
3125 fn test_fixed_journal_stale_pruning_metadata_preserves_watermark() {
3126 let executor = deterministic::Runner::default();
3127 executor.start(|context| async move {
3128 let cfg = test_cfg(&context, NZU64!(5));
3129 let mut journal =
3130 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
3131 .await
3132 .expect("failed to initialize journal at size");
3133
3134 for i in 0..10u64 {
3135 (journal, _) = journal
3136 .append(&test_digest(i))
3137 .await
3138 .expect("failed to append data");
3139 }
3140 let journal = journal.sync().await.expect("failed to sync journal");
3141 assert_eq!(journal.bounds(), 7..17);
3142
3143 let journal = journal
3146 .test_set_recovery_watermark(12)
3147 .await
3148 .expect("failed to sync recovery watermark");
3149 drop(journal);
3150
3151 {
3154 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
3155 let (blob, blob_size) = context
3156 .open(&blob_partition(&cfg), &2u64.to_be_bytes())
3157 .await
3158 .expect("failed to open blob 2");
3159 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
3160 .await
3161 .expect("failed to wrap blob 2");
3162 append
3163 .resize(2 * Digest::SIZE as u64)
3164 .await
3165 .expect("failed to shorten anchored blob");
3166 append.sync().await.expect("failed to sync blob 2");
3167 }
3168
3169 context
3172 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
3173 .await
3174 .expect("failed to remove stale oldest blob");
3175
3176 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3177 .await
3178 .expect("failed to recover journal");
3179 assert_eq!(journal.bounds(), 10..12);
3180 assert_eq!(journal.0.recovery_watermark(), 12);
3181 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
3182 assert_eq!(journal.read(11).await.unwrap(), test_digest(4));
3183 assert!(matches!(
3184 journal.read(12).await,
3185 Err(Error::ItemOutOfRange(12))
3186 ));
3187
3188 journal.destroy().await.unwrap();
3189 });
3190 }
3191
3192 #[test_traced]
3193 fn test_fixed_journal_stale_pruning_metadata_without_watermark_walks_lengths() {
3194 let executor = deterministic::Runner::default();
3195 executor.start(|context| async move {
3196 let cfg = test_cfg(&context, NZU64!(5));
3197 let mut journal =
3198 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
3199 .await
3200 .expect("failed to initialize journal at size");
3201
3202 for i in 0..10u64 {
3203 (journal, _) = journal
3204 .append(&test_digest(i))
3205 .await
3206 .expect("failed to append data");
3207 }
3208 let mut journal = journal.sync().await.expect("failed to sync journal");
3209 assert_eq!(journal.bounds(), 7..17);
3210
3211 {
3212 journal.0.checkpoint.set_watermark(None);
3213 journal.0.checkpoint = journal
3214 .0
3215 .checkpoint
3216 .sync()
3217 .await
3218 .expect("failed to remove recovery watermark");
3219 }
3220 drop(journal);
3221
3222 context
3225 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
3226 .await
3227 .expect("failed to remove stale oldest blob");
3228
3229 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3230 .await
3231 .expect("failed to recover journal");
3232 assert_eq!(journal.bounds(), 10..17);
3233 assert_eq!(journal.0.recovery_watermark(), 15);
3235 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
3236 assert_eq!(journal.read(16).await.unwrap(), test_digest(9));
3237
3238 journal = journal.sync().await.expect("failed to sync");
3240 assert_eq!(journal.0.recovery_watermark(), 17);
3241
3242 journal.destroy().await.unwrap();
3243 });
3244 }
3245
3246 #[test_traced]
3250 fn test_fixed_journal_boundary_hint_ahead_of_blobs_is_corruption() {
3251 let executor = deterministic::Runner::default();
3252 executor.start(|context| async move {
3253 let cfg = test_cfg(&context, NZU64!(5));
3254 let mut journal =
3255 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 3)
3256 .await
3257 .unwrap();
3258
3259 for i in 0..12u64 {
3261 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
3262 }
3263 let mut journal = journal.sync().await.unwrap();
3264 assert_eq!(journal.bounds(), 3..15);
3265
3266 {
3271 journal.0.checkpoint.set_boundary_hint(8);
3272 journal.0.checkpoint.set_watermark(Some(3));
3273 journal.0.checkpoint = journal.0.checkpoint.sync().await.unwrap();
3274 }
3275 drop(journal);
3276
3277 context
3278 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
3279 .await
3280 .unwrap();
3281
3282 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
3283 assert!(matches!(result, Err(Error::Corruption(_))));
3284 });
3285 }
3286
3287 #[test_traced]
3290 fn test_fixed_journal_boundary_hint_with_no_blobs_is_corruption() {
3291 let executor = deterministic::Runner::default();
3292 executor.start(|context| async move {
3293 let cfg = test_cfg(&context, NZU64!(5));
3294 let mut journal =
3295 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
3296 .await
3297 .unwrap();
3298
3299 for i in 0..3u64 {
3300 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
3301 }
3302 let journal = journal.sync().await.unwrap();
3303 drop(journal);
3304
3305 for name in scan_partition(&context, &blob_partition(&cfg)).await {
3307 context
3308 .remove(&blob_partition(&cfg), Some(&name))
3309 .await
3310 .unwrap();
3311 }
3312
3313 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
3314 assert!(matches!(result, Err(Error::Corruption(_))));
3315 });
3316 }
3317
3318 #[test_traced]
3319 fn test_fixed_journal_legacy_recovery_installs_watermark() {
3320 let executor = deterministic::Runner::default();
3321 executor.start(|context| async move {
3322 let cfg = test_cfg(&context, NZU64!(5));
3323 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3324 .await
3325 .expect("failed to initialize journal");
3326
3327 for i in 0..12u64 {
3328 (journal, _) = journal
3329 .append(&test_digest(i))
3330 .await
3331 .expect("failed to append data");
3332 }
3333 let mut journal = journal.sync().await.expect("failed to sync journal");
3334
3335 {
3336 journal.0.checkpoint.set_watermark(None);
3337 journal.0.checkpoint = journal
3338 .0
3339 .checkpoint
3340 .sync()
3341 .await
3342 .expect("failed to remove recovery watermark");
3343 }
3344 drop(journal);
3345
3346 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3349 .await
3350 .expect("failed to recover legacy journal");
3351 assert_eq!(journal.bounds(), 0..12);
3352 assert_eq!(journal.0.recovery_watermark(), 10);
3353
3354 journal = journal
3356 .sync()
3357 .await
3358 .expect("failed to sync after legacy recovery");
3359 assert_eq!(journal.0.recovery_watermark(), 12);
3360
3361 journal.destroy().await.unwrap();
3362 });
3363 }
3364
3365 #[test_traced]
3369 fn test_fixed_journal_legacy_upgrade_syncs_recovered_tail() {
3370 let executor = deterministic::Runner::default();
3371 executor.start(|context| async move {
3372 let cfg = test_cfg(&context, NZU64!(5));
3373 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3374 .await
3375 .unwrap();
3376
3377 for i in 0..7u64 {
3378 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
3379 }
3380 let mut journal = journal.sync().await.unwrap();
3381
3382 {
3384 journal.0.checkpoint.set_watermark(None);
3385 journal.0.checkpoint = journal.0.checkpoint.sync().await.unwrap();
3386 }
3387 drop(journal);
3388
3389 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3390 .await
3391 .unwrap();
3392 assert_eq!(journal.size(), 7);
3393 assert_eq!(journal.0.recovery_watermark(), 5);
3395
3396 *context.storage_fault_config().write() = deterministic::FaultConfig {
3399 sync_rate: Some(probability!(1.0)),
3400 ..Default::default()
3401 };
3402 assert!(
3403 journal.commit().await.is_err(),
3404 "commit must sync recovered data before the watermark can advance"
3405 );
3406 });
3407 }
3408
3409 #[test_traced]
3410 fn test_fixed_journal_commit_does_not_advance_recovery_watermark() {
3411 let executor = deterministic::Runner::default();
3412 executor.start(|context| async move {
3413 let cfg = test_cfg(&context, NZU64!(5));
3414 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
3415 .await
3416 .unwrap();
3417
3418 (journal, _) = journal.append(&test_digest(0)).await.unwrap();
3419 let mut journal = journal.sync().await.unwrap();
3420 assert_eq!(journal.0.recovery_watermark(), 1);
3421
3422 (journal, _) = journal.append(&test_digest(1)).await.unwrap();
3423 let mut journal = journal.commit().await.unwrap();
3424 assert_eq!(
3425 journal.0.recovery_watermark(),
3426 1,
3427 "commit must make new data durable without advancing the recovery watermark",
3428 );
3429
3430 journal = journal.sync().await.unwrap();
3431 assert_eq!(journal.0.recovery_watermark(), 2);
3432 journal.destroy().await.unwrap();
3433 });
3434 }
3435
3436 #[test_traced]
3437 fn test_fixed_journal_prune_to_blob_boundary_removes_pruning_metadata() {
3438 let executor = deterministic::Runner::default();
3439 executor.start(|context| async move {
3440 let cfg = test_cfg(&context, NZU64!(5));
3441 let mut journal =
3442 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
3443 .await
3444 .expect("failed to initialize journal at size");
3445
3446 for i in 0..8u64 {
3447 (journal, _) = journal
3448 .append(&test_digest(i))
3449 .await
3450 .expect("failed to append data");
3451 }
3452 let mut journal = journal.sync().await.expect("failed to sync journal");
3453 assert_eq!(journal.bounds(), 7..15);
3454
3455 (journal, _) = journal.prune(10).await.expect("failed to prune journal");
3456 let journal = journal.sync().await.expect("failed to sync pruned journal");
3457 assert_eq!(journal.bounds(), 10..15);
3458 drop(journal);
3459
3460 let checkpoint = Checkpoint::open(context.child("metadata"), &cfg.partition)
3461 .await
3462 .expect("failed to reopen checkpoint");
3463 assert!(checkpoint.boundary_hint().is_none());
3464 drop(checkpoint);
3465
3466 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3467 .await
3468 .expect("failed to reopen journal");
3469 assert_eq!(journal.bounds(), 10..15);
3470 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
3471 journal.destroy().await.unwrap();
3472 });
3473 }
3474
3475 #[test_traced]
3476 fn test_fixed_journal_recover_rejects_overlong_blob() {
3477 let executor = deterministic::Runner::default();
3478 executor.start(|context| async move {
3479 let cfg = test_cfg(&context, NZU64!(5));
3480 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3481 .await
3482 .expect("failed to initialize journal");
3483
3484 for i in 0..5u64 {
3485 (journal, _) = journal
3486 .append(&test_digest(i))
3487 .await
3488 .expect("failed to append data");
3489 }
3490 let journal = journal.sync().await.expect("failed to sync journal");
3491 drop(journal);
3492
3493 {
3496 let extra = test_digest(99);
3497 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE);
3498 let (blob, blob_size) = context
3499 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
3500 .await
3501 .expect("failed to open blob 0");
3502 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
3503 .await
3504 .expect("failed to wrap blob 0");
3505 append
3506 .append(extra.as_ref())
3507 .await
3508 .expect("failed to append extra item");
3509 append.sync().await.expect("failed to sync corrupted blob");
3510 }
3511
3512 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
3513 assert!(matches!(result, Err(Error::Corruption(_))));
3514 });
3515 }
3516
3517 #[test_traced("DEBUG")]
3518 fn test_fixed_journal_recover_from_unwritten_data() {
3519 let executor = deterministic::Runner::default();
3520 executor.start(|context| async move {
3521 let cfg = test_cfg(&context, NZU64!(10));
3523 let mut journal = Journal::init(context.child("first"), cfg.clone())
3524 .await
3525 .expect("failed to initialize journal");
3526
3527 (journal, _) = journal
3529 .append(&test_digest(0))
3530 .await
3531 .expect("failed to append data");
3532 assert_eq!(journal.size(), 1);
3533 let journal = journal.sync().await.expect("Failed to sync journal");
3534 drop(journal);
3535
3536 let (blob, size) = context
3539 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
3540 .await
3541 .expect("Failed to open blob");
3542 blob.write_at(
3543 size,
3544 vec![0u8; PAGE_SIZE.get() as usize * 3],
3545 WriteOptions::SYNC,
3546 )
3547 .await
3548 .expect("Failed to extend blob");
3549
3550 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3552 .await
3553 .expect("Failed to re-initialize journal");
3554
3555 assert_eq!(journal.size(), 1);
3558
3559 (journal, _) = journal
3561 .append(&test_digest(1))
3562 .await
3563 .expect("failed to append data");
3564
3565 journal.destroy().await.unwrap();
3566 });
3567 }
3568
3569 #[test_traced]
3570 fn test_fixed_journal_rewinding() {
3571 let executor = deterministic::Runner::default();
3572 executor.start(|context| async move {
3573 let cfg = test_cfg(&context, NZU64!(2));
3575 let journal: Journal<_, Digest> = Journal::init(context.child("first"), cfg.clone())
3576 .await
3577 .expect("failed to initialize journal");
3578 let journal = journal.rewind(0).await.unwrap();
3579 assert!(matches!(
3580 journal.rewind(1).await,
3581 Err(Error::InvalidRewind(1))
3582 ));
3583 let mut journal: Journal<_, Digest> =
3584 Journal::init(context.child("reopen"), cfg.clone())
3585 .await
3586 .expect("failed to re-initialize journal");
3587
3588 (journal, _) = journal
3590 .append(&test_digest(0))
3591 .await
3592 .expect("failed to append data 0");
3593 assert_eq!(journal.size(), 1);
3594 journal = journal.rewind(1).await.unwrap(); journal = journal.rewind(0).await.unwrap();
3596 assert_eq!(journal.size(), 0);
3597
3598 for i in 0..7 {
3600 let pos;
3601 (journal, pos) = journal
3602 .append(&test_digest(i))
3603 .await
3604 .expect("failed to append data");
3605 assert_eq!(pos, i);
3606 }
3607 assert_eq!(journal.size(), 7);
3608
3609 journal = journal.rewind(4).await.unwrap();
3611 assert_eq!(journal.size(), 4);
3612
3613 journal = journal.rewind(0).await.unwrap();
3615 assert_eq!(journal.size(), 0);
3616
3617 for _ in 0..10 {
3619 for i in 0..100 {
3620 (journal, _) = journal
3621 .append(&test_digest(i))
3622 .await
3623 .expect("failed to append data");
3624 }
3625 let size = journal.size();
3626 journal = journal.rewind(size - 49).await.unwrap();
3627 }
3628 const ITEMS_REMAINING: u64 = 10 * (100 - 49);
3629 assert_eq!(journal.size(), ITEMS_REMAINING);
3630
3631 let journal = journal.sync().await.expect("Failed to sync journal");
3632 drop(journal);
3633
3634 let mut cfg = test_cfg(&context, NZU64!(3));
3636 cfg.partition = "test-partition-2".into();
3637 let mut journal = Journal::init(context.child("second"), cfg.clone())
3638 .await
3639 .expect("failed to initialize journal");
3640 for _ in 0..10 {
3641 for i in 0..100 {
3642 (journal, _) = journal
3643 .append(&test_digest(i))
3644 .await
3645 .expect("failed to append data");
3646 }
3647 let size = journal.size();
3648 journal = journal.rewind(size - 49).await.unwrap();
3649 }
3650 assert_eq!(journal.size(), ITEMS_REMAINING);
3651
3652 journal.sync().await.expect("Failed to sync journal");
3653
3654 let mut journal: Journal<_, Digest> =
3656 Journal::init(context.child("third"), cfg.clone())
3657 .await
3658 .expect("failed to re-initialize journal");
3659 assert_eq!(journal.size(), 10 * (100 - 49));
3660
3661 (journal, _) = journal.prune(300).await.expect("pruning failed");
3663 assert_eq!(journal.size(), ITEMS_REMAINING);
3664 journal = journal.rewind(300).await.unwrap();
3667 let bounds = journal.bounds();
3668 assert_eq!(bounds.end, 300);
3669 assert!(bounds.is_empty());
3670
3671 assert!(matches!(
3673 journal.rewind(299).await,
3674 Err(Error::ItemPruned(299))
3675 ));
3676 });
3677 }
3678
3679 #[test_traced]
3680 fn test_fixed_journal_rewind_commit_reopen() {
3681 let executor = deterministic::Runner::default();
3682 executor.start(|context| async move {
3683 let cfg = test_cfg(&context, NZU64!(5));
3684 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3685 .await
3686 .expect("failed to initialize journal");
3687
3688 for i in 0..12u64 {
3689 (journal, _) = journal
3690 .append(&test_digest(i))
3691 .await
3692 .expect("failed to append data");
3693 }
3694 let journal = journal.sync().await.expect("failed to sync journal");
3695
3696 let journal = journal.rewind(7).await.expect("failed to rewind journal");
3697 journal.commit().await.expect("failed to commit journal");
3698
3699 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3700 .await
3701 .expect("failed to re-initialize journal");
3702 assert_eq!(journal.bounds(), 0..7);
3703 for i in 0..7u64 {
3704 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3705 }
3706 assert!(matches!(
3707 journal.read(7).await,
3708 Err(Error::ItemOutOfRange(7))
3709 ));
3710
3711 journal.destroy().await.unwrap();
3712 });
3713 }
3714
3715 #[test_traced]
3716 fn test_fixed_journal_rewind_persists_lower_watermark() {
3717 let executor = deterministic::Runner::default();
3718 executor.start(|context| async move {
3719 let cfg = test_cfg(&context, NZU64!(5));
3720 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3721 .await
3722 .expect("failed to initialize journal");
3723
3724 for i in 0..12u64 {
3725 (journal, _) = journal
3726 .append(&test_digest(i))
3727 .await
3728 .expect("failed to append data");
3729 }
3730 let journal = journal.sync().await.expect("failed to sync journal");
3731 journal.rewind(7).await.expect("failed to rewind journal");
3732
3733 let checkpoint = Checkpoint::open(context.child("metadata"), &cfg.partition)
3734 .await
3735 .expect("failed to reopen checkpoint");
3736 let persisted_watermark = checkpoint
3737 .watermark()
3738 .expect("missing recovery watermark after rewind");
3739 assert_eq!(persisted_watermark, 7);
3740 drop(checkpoint);
3741
3742 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3743 .await
3744 .expect("failed to re-initialize journal");
3745 journal.destroy().await.unwrap();
3746 });
3747 }
3748
3749 #[test_traced]
3750 fn test_fixed_journal_recover_after_watermark_lowered_before_rewind() {
3751 let executor = deterministic::Runner::default();
3752 executor.start(|context| async move {
3753 let cfg = test_cfg(&context, NZU64!(5));
3754 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3755 .await
3756 .expect("failed to initialize journal");
3757
3758 for i in 0..12u64 {
3759 (journal, _) = journal
3760 .append(&test_digest(i))
3761 .await
3762 .expect("failed to append data");
3763 }
3764 let journal = journal.sync().await.expect("failed to sync journal");
3765
3766 let journal = journal
3767 .test_set_recovery_watermark(7)
3768 .await
3769 .expect("failed to lower recovery watermark");
3770 drop(journal);
3771
3772 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3773 .await
3774 .expect("failed to recover journal");
3775 assert_eq!(journal.bounds(), 0..12);
3776 assert_eq!(journal.0.recovery_watermark(), 7);
3777 assert_eq!(journal.read(11).await.unwrap(), test_digest(11));
3778 journal.destroy().await.unwrap();
3779 });
3780 }
3781
3782 #[test_traced]
3783 fn test_fixed_journal_rewind_append_commit_reopen() {
3784 let executor = deterministic::Runner::default();
3785 executor.start(|context| async move {
3786 let cfg = test_cfg(&context, NZU64!(5));
3787 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3788 .await
3789 .expect("failed to initialize journal");
3790
3791 for i in 0..12u64 {
3792 (journal, _) = journal
3793 .append(&test_digest(i))
3794 .await
3795 .expect("failed to append data");
3796 }
3797 let journal = journal.sync().await.expect("failed to sync journal");
3798
3799 let mut journal = journal.rewind(7).await.expect("failed to rewind journal");
3800 for i in 0..3u64 {
3801 (journal, _) = journal
3802 .append(&test_digest(100 + i))
3803 .await
3804 .expect("failed to append data");
3805 }
3806 journal.commit().await.expect("failed to commit journal");
3807
3808 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3809 .await
3810 .expect("failed to re-initialize journal");
3811 assert_eq!(journal.bounds(), 0..10);
3812 assert_eq!(journal.0.recovery_watermark(), 7);
3813 for i in 0..7u64 {
3814 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
3815 }
3816 for i in 0..3u64 {
3817 assert_eq!(journal.read(7 + i).await.unwrap(), test_digest(100 + i));
3818 }
3819 assert!(matches!(
3820 journal.read(10).await,
3821 Err(Error::ItemOutOfRange(10))
3822 ));
3823
3824 journal.destroy().await.unwrap();
3825 });
3826 }
3827
3828 #[test_traced]
3829 fn test_fixed_recovery_preserves_rolled_predecessors_without_commit() {
3830 let executor = deterministic::Runner::default();
3831 executor.start(|context| async move {
3832 let cfg = test_cfg(&context, NZU64!(1));
3833 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
3834 .await
3835 .unwrap();
3836
3837 let appended;
3841 (journal, appended) = journal.append(&test_digest(10)).await.unwrap();
3842 assert_eq!(appended, 0);
3843 journal = journal.sync().await.unwrap();
3844 let appended;
3845 (journal, appended) = journal.append(&test_digest(20)).await.unwrap();
3846 assert_eq!(appended, 1);
3847 let appended;
3848 (journal, appended) = journal.append(&test_digest(30)).await.unwrap();
3849 assert_eq!(appended, 2);
3850 drop(journal);
3851
3852 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3853 assert!(
3854 blobs.len() > 2,
3855 "expected multiple empty trailing blobs, got {}",
3856 blobs.len()
3857 );
3858
3859 let journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3860 .await
3861 .unwrap();
3862 assert_eq!(journal.bounds(), 0..3);
3863 assert_eq!(journal.read(0).await.unwrap(), test_digest(10));
3864 assert_eq!(journal.read(1).await.unwrap(), test_digest(20));
3865 assert_eq!(journal.read(2).await.unwrap(), test_digest(30));
3866 drop(journal);
3867
3868 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3870 assert_eq!(blobs.len(), 4);
3871
3872 let mut journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3873 .await
3874 .unwrap();
3875 let appended;
3876 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
3877 assert_eq!(appended, 3);
3878 assert_eq!(journal.read(3).await.unwrap(), test_digest(42));
3879 journal.destroy().await.unwrap();
3880 });
3881 }
3882
3883 #[test_traced]
3884 fn test_fixed_recovery_preserves_first_rollover_without_commit() {
3885 let executor = deterministic::Runner::default();
3886 executor.start(|context| async move {
3887 let cfg = test_cfg(&context, NZU64!(1));
3888 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
3889 .await
3890 .unwrap();
3891
3892 let appended;
3895 (journal, appended) = journal.append(&test_digest(10)).await.unwrap();
3896 assert_eq!(appended, 0);
3897 let appended;
3898 (journal, appended) = journal.append(&test_digest(20)).await.unwrap();
3899 assert_eq!(appended, 1);
3900 drop(journal);
3901
3902 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3903 assert!(
3904 blobs.len() > 1,
3905 "expected multiple empty blobs, got {}",
3906 blobs.len()
3907 );
3908
3909 let journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3910 .await
3911 .unwrap();
3912 assert_eq!(journal.bounds(), 0..2);
3913 drop(journal);
3914
3915 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
3917 assert_eq!(blobs.len(), 3);
3918
3919 let mut journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
3920 .await
3921 .unwrap();
3922 let appended;
3923 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
3924 assert_eq!(appended, 2);
3925 assert_eq!(journal.read(2).await.unwrap(), test_digest(42));
3926 journal.destroy().await.unwrap();
3927 });
3928 }
3929
3930 #[test_traced]
3932 fn test_fixed_recovery_validates_pages_before_trailing_bytes() {
3933 let executor = deterministic::Runner::default();
3934 executor.start(|context| async move {
3935 const LOGICAL_PAGE_SIZE: u64 = 5;
3936
3937 let cfg = Config {
3938 partition: "fixed-validate-before-tail-trim".into(),
3939 items_per_blob: NZU64!(10),
3940 page_cache: CacheRef::from_pooler(
3941 &context,
3942 NZU16!(LOGICAL_PAGE_SIZE as u16),
3943 NZUsize!(4),
3944 ),
3945 write_buffer: NZUsize!(128),
3946 replay_buffer: NZUsize!(128),
3947 };
3948 let partition = blob_partition(&cfg);
3949 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
3950 let mut writer = Writer::new(blob, size, 128, cfg.page_cache.clone())
3951 .await
3952 .unwrap();
3953 let values = [11u64, 22, 33, 44];
3954 let mut bytes = Vec::new();
3955 for value in values {
3956 bytes.extend_from_slice(&value.to_be_bytes());
3957 }
3958 writer.append(&bytes).await.unwrap();
3959 writer.resize(30).await.unwrap();
3960 writer.sync().await.unwrap();
3961 drop(writer);
3962
3963 corrupt_page(
3975 &context,
3976 &partition,
3977 &0u64.to_be_bytes(),
3978 4,
3979 LOGICAL_PAGE_SIZE,
3980 )
3981 .await;
3982
3983 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
3984 .await
3985 .unwrap();
3986 assert_eq!(journal.bounds(), 0..2);
3987 assert_eq!(journal.read(0).await.unwrap(), 11);
3988 assert_eq!(journal.read(1).await.unwrap(), 22);
3989 journal.destroy().await.unwrap();
3990 });
3991 }
3992
3993 #[test_traced]
3996 fn test_fixed_recovery_watermark_uses_retained_blob_start() {
3997 let executor = deterministic::Runner::default();
3998 executor.start(|context| async move {
3999 let cfg = test_cfg(&context, NZU64!(10));
4000 let mut journal =
4001 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
4002 .await
4003 .unwrap();
4004 for (offset, value) in [11u64, 22].into_iter().enumerate() {
4005 let position;
4006 (journal, position) = journal.append(&value).await.unwrap();
4007 assert_eq!(position, 7 + offset as u64);
4008 }
4009 journal = journal.sync().await.unwrap();
4010 drop(journal);
4011
4012 let partition = blob_partition(&cfg);
4013 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
4014 let mut writer =
4015 Writer::new(blob, size, cfg.write_buffer.get(), cfg.page_cache.clone())
4016 .await
4017 .unwrap();
4018 assert_eq!(writer.size(), 16);
4019 writer.resize(20).await.unwrap();
4020 writer.sync().await.unwrap();
4021 drop(writer);
4022
4023 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
4024 .await
4025 .unwrap();
4026 assert_eq!(journal.bounds(), 7..9);
4027 assert_eq!(journal.read(7).await.unwrap(), 11);
4028 assert_eq!(journal.read(8).await.unwrap(), 22);
4029 journal.destroy().await.unwrap();
4030 });
4031 }
4032
4033 #[test_traced]
4037 fn test_fixed_recovery_truncates_partial_item_tail() {
4038 let executor = deterministic::Runner::default();
4039 executor.start(|context| async move {
4040 let cfg = test_cfg(&context, NZU64!(10));
4041 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4042 .await
4043 .unwrap();
4044 for value in [11u64, 22, 33] {
4045 (journal, _) = journal.append(&value).await.unwrap();
4046 }
4047 journal = journal.sync().await.unwrap();
4048 drop(journal);
4049
4050 let partition = blob_partition(&cfg);
4053 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
4054 let mut writer =
4055 Writer::new(blob, size, cfg.write_buffer.get(), cfg.page_cache.clone())
4056 .await
4057 .unwrap();
4058 assert_eq!(writer.size(), 24);
4059 writer.append(&44u64.to_be_bytes()[..3]).await.unwrap();
4060 writer.sync().await.unwrap();
4061 drop(writer);
4062
4063 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg)
4065 .await
4066 .unwrap();
4067 assert_eq!(journal.bounds(), 0..3);
4068 assert_eq!(journal.read(2).await.unwrap(), 33);
4069 let position;
4070 (journal, position) = journal.append(&44u64).await.unwrap();
4071 assert_eq!(position, 3);
4072 let journal = journal.sync().await.unwrap();
4073 assert_eq!(journal.read(3).await.unwrap(), 44);
4074 journal.destroy().await.unwrap();
4075 });
4076 }
4077
4078 #[test_traced]
4082 fn test_fixed_recovery_truncates_above_mid_blob_watermark() {
4083 let executor = deterministic::Runner::default();
4084 executor.start(|context| async move {
4085 const LOGICAL_PAGE_SIZE: u64 = 5;
4086 let cfg = Config {
4087 partition: "fixed-truncate-above-watermark".into(),
4088 items_per_blob: NZU64!(10),
4089 page_cache: CacheRef::from_pooler(
4090 &context,
4091 NZU16!(LOGICAL_PAGE_SIZE as u16),
4092 NZUsize!(4),
4093 ),
4094 write_buffer: NZUsize!(128),
4095 replay_buffer: NZUsize!(128),
4096 };
4097 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4098 .await
4099 .unwrap();
4100 for value in [11u64, 22] {
4101 (journal, _) = journal.append(&value).await.unwrap();
4102 }
4103 journal.sync().await.unwrap();
4104
4105 let partition = blob_partition(&cfg);
4109 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
4110 let mut writer = Writer::new(blob, size, 128, cfg.page_cache.clone())
4111 .await
4112 .unwrap();
4113 assert_eq!(writer.size(), 16);
4114 let mut bytes = Vec::new();
4115 for value in [33u64, 44] {
4116 bytes.extend_from_slice(&value.to_be_bytes());
4117 }
4118 writer.append(&bytes).await.unwrap();
4119 writer.sync().await.unwrap();
4120 drop(writer);
4121 corrupt_page(
4122 &context,
4123 &partition,
4124 &0u64.to_be_bytes(),
4125 1,
4126 LOGICAL_PAGE_SIZE,
4127 )
4128 .await;
4129 corrupt_page(
4130 &context,
4131 &partition,
4132 &0u64.to_be_bytes(),
4133 5,
4134 LOGICAL_PAGE_SIZE,
4135 )
4136 .await;
4137
4138 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
4141 .await
4142 .unwrap();
4143 assert_eq!(journal.bounds(), 0..3);
4144 assert!(journal.read(0).await.is_err());
4145 assert!(journal.read(1).await.is_err());
4146 assert_eq!(journal.read(2).await.unwrap(), 33);
4147 journal.destroy().await.unwrap();
4148 });
4149 }
4150
4151 #[test_traced]
4155 fn test_fixed_recovery_truncates_torn_interior_page() {
4156 let executor = deterministic::Runner::default();
4157 executor.start(|context| async move {
4158 let cfg = test_cfg(&context, NZU64!(10));
4159 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4160 .await
4161 .unwrap();
4162 for i in 0..15u64 {
4163 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4164 }
4165 journal.commit().await.unwrap();
4166
4167 corrupt_page(
4169 &context,
4170 &blob_partition(&cfg),
4171 &0u64.to_be_bytes(),
4172 3,
4173 PAGE_SIZE.get() as u64,
4174 )
4175 .await;
4176
4177 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
4179 .await
4180 .unwrap();
4181 assert_eq!(journal.bounds(), 0..4);
4182 for i in 0..4u64 {
4183 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4184 }
4185 let appended;
4186 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
4187 assert_eq!(appended, 4);
4188 journal.destroy().await.unwrap();
4189 });
4190 }
4191
4192 #[test_traced]
4195 fn test_fixed_recovery_truncates_torn_interior_page_in_tail() {
4196 let executor = deterministic::Runner::default();
4197 executor.start(|context| async move {
4198 let cfg = test_cfg(&context, NZU64!(10));
4199 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4200 .await
4201 .unwrap();
4202 for i in 0..15u64 {
4203 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4204 }
4205 journal.commit().await.unwrap();
4206
4207 corrupt_page(
4209 &context,
4210 &blob_partition(&cfg),
4211 &1u64.to_be_bytes(),
4212 1,
4213 PAGE_SIZE.get() as u64,
4214 )
4215 .await;
4216
4217 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
4219 .await
4220 .unwrap();
4221 assert_eq!(journal.bounds(), 0..11);
4222 for i in 0..11u64 {
4223 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4224 }
4225 let appended;
4226 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
4227 assert_eq!(appended, 11);
4228 journal.destroy().await.unwrap();
4229 });
4230 }
4231
4232 #[test_traced]
4237 fn test_fixed_recovery_adopts_torn_page_below_watermark() {
4238 let executor = deterministic::Runner::default();
4239 executor.start(|context| async move {
4240 let cfg = test_cfg(&context, NZU64!(10));
4241 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4242 .await
4243 .unwrap();
4244 for i in 0..15u64 {
4245 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4246 }
4247 journal.sync().await.unwrap();
4248
4249 corrupt_page(
4250 &context,
4251 &blob_partition(&cfg),
4252 &0u64.to_be_bytes(),
4253 3,
4254 PAGE_SIZE.get() as u64,
4255 )
4256 .await;
4257 let (_, size_before) = context
4258 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
4259 .await
4260 .unwrap();
4261
4262 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4263 .await
4264 .expect("acknowledged damage must not fail recovery");
4265
4266 let (_, size_after) = context
4269 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
4270 .await
4271 .unwrap();
4272 assert_eq!(
4273 size_after, size_before,
4274 "adoption must preserve the evidence"
4275 );
4276 let mut damaged = 0;
4277 for i in 0..10u64 {
4278 match journal.read(i).await {
4279 Ok(item) => assert_eq!(item, test_digest(i)),
4280 Err(_) => damaged += 1,
4281 }
4282 }
4283 assert!(damaged > 0, "the torn page must surface as read errors");
4284 for i in 10..15u64 {
4285 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4286 }
4287 drop(journal);
4288
4289 let _ = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
4291 .await
4292 .unwrap();
4293 let (_, size_retry) = context
4294 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
4295 .await
4296 .unwrap();
4297 assert_eq!(size_retry, size_before);
4298 });
4299 }
4300
4301 #[test_traced]
4306 fn test_fixed_recovery_skips_watermark_covered_blobs() {
4307 let executor = deterministic::Runner::default();
4308 executor.start(|context| async move {
4309 let cfg = test_cfg(&context, NZU64!(10));
4310 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4311 .await
4312 .unwrap();
4313 for i in 0..15u64 {
4314 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4315 }
4316 journal.sync().await.unwrap();
4317
4318 corrupt_page(
4321 &context,
4322 &blob_partition(&cfg),
4323 &0u64.to_be_bytes(),
4324 2,
4325 u64::from(PAGE_SIZE.get()),
4326 )
4327 .await;
4328
4329 let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
4330 .await
4331 .unwrap();
4332 assert_eq!(journal.bounds(), 0..15);
4333 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
4334 assert!(journal.read(2).await.is_err());
4335 assert_eq!(journal.read(14).await.unwrap(), test_digest(14));
4336 journal.destroy().await.unwrap();
4337 });
4338 }
4339
4340 #[test_traced]
4343 fn test_fixed_recovery_adopts_torn_page_below_mid_blob_watermark() {
4344 let executor = deterministic::Runner::default();
4345 executor.start(|context| async move {
4346 let cfg = test_cfg(&context, NZU64!(10));
4347 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4348 .await
4349 .unwrap();
4350 for i in 0..15u64 {
4351 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4352 }
4353 journal.sync().await.unwrap();
4355
4356 corrupt_page(
4358 &context,
4359 &blob_partition(&cfg),
4360 &1u64.to_be_bytes(),
4361 1,
4362 PAGE_SIZE.get() as u64,
4363 )
4364 .await;
4365 let (_, size_before) = context
4366 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4367 .await
4368 .unwrap();
4369
4370 for child in ["second", "retry"] {
4371 let journal = Journal::<_, Digest>::init(context.child(child), cfg.clone())
4372 .await
4373 .expect("acknowledged damage must not fail recovery");
4374 let mut damaged = 0;
4375 for i in 10..15u64 {
4376 match journal.read(i).await {
4377 Ok(item) => assert_eq!(item, test_digest(i)),
4378 Err(_) => damaged += 1,
4379 }
4380 }
4381 assert!(damaged > 0, "the torn page must surface as read errors");
4382 for i in 0..10u64 {
4383 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4384 }
4385 }
4386
4387 let (_, size_after) = context
4389 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4390 .await
4391 .unwrap();
4392 assert_eq!(size_after, size_before);
4393 });
4394 }
4395
4396 #[test_traced]
4401 fn test_fixed_recovery_rejects_torn_boundary_page() {
4402 let executor = deterministic::Runner::default();
4403 executor.start(|context| async move {
4404 let cfg = test_cfg(&context, NZU64!(10));
4405 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4406 .await
4407 .unwrap();
4408 for i in 0..15u64 {
4409 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4410 }
4411
4412 journal.sync().await.unwrap();
4414
4415 let physical_page_size = PAGE_SIZE.get() as u64 + 12;
4417 let offset = 3 * physical_page_size + 5;
4418 let (blob, _) = context
4419 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4420 .await
4421 .unwrap();
4422 let byte = blob
4423 .read_at(offset, 1, commonware_runtime::ReadOptions::default())
4424 .await
4425 .unwrap()
4426 .coalesce();
4427 blob.write_at(
4428 offset,
4429 vec![byte.as_ref()[0] ^ 0xFF],
4430 WriteOptions::default(),
4431 )
4432 .await
4433 .unwrap();
4434 blob.sync().await.unwrap();
4435 drop(blob);
4436
4437 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
4438 assert!(matches!(result, Err(Error::Corruption(_))));
4439 let (_, size_mid) = context
4440 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4441 .await
4442 .unwrap();
4443
4444 let result = Journal::<_, Digest>::init(context.child("retry"), cfg.clone()).await;
4446 assert!(matches!(result, Err(Error::Corruption(_))));
4447 let (_, size_after) = context
4448 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4449 .await
4450 .unwrap();
4451 assert_eq!(size_after, size_mid);
4452 });
4453 }
4454
4455 #[test_traced]
4458 fn test_fixed_recovery_unsynced_tail_keeps_contiguous_prefix() {
4459 let executor = deterministic::Runner::default();
4460 executor.start(|context| async move {
4461 let cfg = test_cfg(&context, NZU64!(10));
4462 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4463 .await
4464 .unwrap();
4465
4466 for i in 0..25u64 {
4469 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4470 }
4471
4472 {
4476 journal.test_sync_blob(0).await.unwrap();
4477 journal.test_sync_blob(1).await.unwrap();
4478 }
4479 drop(journal);
4480
4481 let names = scan_partition(&context, &blob_partition(&cfg)).await;
4484 assert_eq!(names.len(), 3);
4485 for (blob, name) in names.iter().enumerate() {
4486 let (_blob, size) = context.open(&blob_partition(&cfg), name).await.unwrap();
4487 if blob < 2 {
4488 assert!(size > 0, "blob {blob} should be durable");
4489 } else {
4490 assert_eq!(size, 0, "blob {blob} should be empty");
4491 }
4492 }
4493
4494 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4496 .await
4497 .unwrap();
4498 assert_eq!(journal.bounds(), 0..20);
4499 for i in 0..20u64 {
4500 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4501 }
4502 assert!(matches!(
4503 journal.read(20).await,
4504 Err(Error::ItemOutOfRange(20))
4505 ));
4506
4507 let appended;
4509 (journal, appended) = journal.append(&test_digest(999)).await.unwrap();
4510 assert_eq!(appended, 20);
4511 assert_eq!(journal.read(20).await.unwrap(), test_digest(999));
4512
4513 journal.destroy().await.unwrap();
4514 });
4515 }
4516
4517 #[test_traced]
4526 fn test_fixed_recovery_rolls_back_durable_blob_after_gap() {
4527 let executor = deterministic::Runner::default();
4528 executor.start(|context| async move {
4529 let cfg = test_cfg(&context, NZU64!(10));
4530 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4531 .await
4532 .unwrap();
4533
4534 for i in 0..10u64 {
4536 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4537 }
4538 journal = journal.sync().await.unwrap();
4539
4540 for i in 10..28u64 {
4543 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4544 }
4545 {
4546 journal.test_sync_blob(2).await.unwrap();
4547 }
4548 drop(journal);
4549 let (blob, _) = context
4550 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4551 .await
4552 .unwrap();
4553 blob.resize(0).await.unwrap();
4554 blob.sync().await.unwrap();
4555
4556 let names = scan_partition(&context, &blob_partition(&cfg)).await;
4558 assert_eq!(names.len(), 3);
4559 let mut sizes = Vec::new();
4560 for name in &names {
4561 let (_blob, size) = context.open(&blob_partition(&cfg), name).await.unwrap();
4562 sizes.push(size);
4563 }
4564 assert!(sizes[0] > 0, "blob 0 should be durable");
4565 assert_eq!(sizes[1], 0, "blob 1 should be the gap");
4566 assert!(sizes[2] > 0, "blob 2 should be incidentally durable");
4567
4568 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4571 .await
4572 .unwrap();
4573 assert_eq!(journal.bounds(), 0..10);
4574 for i in 0..10u64 {
4575 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4576 }
4577 assert!(matches!(
4578 journal.read(10).await,
4579 Err(Error::ItemOutOfRange(10))
4580 ));
4581
4582 let names = scan_partition(&context, &blob_partition(&cfg)).await;
4584 assert_eq!(names.len(), 2);
4585
4586 let appended;
4588 (journal, appended) = journal.append(&test_digest(999)).await.unwrap();
4589 assert_eq!(appended, 10);
4590 assert_eq!(journal.read(10).await.unwrap(), test_digest(999));
4591
4592 journal.destroy().await.unwrap();
4593 });
4594 }
4595
4596 #[test_traced]
4601 fn test_fixed_recovery_prune_crash_retains_unsynced_tail() {
4602 let executor = deterministic::Runner::default();
4603 executor.start(|context| async move {
4604 let cfg = test_cfg(&context, NZU64!(10));
4605 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4606 .await
4607 .unwrap();
4608
4609 for i in 0..10u64 {
4612 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4613 }
4614 journal = journal.sync().await.unwrap();
4615 for i in 10..25u64 {
4616 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4617 }
4618
4619 let (journal, pruned) = journal.prune(10).await.unwrap();
4621 assert!(pruned);
4622 drop(journal);
4623
4624 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4625 .await
4626 .unwrap();
4627 assert_eq!(journal.bounds(), 10..25);
4628 for i in 10..25u64 {
4629 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4630 }
4631 journal.destroy().await.unwrap();
4632 });
4633 }
4634
4635 #[test_traced]
4643 fn test_fixed_recovery_empty_oldest_blob_orphaned_newer_blob() {
4644 let executor = deterministic::Runner::default();
4645 executor.start(|context| async move {
4646 let cfg = test_cfg(&context, NZU64!(10));
4647
4648 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4650 .await
4651 .unwrap();
4652 for i in 0..20u64 {
4653 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4654 }
4655 let journal = journal.sync().await.unwrap();
4656 drop(journal);
4657
4658 let (blob0, size0) = context
4661 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
4662 .await
4663 .unwrap();
4664 assert!(size0 > 0);
4665 blob0.resize(0).await.unwrap();
4666 blob0.sync().await.unwrap();
4667
4668 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
4669 assert!(matches!(result, Err(Error::Corruption(_))));
4670 });
4671 }
4672
4673 #[test_traced]
4679 fn test_single_item_per_blob() {
4680 let executor = deterministic::Runner::default();
4681 executor.start(|context| async move {
4682 let cfg = Config {
4683 partition: "single-item-per-blob".into(),
4684 items_per_blob: NZU64!(1),
4685 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4686 write_buffer: NZUsize!(2048),
4687 replay_buffer: NZUsize!(2048),
4688 };
4689
4690 let mut journal = Journal::init(context.child("first"), cfg.clone())
4692 .await
4693 .expect("failed to initialize journal");
4694
4695 let bounds = journal.bounds();
4697 assert_eq!(bounds.end, 0);
4698 assert!(bounds.is_empty());
4699
4700 let pos;
4702 (journal, pos) = journal
4703 .append(&test_digest(0))
4704 .await
4705 .expect("failed to append");
4706 assert_eq!(pos, 0);
4707 assert_eq!(journal.size(), 1);
4708
4709 journal = journal.sync().await.expect("failed to sync");
4711
4712 let value = journal
4714 .read(journal.size() - 1)
4715 .await
4716 .expect("failed to read");
4717 assert_eq!(value, test_digest(0));
4718
4719 for i in 1..10u64 {
4721 let pos;
4722 (journal, pos) = journal
4723 .append(&test_digest(i))
4724 .await
4725 .expect("failed to append");
4726 assert_eq!(pos, i);
4727 assert_eq!(journal.size(), i + 1);
4728
4729 let value = journal
4731 .read(journal.size() - 1)
4732 .await
4733 .expect("failed to read");
4734 assert_eq!(value, test_digest(i));
4735 }
4736
4737 for i in 0..10u64 {
4739 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4740 }
4741
4742 journal = journal.sync().await.expect("failed to sync");
4743
4744 (journal, _) = journal.prune(5).await.expect("failed to prune");
4747
4748 assert_eq!(journal.size(), 10);
4750
4751 assert_eq!(journal.bounds().start, 5);
4753
4754 let value = journal
4756 .read(journal.size() - 1)
4757 .await
4758 .expect("failed to read");
4759 assert_eq!(value, test_digest(9));
4760
4761 for i in 0..5 {
4763 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
4764 }
4765
4766 for i in 5..10u64 {
4768 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4769 }
4770
4771 for i in 10..15u64 {
4773 let pos;
4774 (journal, pos) = journal
4775 .append(&test_digest(i))
4776 .await
4777 .expect("failed to append");
4778 assert_eq!(pos, i);
4779
4780 let value = journal
4782 .read(journal.size() - 1)
4783 .await
4784 .expect("failed to read");
4785 assert_eq!(value, test_digest(i));
4786 }
4787
4788 journal.sync().await.expect("failed to sync");
4789
4790 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4792 .await
4793 .expect("failed to re-initialize journal");
4794
4795 assert_eq!(journal.size(), 15);
4797
4798 assert_eq!(journal.bounds().start, 5);
4800
4801 let value = journal
4803 .read(journal.size() - 1)
4804 .await
4805 .expect("failed to read");
4806 assert_eq!(value, test_digest(14));
4807
4808 for i in 5..15u64 {
4810 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4811 }
4812
4813 journal.destroy().await.expect("failed to destroy journal");
4814
4815 let mut journal = Journal::init(context.child("third"), cfg.clone())
4818 .await
4819 .expect("failed to initialize journal");
4820
4821 for i in 0..10u64 {
4823 (journal, _) = journal.append(&test_digest(i + 100)).await.unwrap();
4824 }
4825
4826 (journal, _) = journal.prune(5).await.unwrap();
4828 let bounds = journal.bounds();
4829 assert_eq!(bounds.end, 10);
4830 assert_eq!(bounds.start, 5);
4831
4832 journal.sync().await.unwrap();
4834
4835 let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
4837 .await
4838 .expect("failed to re-initialize journal");
4839
4840 let bounds = journal.bounds();
4842 assert_eq!(bounds.end, 10);
4843 assert_eq!(bounds.start, 5);
4844
4845 let value = journal.read(journal.size() - 1).await.unwrap();
4847 assert_eq!(value, test_digest(109));
4848
4849 for i in 5..10u64 {
4851 assert_eq!(journal.read(i).await.unwrap(), test_digest(i + 100));
4852 }
4853
4854 journal.destroy().await.expect("failed to destroy journal");
4855
4856 let mut journal = Journal::init(context.child("storage"), cfg.clone())
4858 .await
4859 .expect("failed to initialize journal");
4860
4861 for i in 0..5u64 {
4862 (journal, _) = journal.append(&test_digest(i + 200)).await.unwrap();
4863 }
4864 journal = journal.sync().await.unwrap();
4865
4866 (journal, _) = journal.prune(5).await.unwrap();
4868 let bounds = journal.bounds();
4869 assert_eq!(bounds.end, 5); assert!(bounds.is_empty()); let result = journal.read(journal.size() - 1).await;
4874 assert!(matches!(result, Err(Error::ItemPruned(4))));
4875
4876 (journal, _) = journal.append(&test_digest(205)).await.unwrap();
4878 assert_eq!(journal.bounds().start, 5);
4879 assert_eq!(
4880 journal.read(journal.size() - 1).await.unwrap(),
4881 test_digest(205)
4882 );
4883
4884 journal.destroy().await.expect("failed to destroy journal");
4885 });
4886 }
4887
4888 #[test_traced]
4889 fn test_fixed_journal_init_at_size_zero() {
4890 let executor = deterministic::Runner::default();
4891 executor.start(|context| async move {
4892 let cfg = test_cfg(&context, NZU64!(5));
4893 let mut journal =
4894 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 0)
4895 .await
4896 .unwrap();
4897
4898 let bounds = journal.bounds();
4899 assert_eq!(bounds.end, 0);
4900 assert!(bounds.is_empty());
4901
4902 let pos;
4904 (journal, pos) = journal.append(&test_digest(100)).await.unwrap();
4905 assert_eq!(pos, 0);
4906 assert_eq!(journal.size(), 1);
4907 assert_eq!(journal.read(0).await.unwrap(), test_digest(100));
4908
4909 journal.destroy().await.unwrap();
4910 });
4911 }
4912
4913 #[test_traced]
4914 fn test_fixed_journal_init_at_max_size_rejected() {
4915 let executor = deterministic::Runner::default();
4916 executor.start(|context| async move {
4917 let mut cfg = test_cfg(&context, NZU64!(1));
4918 cfg.partition = "max-size-rejected".into();
4919
4920 assert!(matches!(
4922 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg, u64::MAX).await,
4923 Err(Error::SizeOverflow)
4924 ));
4925 });
4926 }
4927
4928 #[test_traced]
4929 fn test_fixed_journal_append_size_overflow() {
4930 let executor = deterministic::Runner::default();
4931 executor.start(|context| async move {
4932 let mut cfg = test_cfg(&context, NZU64!(1));
4933 cfg.partition = "append-size-overflow".into();
4934
4935 let mut journal =
4937 Journal::<_, Digest>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
4938 .await
4939 .unwrap();
4940
4941 let appended;
4943 (journal, appended) = journal.append(&test_digest(7)).await.unwrap();
4944 assert_eq!(appended, u64::MAX - 1);
4945 assert_eq!(journal.size(), u64::MAX);
4946
4947 assert!(matches!(
4950 journal.append(&test_digest(8)).await,
4951 Err(Error::SizeOverflow)
4952 ));
4953 });
4954 }
4955
4956 #[test_traced]
4957 fn test_fixed_journal_replay_near_max_size() {
4958 let executor = deterministic::Runner::default();
4959 executor.start(|context| async move {
4960 let mut cfg = test_cfg(&context, NZU64!(10));
4961 cfg.partition = "replay-near-max-size".into();
4962
4963 let mut journal =
4964 Journal::<_, Digest>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
4965 .await
4966 .unwrap();
4967 let expected = test_digest(7);
4968 let appended;
4969 (journal, appended) = journal.append(&expected).await.unwrap();
4970 assert_eq!(appended, u64::MAX - 1);
4971
4972 {
4973 let reader;
4974 (journal, reader) = journal.snapshot().await.unwrap();
4975 let stream = reader
4976 .replay(u64::MAX - 1, NZUsize!(1024), ReadOptions::default())
4977 .await
4978 .unwrap();
4979 pin_mut!(stream);
4980 let (pos, item) = stream.next().await.unwrap().unwrap();
4981 assert_eq!(pos, u64::MAX - 1);
4982 assert_eq!(item, expected);
4983 assert!(stream.next().await.is_none());
4984 }
4985
4986 journal.destroy().await.unwrap();
4987 });
4988 }
4989
4990 #[test_traced]
4991 fn test_fixed_journal_init_at_size_blob_boundary() {
4992 let executor = deterministic::Runner::default();
4993 executor.start(|context| async move {
4994 let cfg = test_cfg(&context, NZU64!(5));
4995
4996 let mut journal =
4998 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10)
4999 .await
5000 .unwrap();
5001
5002 let bounds = journal.bounds();
5003 assert_eq!(bounds.end, 10);
5004 assert!(bounds.is_empty());
5005
5006 let pos;
5008 (journal, pos) = journal.append(&test_digest(1000)).await.unwrap();
5009 assert_eq!(pos, 10);
5010 assert_eq!(journal.size(), 11);
5011 assert_eq!(journal.read(10).await.unwrap(), test_digest(1000));
5012
5013 let pos;
5015 (journal, pos) = journal.append(&test_digest(1001)).await.unwrap();
5016 assert_eq!(pos, 11);
5017 assert_eq!(journal.read(11).await.unwrap(), test_digest(1001));
5018
5019 journal.destroy().await.unwrap();
5020 });
5021 }
5022
5023 #[test_traced]
5024 fn test_fixed_journal_init_at_size_mid_blob() {
5025 let executor = deterministic::Runner::default();
5026 executor.start(|context| async move {
5027 let cfg = test_cfg(&context, NZU64!(5));
5028
5029 let mut journal =
5031 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7)
5032 .await
5033 .unwrap();
5034
5035 let bounds = journal.bounds();
5036 assert_eq!(bounds.end, 7);
5037 assert!(bounds.is_empty());
5039
5040 assert!(matches!(journal.read(5).await, Err(Error::ItemPruned(5))));
5042 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
5043
5044 let pos;
5046 (journal, pos) = journal.append(&test_digest(700)).await.unwrap();
5047 assert_eq!(pos, 7);
5048 assert_eq!(journal.size(), 8);
5049 assert_eq!(journal.read(7).await.unwrap(), test_digest(700));
5050 assert_eq!(journal.bounds().start, 7);
5052
5053 journal.destroy().await.unwrap();
5054 });
5055 }
5056
5057 #[test_traced]
5058 fn test_fixed_journal_append_many_after_mid_blob_start() {
5059 let executor = deterministic::Runner::default();
5060 executor.start(|context| async move {
5061 let cfg = test_cfg(&context, NZU64!(100));
5062 let mut journal =
5063 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 150)
5064 .await
5065 .unwrap();
5066
5067 let items: Vec<_> = (0..100u64).map(|i| test_digest(1500 + i)).collect();
5068 let last;
5069 (journal, last) = journal.append_many(Many::Flat(&items)).await.unwrap();
5070 assert_eq!(last, 249);
5071 assert_eq!(journal.bounds(), 150..250);
5072
5073 for (position, index) in [(150, 0), (199, 49), (200, 50), (249, 99)] {
5074 assert_eq!(
5075 journal.read(position).await.unwrap(),
5076 items[index],
5077 "item at position {position} did not match"
5078 );
5079 }
5080
5081 journal.sync().await.unwrap();
5082
5083 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5084 .await
5085 .unwrap();
5086 assert_eq!(journal.bounds(), 150..250);
5087 for (position, index) in [(150, 0), (199, 49), (200, 50), (249, 99)] {
5088 assert_eq!(
5089 journal.read(position).await.unwrap(),
5090 items[index],
5091 "item at position {position} did not match after reopen"
5092 );
5093 }
5094
5095 journal.destroy().await.unwrap();
5096 });
5097 }
5098
5099 #[test_traced]
5100 fn test_fixed_journal_init_at_size_persistence() {
5101 let executor = deterministic::Runner::default();
5102 executor.start(|context| async move {
5103 let cfg = test_cfg(&context, NZU64!(5));
5104
5105 let mut journal =
5107 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15)
5108 .await
5109 .unwrap();
5110
5111 for i in 0..5u64 {
5113 let pos;
5114 (journal, pos) = journal.append(&test_digest(1500 + i)).await.unwrap();
5115 assert_eq!(pos, 15 + i);
5116 }
5117
5118 assert_eq!(journal.size(), 20);
5119
5120 journal.sync().await.unwrap();
5122
5123 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5124 .await
5125 .unwrap();
5126
5127 let bounds = journal.bounds();
5129 assert_eq!(bounds.end, 20);
5130 assert_eq!(bounds.start, 15);
5131
5132 for i in 0..5u64 {
5134 assert_eq!(journal.read(15 + i).await.unwrap(), test_digest(1500 + i));
5135 }
5136
5137 let pos;
5139 (journal, pos) = journal.append(&test_digest(9999)).await.unwrap();
5140 assert_eq!(pos, 20);
5141 assert_eq!(journal.read(20).await.unwrap(), test_digest(9999));
5142
5143 journal.destroy().await.unwrap();
5144 });
5145 }
5146
5147 #[test_traced]
5148 fn test_fixed_journal_init_at_size_persistence_without_data() {
5149 let executor = deterministic::Runner::default();
5150 executor.start(|context| async move {
5151 let cfg = test_cfg(&context, NZU64!(5));
5152
5153 let journal =
5155 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15)
5156 .await
5157 .unwrap();
5158
5159 let bounds = journal.bounds();
5160 assert_eq!(bounds.end, 15);
5161 assert!(bounds.is_empty());
5162
5163 drop(journal);
5165
5166 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5168 .await
5169 .unwrap();
5170
5171 let bounds = journal.bounds();
5172 assert_eq!(bounds.end, 15);
5173 assert!(bounds.is_empty());
5174
5175 let pos;
5177 (journal, pos) = journal.append(&test_digest(1500)).await.unwrap();
5178 assert_eq!(pos, 15);
5179 assert_eq!(journal.read(15).await.unwrap(), test_digest(1500));
5180
5181 journal.destroy().await.unwrap();
5182 });
5183 }
5184
5185 #[test_traced]
5186 fn test_fixed_journal_init_at_size_large_offset() {
5187 let executor = deterministic::Runner::default();
5188 executor.start(|context| async move {
5189 let cfg = test_cfg(&context, NZU64!(5));
5190
5191 let mut journal =
5193 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 1000)
5194 .await
5195 .unwrap();
5196
5197 let bounds = journal.bounds();
5198 assert_eq!(bounds.end, 1000);
5199 assert!(bounds.is_empty());
5200
5201 let pos;
5203 (journal, pos) = journal.append(&test_digest(100000)).await.unwrap();
5204 assert_eq!(pos, 1000);
5205 assert_eq!(journal.read(1000).await.unwrap(), test_digest(100000));
5206
5207 journal.destroy().await.unwrap();
5208 });
5209 }
5210
5211 #[test_traced]
5212 fn test_fixed_journal_init_at_size_prune_and_append() {
5213 let executor = deterministic::Runner::default();
5214 executor.start(|context| async move {
5215 let cfg = test_cfg(&context, NZU64!(5));
5216
5217 let mut journal =
5219 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 20)
5220 .await
5221 .unwrap();
5222
5223 for i in 0..10u64 {
5225 (journal, _) = journal.append(&test_digest(2000 + i)).await.unwrap();
5226 }
5227
5228 assert_eq!(journal.size(), 30);
5229
5230 (journal, _) = journal.prune(25).await.unwrap();
5232
5233 let bounds = journal.bounds();
5234 assert_eq!(bounds.end, 30);
5235 assert_eq!(bounds.start, 25);
5236
5237 for i in 25..30u64 {
5239 assert_eq!(journal.read(i).await.unwrap(), test_digest(2000 + (i - 20)));
5240 }
5241
5242 let pos;
5244 (journal, pos) = journal.append(&test_digest(3000)).await.unwrap();
5245 assert_eq!(pos, 30);
5246
5247 journal.destroy().await.unwrap();
5248 });
5249 }
5250
5251 #[test_traced]
5252 fn test_fixed_journal_clear_to_size() {
5253 let executor = deterministic::Runner::default();
5254 executor.start(|context| async move {
5255 let cfg = test_cfg(&context, NZU64!(10));
5256 let mut journal = Journal::init(context.child("journal"), cfg.clone())
5257 .await
5258 .expect("failed to initialize journal");
5259
5260 for i in 0..25u64 {
5262 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5263 }
5264 assert_eq!(journal.size(), 25);
5265 journal = journal.sync().await.unwrap();
5266
5267 journal.0 = journal.0.clear_to_size(100).await.unwrap();
5269 assert_eq!(journal.size(), 100);
5270
5271 for i in 0..25 {
5273 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
5274 }
5275
5276 drop(journal);
5278 let mut journal =
5279 Journal::<_, Digest>::init(context.child("journal_after_clear"), cfg.clone())
5280 .await
5281 .expect("failed to re-initialize journal after clear");
5282 assert_eq!(journal.size(), 100);
5283
5284 for i in 100..105u64 {
5286 let pos;
5287 (journal, pos) = journal.append(&test_digest(i)).await.unwrap();
5288 assert_eq!(pos, i);
5289 }
5290 assert_eq!(journal.size(), 105);
5291
5292 for i in 100..105u64 {
5294 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5295 }
5296
5297 journal.sync().await.unwrap();
5299
5300 let journal = Journal::<_, Digest>::init(context.child("journal_reopened"), cfg)
5301 .await
5302 .expect("failed to re-initialize journal");
5303
5304 assert_eq!(journal.size(), 105);
5305 for i in 100..105u64 {
5306 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5307 }
5308
5309 journal.destroy().await.unwrap();
5310 });
5311 }
5312
5313 #[test_traced]
5314 fn test_fixed_journal_clear_to_size_rejects_max() {
5315 let executor = deterministic::Runner::default();
5318 executor.start(|context| async move {
5319 let cfg = test_cfg(&context, NZU64!(10));
5320 let journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
5321 .await
5322 .unwrap();
5323 assert!(matches!(
5324 journal.0.clear_to_size(u64::MAX).await,
5325 Err(Error::SizeOverflow)
5326 ));
5327 let journal = Journal::<_, Digest>::init(context.child("journal_intent"), cfg)
5329 .await
5330 .unwrap();
5331 assert!(matches!(
5332 journal.0.stage_clear_intent(u64::MAX).await,
5333 Err(Error::SizeOverflow)
5334 ));
5335 });
5336 }
5337
5338 #[test_traced]
5339 fn test_fixed_journal_sync_crash_meta_none_boundary_aligned() {
5340 let executor = deterministic::Runner::default();
5342 executor.start(|context| async move {
5343 let cfg = test_cfg(&context, NZU64!(5));
5344 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5345 .await
5346 .unwrap();
5347
5348 for i in 0..5u64 {
5349 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5350 }
5351 journal.commit().await.unwrap();
5352
5353 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5354 .await
5355 .unwrap();
5356 let bounds = journal.bounds();
5357 assert_eq!(bounds.start, 0);
5358 assert_eq!(bounds.end, 5);
5359 journal.destroy().await.unwrap();
5360 });
5361 }
5362
5363 #[test_traced]
5364 fn test_fixed_journal_missing_metadata_with_short_blob_is_corruption() {
5365 let executor = deterministic::Runner::default();
5368 executor.start(|context| async move {
5369 let cfg = test_cfg(&context, NZU64!(5));
5370 let mut journal =
5371 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
5372 .await
5373 .unwrap();
5374 for i in 0..3u64 {
5375 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5376 }
5377 let mut journal = journal.sync().await.unwrap();
5378
5379 journal.0.checkpoint.clear();
5381 journal.0.checkpoint = journal.0.checkpoint.sync().await.unwrap();
5382 drop(journal);
5383
5384 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
5385 assert!(matches!(result, Err(Error::Corruption(_))));
5386 });
5387 }
5388
5389 #[test_traced]
5390 fn test_fixed_journal_sync_crash_meta_mid_boundary_unchanged() {
5391 let executor = deterministic::Runner::default();
5393 executor.start(|context| async move {
5394 let cfg = test_cfg(&context, NZU64!(5));
5395 let mut journal =
5396 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
5397 .await
5398 .unwrap();
5399 for i in 0..3u64 {
5400 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5401 }
5402 journal.commit().await.unwrap();
5403
5404 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5405 .await
5406 .unwrap();
5407 let bounds = journal.bounds();
5408 assert_eq!(bounds.start, 7);
5409 assert_eq!(bounds.end, 10);
5410 journal.destroy().await.unwrap();
5411 });
5412 }
5413 #[test_traced]
5414 fn test_fixed_journal_sync_crash_meta_mid_to_aligned_becomes_stale() {
5415 let executor = deterministic::Runner::default();
5417 executor.start(|context| async move {
5418 let cfg = test_cfg(&context, NZU64!(5));
5419 let mut journal =
5420 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
5421 .await
5422 .unwrap();
5423 for i in 0..10u64 {
5424 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5425 }
5426 assert_eq!(journal.size(), 17);
5427 (journal, _) = journal.prune(10).await.unwrap();
5428
5429 journal.commit().await.unwrap();
5430
5431 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5432 .await
5433 .unwrap();
5434 let bounds = journal.bounds();
5435 assert_eq!(bounds.start, 10);
5436 assert_eq!(bounds.end, 17);
5437 journal.destroy().await.unwrap();
5438 });
5439 }
5440
5441 #[test_traced]
5442 fn test_fixed_journal_prune_does_not_move_boundary_backwards() {
5443 let executor = deterministic::Runner::default();
5446 executor.start(|context| async move {
5447 let cfg = test_cfg(&context, NZU64!(5));
5448 let mut journal =
5450 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
5451 .await
5452 .unwrap();
5453 for i in 0..5u64 {
5455 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5456 }
5457 let (journal, _) = journal.prune(5).await.unwrap();
5459 assert_eq!(journal.bounds().start, 7);
5460 journal.destroy().await.unwrap();
5461 });
5462 }
5463
5464 #[test_traced]
5467 fn test_fixed_journal_prune_durability_survives_crash() {
5468 let executor = deterministic::Runner::default();
5469 let (_, checkpoint) = executor.start_and_recover(|context| async move {
5470 let cfg = test_cfg(&context, NZU64!(3));
5471 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg)
5472 .await
5473 .unwrap();
5474
5475 for i in 0..8u64 {
5479 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5480 }
5481 let (journal, pruned) = journal.prune(3).await.unwrap();
5482 assert!(pruned);
5483 drop(journal);
5484 });
5485
5486 deterministic::Runner::from(checkpoint).start(|context| async move {
5487 let cfg = test_cfg(&context, NZU64!(3));
5488 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg)
5489 .await
5490 .unwrap();
5491 assert_eq!(
5492 journal.bounds(),
5493 3..8,
5494 "pruned journal lost acknowledged items"
5495 );
5496 for i in 3..8u64 {
5497 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5498 }
5499 journal.destroy().await.unwrap();
5500 });
5501 }
5502
5503 #[test_traced]
5506 fn test_fixed_journal_commit_after_prune() {
5507 let executor = deterministic::Runner::default();
5508 executor.start(|context| async move {
5509 let cfg = test_cfg(&context, NZU64!(5));
5510 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
5511 .await
5512 .unwrap();
5513
5514 for i in 0..12 {
5515 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5516 }
5517
5518 let (mut journal, _) = journal.prune(5).await.unwrap();
5519 journal = journal
5520 .commit()
5521 .await
5522 .expect("commit should not try to sync pruned blobs");
5523 assert_eq!(journal.bounds(), 5..12);
5524 journal.destroy().await.unwrap();
5525 });
5526 }
5527
5528 #[test_traced]
5529 fn test_fixed_journal_replay_after_init_at_size_spanning_blobs() {
5530 let executor = deterministic::Runner::default();
5533 executor.start(|context| async move {
5534 let cfg = test_cfg(&context, NZU64!(5));
5535
5536 let mut journal =
5539 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7)
5540 .await
5541 .unwrap();
5542
5543 for i in 0..13u64 {
5545 let pos;
5546 (journal, pos) = journal.append(&test_digest(100 + i)).await.unwrap();
5547 assert_eq!(pos, 7 + i);
5548 }
5549 assert_eq!(journal.size(), 20);
5550 journal = journal.sync().await.unwrap();
5551
5552 {
5554 let reader;
5555 (journal, reader) = journal.snapshot().await.unwrap();
5556 let stream = reader
5557 .replay(7, NZUsize!(1024), ReadOptions::default())
5558 .await
5559 .expect("failed to replay");
5560 pin_mut!(stream);
5561 let mut items: Vec<(u64, Digest)> = Vec::new();
5562 while let Some(result) = stream.next().await {
5563 items.push(result.expect("replay item failed"));
5564 }
5565
5566 assert_eq!(items.len(), 13);
5568 for (i, (pos, item)) in items.iter().enumerate() {
5569 assert_eq!(*pos, 7 + i as u64);
5570 assert_eq!(*item, test_digest(100 + i as u64));
5571 }
5572 }
5573
5574 {
5576 let reader;
5577 (journal, reader) = journal.snapshot().await.unwrap();
5578 let stream = reader
5579 .replay(12, NZUsize!(1024), ReadOptions::default())
5580 .await
5581 .expect("failed to replay from mid-stream");
5582 pin_mut!(stream);
5583 let mut items: Vec<(u64, Digest)> = Vec::new();
5584 while let Some(result) = stream.next().await {
5585 items.push(result.expect("replay item failed"));
5586 }
5587
5588 assert_eq!(items.len(), 8);
5590 for (i, (pos, item)) in items.iter().enumerate() {
5591 assert_eq!(*pos, 12 + i as u64);
5592 assert_eq!(*item, test_digest(100 + 5 + i as u64));
5593 }
5594 }
5595
5596 journal.destroy().await.unwrap();
5597 });
5598 }
5599
5600 #[test_traced]
5601 fn test_fixed_journal_rewind_error_before_bounds_start() {
5602 let executor = deterministic::Runner::default();
5604 executor.start(|context| async move {
5605 let cfg = test_cfg(&context, NZU64!(5));
5606
5607 let mut journal =
5608 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10)
5609 .await
5610 .unwrap();
5611
5612 for i in 0..3u64 {
5614 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5615 }
5616 assert_eq!(journal.size(), 13);
5617
5618 journal = journal.rewind(11).await.unwrap();
5620 assert_eq!(journal.size(), 11);
5621
5622 journal = journal.rewind(10).await.unwrap();
5624 assert_eq!(journal.size(), 10);
5625
5626 let result = journal.rewind(9).await;
5628 assert!(matches!(result, Err(Error::ItemPruned(9))));
5629 });
5630 }
5631
5632 #[test_traced]
5633 fn test_fixed_journal_init_at_size_crash_scenarios() {
5634 let executor = deterministic::Runner::default();
5635 executor.start(|context| async move {
5636 let cfg = test_cfg(&context, NZU64!(5));
5637
5638 let mut journal =
5640 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
5641 .await
5642 .unwrap();
5643 for i in 0..5u64 {
5644 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5645 }
5646 let journal = journal.sync().await.unwrap();
5647 drop(journal);
5648
5649 let blob_part = blob_partition(&cfg);
5652 let mut checkpoint = Checkpoint::open(context.child("intent_meta"), &cfg.partition)
5653 .await
5654 .unwrap();
5655 checkpoint.set_clear_target(12);
5656 let checkpoint = checkpoint.sync().await.unwrap();
5657 drop(checkpoint);
5658 context.remove(&blob_part, None).await.unwrap();
5659
5660 let journal = Journal::<_, Digest>::init(
5662 context.child("crash").with_attribute("index", 1),
5663 cfg.clone(),
5664 )
5665 .await
5666 .expect("init failed after clear crash");
5667 let bounds = journal.bounds();
5668 assert_eq!(bounds.end, 12);
5669 assert_eq!(bounds.start, 12);
5670 drop(journal);
5671
5672 let mut checkpoint = Checkpoint::open(context.child("restore_meta"), &cfg.partition)
5674 .await
5675 .unwrap();
5676 checkpoint.set_boundary_hint(7);
5677 checkpoint.set_clear_target(2);
5678 let checkpoint = checkpoint.sync().await.unwrap();
5679 drop(checkpoint);
5680
5681 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
5684 blob.sync().await.unwrap(); drop(blob);
5686
5687 let journal = Journal::<_, Digest>::init(
5689 context.child("crash").with_attribute("index", 2),
5690 cfg.clone(),
5691 )
5692 .await
5693 .expect("init failed after create crash");
5694
5695 let bounds = journal.bounds();
5696 assert_eq!(bounds.start, 2);
5697 assert_eq!(bounds.end, 2);
5698 journal.destroy().await.unwrap();
5699 });
5700 }
5701
5702 #[test_traced]
5703 fn test_fixed_journal_clear_to_size_crash_scenarios() {
5704 let executor = deterministic::Runner::default();
5705 executor.start(|context| async move {
5706 let cfg = test_cfg(&context, NZU64!(5));
5707
5708 let journal =
5711 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 12)
5712 .await
5713 .unwrap();
5714 let journal = journal.sync().await.unwrap();
5715 drop(journal);
5716
5717 let blob_part = blob_partition(&cfg);
5721 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
5722 .await
5723 .unwrap();
5724 checkpoint.set_clear_target(2);
5725 let checkpoint = checkpoint.sync().await.unwrap();
5726 drop(checkpoint);
5727
5728 context.remove(&blob_part, None).await.unwrap();
5729
5730 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
5731 blob.sync().await.unwrap();
5732
5733 let journal = Journal::<_, Digest>::init(context.child("crash_clear"), cfg.clone())
5734 .await
5735 .expect("init failed after clear_to_size crash");
5736
5737 let bounds = journal.bounds();
5738 assert_eq!(bounds.start, 2);
5739 assert_eq!(bounds.end, 2);
5740 journal.destroy().await.unwrap();
5741 });
5742 }
5743
5744 #[test_traced]
5745 fn test_fixed_journal_clear_to_size_crash_after_intent_before_blobs() {
5746 let executor = deterministic::Runner::default();
5747 executor.start(|context| async move {
5748 let cfg = test_cfg(&context, NZU64!(5));
5749 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5750 .await
5751 .unwrap();
5752 for i in 0..12u64 {
5753 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5754 }
5755 journal = journal.sync().await.unwrap();
5756
5757 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
5758 .await
5759 .unwrap();
5760 checkpoint.set_clear_target(100);
5761 let checkpoint = checkpoint.sync().await.unwrap();
5762 drop(checkpoint);
5763 drop(journal);
5764
5765 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5766 .await
5767 .expect("init failed after clear intent crash");
5768 assert_eq!(journal.bounds(), 100..100);
5769 let pos;
5770 (journal, pos) = journal.append(&test_digest(100)).await.unwrap();
5771 assert_eq!(pos, 100);
5772 journal.destroy().await.unwrap();
5773 });
5774 }
5775
5776 #[test_traced]
5777 fn test_fixed_journal_clear_intent_skips_corrupt_stale_blobs() {
5778 let executor = deterministic::Runner::default();
5779 executor.start(|context| async move {
5780 let cfg = test_cfg(&context, NZU64!(5));
5781 let blob_part = blob_partition(&cfg);
5782 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
5783 .await
5784 .unwrap();
5785 checkpoint.set_clear_target(12);
5786 let checkpoint = checkpoint.sync().await.unwrap();
5787 drop(checkpoint);
5788
5789 let (blob, _) = context.open(&blob_part, b"not-u64").await.unwrap();
5792 blob.write_at(0, vec![1, 2, 3], WriteOptions::SYNC)
5793 .await
5794 .unwrap();
5795 drop(blob);
5796
5797 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg.clone())
5798 .await
5799 .expect("clear intent should discard stale corrupt blobs before blob parsing");
5800 assert_eq!(journal.bounds(), 12..12);
5801 assert_eq!(journal.0.recovery_watermark(), 12);
5802 journal.destroy().await.unwrap();
5803 });
5804 }
5805
5806 #[test_traced]
5807 fn test_fixed_journal_clear_to_size_crash_after_mid_blob_intent_with_old_blobs_present() {
5808 let executor = deterministic::Runner::default();
5809 executor.start(|context| async move {
5810 let cfg = test_cfg(&context, NZU64!(10));
5811 let mut journal =
5812 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 10)
5813 .await
5814 .unwrap();
5815
5816 for i in 0..6u64 {
5817 let pos;
5818 (journal, pos) = journal.append(&test_digest(i)).await.unwrap();
5819 assert_eq!(pos, 10 + i);
5820 }
5821 journal = journal.sync().await.unwrap();
5822
5823 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
5824 .await
5825 .unwrap();
5826 checkpoint.set_clear_target(15);
5827 let checkpoint = checkpoint.sync().await.unwrap();
5828 drop(checkpoint);
5829 drop(journal);
5830
5831 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5832 .await
5833 .expect("init failed after mid-blob clear intent crash");
5834 assert_eq!(journal.bounds(), 15..15);
5835 drop(journal);
5836
5837 let mut journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
5838 .await
5839 .expect("init failed after completing mid-blob clear intent");
5840 assert_eq!(journal.bounds(), 15..15);
5841 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(14))));
5842 let pos;
5843 (journal, pos) = journal.append(&test_digest(100)).await.unwrap();
5844 assert_eq!(pos, 15);
5845 assert_eq!(journal.read(15).await.unwrap(), test_digest(100));
5846 journal.destroy().await.unwrap();
5847 });
5848 }
5849
5850 #[test_traced]
5851 fn test_fixed_journal_rejects_watermark_with_aligned_empty_tail() {
5852 let executor = deterministic::Runner::default();
5854 executor.start(|context| async move {
5855 let cfg = test_cfg(&context, NZU64!(5));
5856
5857 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5858 .await
5859 .unwrap();
5860 for i in 0..10u64 {
5861 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5862 }
5863 let journal = journal.sync().await.unwrap();
5864 drop(journal);
5865
5866 let blob_part = blob_partition(&cfg);
5869 context.remove(&blob_part, None).await.unwrap();
5870 let (blob, _) = context.open(&blob_part, &1u64.to_be_bytes()).await.unwrap();
5871 blob.sync().await.unwrap();
5872
5873 let result = Journal::<_, Digest>::init(context.child("crash"), cfg.clone()).await;
5874 assert!(matches!(result, Err(Error::Corruption(_))));
5875 });
5876 }
5877
5878 #[test_traced]
5879 fn test_fixed_journal_rejects_far_watermark_with_aligned_empty_tail() {
5880 let executor = deterministic::Runner::default();
5882 executor.start(|context| async move {
5883 let cfg = test_cfg(&context, NZU64!(5));
5884
5885 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5886 .await
5887 .unwrap();
5888 for i in 0..10u64 {
5889 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5890 }
5891 let journal = journal.sync().await.unwrap();
5892 drop(journal);
5893
5894 let blob_part = blob_partition(&cfg);
5897 context.remove(&blob_part, None).await.unwrap();
5898 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
5899 blob.sync().await.unwrap();
5900
5901 let result = Journal::<_, Digest>::init(context.child("crash"), cfg.clone()).await;
5902 assert!(matches!(result, Err(Error::Corruption(_))));
5903 });
5904 }
5905
5906 #[test_traced]
5907 fn test_read_many_empty() {
5908 let executor = deterministic::Runner::default();
5909 executor.start(|context| async move {
5910 let cfg = test_cfg(&context, NZU64!(10));
5911 let journal = Journal::<_, Digest>::init(context.child("j"), cfg)
5912 .await
5913 .unwrap();
5914
5915 let (journal, reader) = journal.snapshot().await.unwrap();
5916 let items = reader.read_many(&[]).await.unwrap();
5917 assert!(items.is_empty());
5918
5919 journal.destroy().await.unwrap();
5920 });
5921 }
5922
5923 #[test_traced]
5924 fn test_read_many_single_blob() {
5925 let executor = deterministic::Runner::default();
5927 executor.start(|context| async move {
5928 let cfg = test_cfg(&context, NZU64!(10));
5929 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
5930
5931 for i in 0..5u64 {
5932 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5933 }
5934 assert_eq!(journal.size(), 5);
5935
5936 let (journal, reader) = journal.snapshot().await.unwrap();
5937 let items = reader.read_many(&[0, 2, 4]).await.unwrap();
5938 assert_eq!(items, vec![test_digest(0), test_digest(2), test_digest(4)]);
5939
5940 journal.destroy().await.unwrap();
5941 });
5942 }
5943
5944 #[test_traced]
5945 #[should_panic(expected = "positions must be strictly increasing")]
5946 fn test_read_many_rejects_unsorted_positions() {
5947 let executor = deterministic::Runner::default();
5948 executor.start(|context| async move {
5949 let cfg = test_cfg(&context, NZU64!(10));
5950 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
5951 for i in 0..5u64 {
5952 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5953 }
5954
5955 let (_journal, reader) = journal.snapshot().await.unwrap();
5956 let _ = reader.read_many(&[2, 1]).await;
5957 });
5958 }
5959
5960 #[test_traced]
5961 #[should_panic(expected = "positions must be strictly increasing")]
5962 fn test_read_many_rejects_duplicate_positions() {
5963 let executor = deterministic::Runner::default();
5965 executor.start(|context| async move {
5966 let cfg = test_cfg(&context, NZU64!(10));
5967 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
5968 for i in 0..5u64 {
5969 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5970 }
5971
5972 let (_journal, reader) = journal.snapshot().await.unwrap();
5973 let _ = reader.read_many(&[1, 1]).await;
5974 });
5975 }
5976
5977 #[test_traced]
5978 fn test_read_many_across_blobs() {
5979 let executor = deterministic::Runner::default();
5981 executor.start(|context| async move {
5982 let cfg = test_cfg(&context, NZU64!(3));
5983 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
5984
5985 for i in 0..9u64 {
5986 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5987 }
5988 assert_eq!(journal.size(), 9);
5989 let (journal, reader) = journal.snapshot().await.unwrap();
5992 let items = reader.read_many(&[1, 4, 7]).await.unwrap();
5993 assert_eq!(items, vec![test_digest(1), test_digest(4), test_digest(7)]);
5994
5995 journal.destroy().await.unwrap();
5996 });
5997 }
5998
5999 #[test_traced]
6000 fn test_read_many_after_prune() {
6001 let executor = deterministic::Runner::default();
6003 executor.start(|context| async move {
6004 let cfg = test_cfg(&context, NZU64!(3));
6005 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
6006
6007 for i in 0..9u64 {
6008 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6009 }
6010 assert_eq!(journal.size(), 9);
6011 journal = journal.sync().await.unwrap();
6012
6013 (journal, _) = journal.prune(3).await.unwrap();
6015 assert_eq!(journal.bounds(), 3..9);
6016
6017 let (journal, reader) = journal.snapshot().await.unwrap();
6018 let items = reader.read_many(&[3, 5, 8]).await.unwrap();
6019 assert_eq!(items, vec![test_digest(3), test_digest(5), test_digest(8)]);
6020
6021 let (journal, reader) = journal.snapshot().await.unwrap();
6023 let err = reader.read_many(&[1]).await.unwrap_err();
6024 assert!(matches!(err, Error::ItemPruned(1)));
6025
6026 journal.destroy().await.unwrap();
6027 });
6028 }
6029
6030 #[test_traced]
6031 fn test_read_many_out_of_range() {
6032 let executor = deterministic::Runner::default();
6033 executor.start(|context| async move {
6034 let cfg = test_cfg(&context, NZU64!(10));
6035 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
6036
6037 for i in 0..3u64 {
6038 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6039 }
6040 assert_eq!(journal.size(), 3);
6041
6042 let (journal, reader) = journal.snapshot().await.unwrap();
6043 let err = reader.read_many(&[0, 5]).await.unwrap_err();
6044 assert!(matches!(err, Error::ItemOutOfRange(5)));
6045
6046 journal.destroy().await.unwrap();
6047 });
6048 }
6049
6050 #[test_traced]
6051 fn test_read_many_matches_read() {
6052 let executor = deterministic::Runner::default();
6054 executor.start(|context| async move {
6055 let cfg = test_cfg(&context, NZU64!(4));
6056 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
6057
6058 for i in 0..20u64 {
6059 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6060 }
6061 assert_eq!(journal.size(), 20);
6062 journal = journal.sync().await.unwrap();
6063
6064 let positions: Vec<u64> = (0..20).collect();
6065 let reader;
6066 (journal, reader) = journal.snapshot().await.unwrap();
6067 let batch = reader.read_many(&positions).await.unwrap();
6068
6069 for &pos in &positions {
6070 let single = reader.read(pos).await.unwrap();
6071 assert_eq!(batch[pos as usize], single);
6072 }
6073 drop(reader);
6074
6075 journal.destroy().await.unwrap();
6076 });
6077 }
6078
6079 #[test_traced]
6080 fn test_try_read_many_sync_matches_read_many() {
6081 let executor = deterministic::Runner::default();
6084 executor.start(|context| async move {
6085 let cfg = test_cfg(&context, NZU64!(4));
6086 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
6087
6088 for i in 0..20u64 {
6089 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6090 }
6091 journal = journal.sync().await.unwrap();
6092
6093 let positions: Vec<u64> = (0..20).collect();
6094 let reader;
6095 (journal, reader) = journal.snapshot().await.unwrap();
6096 let expected = reader.read_many(&positions).await.unwrap();
6097
6098 let served = reader.try_read_many_sync(&positions);
6101 assert_eq!(served.len(), positions.len());
6102 for (item, expected) in served.iter().zip(&expected) {
6103 if let Some(item) = item {
6104 assert_eq!(item, expected);
6105 }
6106 }
6107
6108 let tail: Vec<u64> = (16..20).collect();
6111 reader.read_many(&tail).await.unwrap();
6112 let served = reader.try_read_many_sync(&tail);
6113 for (item, pos) in served.iter().zip(&tail) {
6114 assert_eq!(
6115 item.as_ref().expect("warmed position is served"),
6116 &expected[*pos as usize]
6117 );
6118 }
6119
6120 assert!(reader.try_read_many_sync(&[0])[0].is_none());
6122
6123 let served = reader.try_read_many_sync(&[19, 20]);
6126 assert!(served[0].is_some());
6127 assert!(served[1].is_none());
6128 drop(served);
6129 drop(reader);
6130
6131 journal = journal.rewind(18).await.unwrap();
6135 let reader;
6136 (journal, reader) = journal.snapshot().await.unwrap();
6137 reader.read_many(&[17]).await.unwrap();
6138 let served = reader.try_read_many_sync(&[17, 18]);
6139 assert!(served[0].is_some());
6140 assert!(served[1].is_none());
6141 drop(served);
6142 drop(reader);
6143
6144 (journal, _) = journal.prune(8).await.unwrap();
6147 let reader;
6148 (journal, reader) = journal.snapshot().await.unwrap();
6149 reader.read_many(&[9]).await.unwrap();
6150 let served = reader.try_read_many_sync(&[3, 9]);
6151 assert!(served[0].is_none());
6152 assert!(served[1].is_some());
6153 drop(served);
6154 drop(reader);
6155
6156 journal.destroy().await.unwrap();
6157 });
6158 }
6159
6160 #[test_traced]
6161 fn test_probe_then_read_many_matches_read_many() {
6162 let executor = deterministic::Runner::default();
6165 executor.start(|context| async move {
6166 let cfg = test_cfg(&context, NZU64!(4));
6167 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
6168
6169 for i in 0..20u64 {
6170 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6171 }
6172 journal = journal.sync().await.unwrap();
6173
6174 let positions: Vec<u64> = (0..20).collect();
6175 let reader;
6176 (journal, reader) = journal.snapshot().await.unwrap();
6177 let expected: Vec<_> = (0..20).map(test_digest).collect();
6178 for _ in 0..2 {
6179 let mut served = reader.try_read_many_sync(&positions);
6180 let misses: Vec<u64> = positions
6181 .iter()
6182 .zip(&served)
6183 .filter_map(|(&pos, item)| item.is_none().then_some(pos))
6184 .collect();
6185 let mut fetched = reader.read_many(&misses).await.unwrap().into_iter();
6186 for item in served.iter_mut().filter(|item| item.is_none()) {
6187 *item = fetched.next();
6188 }
6189 let completed: Vec<_> = served.into_iter().map(Option::unwrap).collect();
6190 assert_eq!(completed, expected);
6191 }
6192 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
6193 drop(reader);
6194
6195 journal.destroy().await.unwrap();
6196 });
6197 }
6198
6199 #[test_traced]
6200 fn test_fixed_journal_metrics() {
6201 let executor = deterministic::Runner::default();
6202 executor.start(|context| async move {
6203 let cfg = test_cfg(&context, NZU64!(2));
6204 let mut journal =
6205 Journal::<_, Digest>::init(context.child("fixed_metrics"), cfg.clone())
6206 .await
6207 .unwrap();
6208
6209 let items: Vec<_> = (0..5).map(test_digest).collect();
6210 (journal, _) = journal.append_many(Many::Flat(&items)).await.unwrap();
6211 (journal, _) = journal.append(&test_digest(5)).await.unwrap();
6212 journal = journal.commit().await.unwrap();
6213 journal = journal.sync().await.unwrap();
6214 let handle;
6215 (journal, handle) = journal.start_sync().await.unwrap();
6216 handle.await.unwrap();
6217 let (journal, reader) = journal.snapshot().await.unwrap();
6218 reader.read(0).await.unwrap();
6219 let (journal, reader) = journal.snapshot().await.unwrap();
6220 reader.try_read_sync(0).unwrap();
6221 let (journal, reader) = journal.snapshot().await.unwrap();
6222 reader.read_many(&[1, 2, 4]).await.unwrap();
6223 let (journal, _) = journal.prune(2).await.unwrap();
6224 let journal = journal.rewind(4).await.unwrap();
6225
6226 let buffer = context.encode();
6227 for expected in [
6228 "fixed_metrics_size 4",
6229 "fixed_metrics_pruning_boundary 2",
6230 "fixed_metrics_retained 2",
6231 "fixed_metrics_tail_items 2",
6232 "fixed_metrics_append_calls_total 1",
6233 "fixed_metrics_append_many_calls_total 1",
6234 "fixed_metrics_read_calls_total 1",
6235 "fixed_metrics_read_many_calls_total 1",
6236 "fixed_metrics_items_read_total 5",
6237 "fixed_metrics_start_sync_calls_total 1",
6238 "fixed_metrics_commit_calls_total 1",
6239 "fixed_metrics_sync_calls_total 1",
6240 "fixed_metrics_append_duration_count 1",
6241 "fixed_metrics_append_many_duration_count 1",
6242 "fixed_metrics_read_duration_count 0",
6243 "fixed_metrics_read_many_duration_count 1",
6244 "fixed_metrics_commit_duration_count 1",
6245 "fixed_metrics_sync_duration_count 1",
6246 "fixed_metrics_cache_hits_total",
6247 "fixed_metrics_cache_misses_total",
6248 "fixed_metrics_blobs_tracked",
6249 ] {
6250 assert!(buffer.contains(expected), "{expected}\n{buffer}");
6251 }
6252
6253 journal.destroy().await.unwrap();
6254 });
6255 }
6256 #[test_traced]
6258 fn test_snapshot_frozen_across_roll() {
6259 let executor = deterministic::Runner::default();
6260 executor.start(|context| async move {
6261 let cfg = test_cfg(&context, NZU64!(5));
6262 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
6263 .await
6264 .unwrap();
6265 for i in 0..7u64 {
6266 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6267 }
6268
6269 let snapshot;
6270 (journal, snapshot) = journal.snapshot().await.unwrap();
6271 assert_eq!(snapshot.bounds(), 0..7);
6272
6273 for i in 7..23u64 {
6276 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6277 }
6278 assert_eq!(snapshot.bounds(), 0..7);
6279 for i in 0..7u64 {
6280 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
6281 }
6282 assert!(matches!(
6283 snapshot.read(7).await,
6284 Err(Error::ItemOutOfRange(7))
6285 ));
6286
6287 let fresh;
6288 (journal, fresh) = journal.snapshot().await.unwrap();
6289 assert_eq!(fresh.bounds(), 0..23);
6290 assert_eq!(fresh.read(22).await.unwrap(), test_digest(22));
6291
6292 drop(snapshot);
6293 drop(fresh);
6294 journal.destroy().await.unwrap();
6295 });
6296 }
6297
6298 #[test_traced]
6301 fn test_prune_under_snapshot() {
6302 let executor = deterministic::Runner::default();
6303 executor.start(|context| async move {
6304 let cfg = test_cfg(&context, NZU64!(5));
6305 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
6306 .await
6307 .unwrap();
6308 for i in 0..17u64 {
6309 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6310 }
6311 journal = journal.sync().await.unwrap();
6312
6313 let snapshot;
6314 (journal, snapshot) = journal.snapshot().await.unwrap();
6315 let pruned;
6316 (journal, pruned) = journal.prune(12).await.unwrap();
6317 assert!(pruned);
6318
6319 assert_eq!(snapshot.bounds(), 0..17);
6321 for i in 0..17u64 {
6322 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
6323 }
6324
6325 let fresh;
6326 (journal, fresh) = journal.snapshot().await.unwrap();
6327 assert_eq!(fresh.bounds(), 10..17);
6328 assert!(matches!(fresh.read(3).await, Err(Error::ItemPruned(3))));
6329
6330 drop(snapshot);
6331 drop(fresh);
6332 journal.destroy().await.unwrap();
6333 });
6334 }
6335
6336 #[test_traced]
6339 fn test_snapshots_readable_during_concurrent_appends() {
6340 let executor = deterministic::Runner::default();
6341 executor.start(|context| async move {
6342 let cfg = test_cfg(&context, NZU64!(5));
6343 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
6344 .await
6345 .unwrap();
6346
6347 let (mut tx, mut rx) =
6348 futures::channel::mpsc::channel::<Reader<'static, Context, Digest>>(8);
6349 let validator = context.child("validator").spawn(|_| async move {
6350 let mut validated = 0usize;
6351 while let Some(snapshot) = rx.next().await {
6352 let bounds = snapshot.bounds();
6353 for i in bounds.clone() {
6354 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
6355 }
6356 validated += (bounds.end - bounds.start) as usize;
6357 }
6358 validated
6359 });
6360
6361 for i in 0..40u64 {
6362 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6363 if i % 7 == 0 {
6364 let snapshot;
6365 (journal, snapshot) = journal.snapshot().await.unwrap();
6366 if tx.try_send(snapshot).is_err() {
6367 break;
6368 }
6369 }
6370 }
6371 drop(tx);
6372 assert!(validator.await.unwrap() > 0);
6373
6374 journal.destroy().await.unwrap();
6375 });
6376 }
6377
6378 #[test_traced]
6381 fn test_replay_from_stale_snapshot() {
6382 let executor = deterministic::Runner::default();
6383 executor.start(|context| async move {
6384 let cfg = test_cfg(&context, NZU64!(5));
6385 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
6386 .await
6387 .unwrap();
6388 for i in 0..7u64 {
6389 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6390 }
6391
6392 let snapshot;
6394 (journal, snapshot) = journal.snapshot().await.unwrap();
6395 assert_eq!(snapshot.bounds(), 0..7);
6396
6397 for i in 7..23u64 {
6399 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6400 }
6401 let pruned;
6402 (journal, pruned) = journal.prune(12).await.unwrap();
6403 assert!(pruned);
6404
6405 {
6406 let stream = snapshot
6407 .replay(0, NZUsize!(1024), ReadOptions::default())
6408 .await
6409 .unwrap();
6410 pin_mut!(stream);
6411 let mut expected = 0u64;
6412 while let Some(result) = stream.next().await {
6413 let (pos, item) = result.unwrap();
6414 assert_eq!(pos, expected);
6415 assert_eq!(item, test_digest(pos));
6416 expected += 1;
6417 }
6418 assert_eq!(expected, 7);
6419 }
6420
6421 drop(snapshot);
6422 journal.destroy().await.unwrap();
6423 });
6424 }
6425
6426 #[test_traced]
6427 fn test_read_many_sparse_sections_and_hit_accounting() {
6428 let executor = deterministic::Runner::default();
6432 executor.start(|context| async move {
6433 let mut cfg = test_cfg(&context, NZU64!(8));
6434 cfg.page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(16));
6437 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
6438
6439 for i in 0..50u64 {
6440 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6441 }
6442 journal = journal.sync().await.unwrap();
6443 (journal, _) = journal.prune(11).await.unwrap();
6445
6446 let reader;
6447 (journal, reader) = journal.snapshot().await.unwrap();
6448
6449 let positions: Vec<u64> = vec![11, 12, 19, 20, 23, 31, 40, 47, 49];
6454 let expected_hits = positions
6455 .iter()
6456 .filter(|&&pos| reader.try_read_sync(pos).is_some())
6457 .count() as u64;
6458 let before = context.encode();
6459 let batch = reader.read_many(&positions).await.unwrap();
6460 let after = context.encode();
6461 assert_eq!(batch.len(), positions.len());
6462 assert_eq!(
6463 counter(&after, "cache_hits") - counter(&before, "cache_hits"),
6464 expected_hits,
6465 "batch read hit count should match the cached subset"
6466 );
6467 assert_eq!(
6468 counter(&after, "cache_misses") - counter(&before, "cache_misses"),
6469 positions.len() as u64 - expected_hits,
6470 "batch read miss count should cover the rest"
6471 );
6472 for (i, &pos) in positions.iter().enumerate() {
6473 let single = reader.read(pos).await.unwrap();
6474 assert_eq!(batch[i], single);
6475 assert_eq!(batch[i], test_digest(pos));
6476 }
6477
6478 let all: Vec<u64> = (11..50).collect();
6480 let batch = reader.read_many(&all).await.unwrap();
6481 for (i, &pos) in all.iter().enumerate() {
6482 assert_eq!(batch[i], reader.read(pos).await.unwrap());
6483 }
6484 drop(reader);
6485
6486 journal.destroy().await.unwrap();
6487 });
6488 }
6489
6490 #[test_traced]
6491 fn test_read_many_cold_blob_groups() {
6492 let executor = deterministic::Runner::default();
6497 executor.start(|context| async move {
6498 let mut cfg = test_cfg(&context, NZU64!(8));
6499 cfg.page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(32));
6500 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6501 .await
6502 .unwrap();
6503 for i in 0..40u64 {
6504 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6505 }
6506 let journal = journal.sync().await.unwrap();
6507 drop(journal);
6508
6509 cfg.page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(32));
6513 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
6514 .await
6515 .unwrap();
6516 let reader;
6517 (journal, reader) = journal.snapshot().await.unwrap();
6518 let positions = [1, 5, 10, 14, 17, 22, 25, 30];
6519 for pos in positions {
6520 assert!(reader.try_read_sync(pos).is_none(), "position {pos}");
6521 }
6522
6523 let before = context.encode();
6525 let batch = reader.read_many(&positions).await.unwrap();
6526 let after = context.encode();
6527 assert_eq!(
6528 counter(&after, "second_cache_misses") - counter(&before, "second_cache_misses"),
6529 positions.len() as u64
6530 );
6531 assert_eq!(
6532 counter(&after, "second_cache_hits"),
6533 counter(&before, "second_cache_hits")
6534 );
6535 for (i, &pos) in positions.iter().enumerate() {
6536 assert_eq!(batch[i], test_digest(pos), "position {pos}");
6537 }
6538
6539 let before = context.encode();
6542 let batch = reader.read_many(&positions).await.unwrap();
6543 let after = context.encode();
6544 assert_eq!(
6545 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
6546 positions.len() as u64
6547 );
6548 for (i, &pos) in positions.iter().enumerate() {
6549 assert_eq!(batch[i], test_digest(pos), "position {pos}");
6550 }
6551 drop(reader);
6552
6553 journal.destroy().await.unwrap();
6554 });
6555 }
6556
6557 #[test_traced]
6558 fn test_fixed_journal_read_miss_timed() {
6559 let executor = deterministic::Runner::default();
6561 executor.start(|context| async move {
6562 let mut journal =
6563 Journal::<_, Digest>::init(context.child("miss"), test_cfg(&context, NZU64!(2)))
6564 .await
6565 .unwrap();
6566 for i in 0..20 {
6567 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6568 }
6569 journal = journal.sync().await.unwrap();
6570
6571 let reader;
6573 (journal, reader) = journal.snapshot().await.unwrap();
6574 let pos = (0..20)
6575 .find(|&pos| reader.try_read_sync(pos).is_none())
6576 .expect("some position should be cold");
6577 assert_eq!(reader.read(pos).await.unwrap(), test_digest(pos));
6578 drop(reader);
6579
6580 let buffer = context.encode();
6581 assert!(buffer.contains("miss_read_duration_count 1"), "{buffer}");
6582
6583 journal.destroy().await.unwrap();
6584 });
6585 }
6586}