1use super::{
137 Batch, Decoded,
138 blobs::{Blob, Blobs, Partition, Replay as BlobReplay, Writable},
139 checkpoint::Checkpoint,
140};
141#[commonware_macros::stability(ALPHA)]
142use crate::journal::authenticated;
143use crate::{
144 Context, SyncCompletion,
145 journal::{
146 Error,
147 contiguous::{Many, Mutable, metrics::Metrics},
148 durability::Barrier,
149 },
150};
151use bytes::Bytes;
152use commonware_codec::{CodecFixedShared, Copying, DecodeExt as _, ReadExt as _};
153use commonware_runtime::{
154 Blob as RBlob, Buf, Handle, IoBuf, ReadOptions,
155 buffer::paged::{CacheRef, Recovery as PagedRecovery},
156};
157use commonware_utils::{Cached, futures::try_join_all};
158use futures::{FutureExt as _, Stream};
159use std::{
160 collections::{BTreeMap, btree_map::Entry},
161 future::Future,
162 marker::PhantomData,
163 num::{NonZeroU64, NonZeroUsize},
164 ops::Range,
165 sync::Arc,
166};
167use tracing::warn;
168
169commonware_utils::thread_local_cache!(static PROBE_SCRATCH: Vec<u8>);
173
174pub struct PreparedAppend<A> {
177 buf: Vec<u8>,
178 _marker: PhantomData<A>,
179}
180
181#[inline]
183fn first_in_blob(pruning_boundary: u64, blob: u64, items_per_blob: u64) -> Result<u64, Error> {
184 let start = super::blob_first_position(blob, items_per_blob)?;
185 Ok(pruning_boundary.max(start))
186}
187
188fn replay_stream<'a, B: RBlob, A: CodecFixedShared>(
194 blobs: &Blobs<'a, B>,
195 bounds: Range<u64>,
196 items_per_blob: NonZeroU64,
197 range: Range<u64>,
198 buffer: NonZeroUsize,
199 read_options: ReadOptions,
200) -> Result<impl Stream<Item = Result<Batch<A>, Error>> + Send + use<'a, B, A>, Error> {
201 if range.start > range.end || range.end > bounds.end {
202 return Err(Error::ItemOutOfRange(if range.start > range.end {
203 range.start
204 } else {
205 range.end
206 }));
207 }
208 if range.start < bounds.start {
209 return Err(Error::ItemPruned(range.start));
210 }
211
212 let mut states = Vec::new();
213 if range.start < range.end {
214 let items_per_blob = items_per_blob.get();
215 let start_pos = range.start;
216 let start_blob = super::position_to_blob(start_pos, items_per_blob);
217 let end_blob = super::position_to_blob(range.end - 1, items_per_blob);
218 let items_per_batch = (buffer.get() / A::SIZE).max(1);
219
220 for blob in start_blob..=end_blob {
221 let blob_first = first_in_blob(bounds.start, blob, items_per_blob)?;
224 let first_pos = if blob == start_blob {
225 start_pos
226 } else {
227 blob_first
228 };
229 let blob_end = super::blob_end_position(blob, items_per_blob, range.end);
230 let offset = (first_pos - blob_first)
231 .checked_mul(A::SIZE as u64)
232 .ok_or(Error::OffsetOverflow)?;
233 let blob = blobs
234 .get(blob)
235 .expect("positions in bounds map to a retained blob");
236
237 states.push(FixedReplayState::<B, A> {
238 replay: blob.replay_from(offset, buffer, read_options)?,
239 pos: first_pos,
240 end_pos: blob_end,
241 items_per_batch,
242 _marker: PhantomData,
243 });
244 }
245 }
246
247 Ok(super::replay_stream_from_states(states))
248}
249
250struct FixedReplayState<'a, B: RBlob, A> {
252 replay: BlobReplay<'a, B>,
254 pos: u64,
256 end_pos: u64,
258 items_per_batch: usize,
260 _marker: PhantomData<A>,
261}
262
263impl<B: RBlob, A: CodecFixedShared> super::BatchDecoder for FixedReplayState<'_, B, A> {
264 type Item = A;
265
266 async fn next(mut self) -> Option<(Decoded<A>, Self)> {
268 if self.pos == self.end_pos {
269 return None;
270 }
271
272 let start = self.pos;
275 match self.replay.ensure(A::SIZE).await {
276 Ok(true) => {}
277 Ok(false) => {
278 let error = Error::Corruption(format!("blob ended before position {start}"));
279 let decoded = Decoded::failed(start, Vec::new(), error);
280 return Some((decoded, self));
281 }
282 Err(err) => {
283 let decoded = Decoded::failed(start, Vec::new(), err);
284 return Some((decoded, self));
285 }
286 }
287
288 let available = (self.replay.remaining() / A::SIZE) as u64;
291 let remaining = self.end_pos - self.pos;
292 let count = available.min(self.items_per_batch as u64).min(remaining) as usize;
293
294 let mut items = Vec::with_capacity(count);
300 while items.len() < count {
301 let n = (self.replay.chunk().len() / A::SIZE).min(count - items.len());
302 if n > 0 {
303 let mut page = self.replay.copy_to_bytes(n * A::SIZE);
304 for _ in 0..n {
305 match A::read(&mut page) {
306 Ok(item) => items.push(item),
307 Err(err) => {
308 let decoded = Decoded::failed(start, items, Error::Codec(err));
309 return Some((decoded, self));
310 }
311 }
312 }
313 } else {
314 match self.replay.read::<A>() {
315 Ok(item) => items.push(item),
316 Err(err) => {
317 let decoded = Decoded::failed(start, items, Error::Codec(err));
318 return Some((decoded, self));
319 }
320 }
321 }
322 }
323 self.pos += count as u64;
324 Some((
325 Decoded {
326 batch: Batch { start, items },
327 error: None,
328 },
329 self,
330 ))
331 }
332}
333
334enum BlobFill {
336 Full { len: u64 },
337 Short { len: u64 },
338 Overfull { len: u64, capacity: u64 },
339}
340
341struct RecoveredBounds {
344 size: u64,
346 recovery_watermark: u64,
348}
349
350#[derive(Clone)]
352pub struct Config {
353 pub partition: String,
358
359 pub items_per_blob: NonZeroU64,
365
366 pub page_cache: CacheRef,
368
369 pub write_buffer: NonZeroUsize,
371
372 pub replay_buffer: NonZeroUsize,
374}
375
376pub(super) struct Inner<E: Context, A> {
378 blobs: Writable<E>,
380
381 checkpoint: Checkpoint<E>,
383
384 bounds: Range<u64>,
386
387 items_per_blob: NonZeroU64,
389
390 metrics: Arc<Metrics<E>>,
392
393 barrier: Barrier,
395
396 _phantom: PhantomData<A>,
397}
398
399pub struct Recovery<E: Context, A> {
401 context: E,
403
404 cfg: Config,
406
407 checkpoint: Checkpoint<E>,
409
410 partition: Partition<E>,
412
413 pending: BTreeMap<u64, PagedRecovery<E::Blob>>,
415
416 discarded: Vec<u64>,
418
419 bounds: Range<u64>,
421
422 watermark: u64,
424
425 bounded: bool,
427
428 _marker: PhantomData<A>,
429}
430
431impl<E: Context, A: CodecFixedShared> Recovery<E, A> {
432 async fn open(
434 context: E,
435 cfg: Config,
436 checkpoint: Checkpoint<E>,
437 max_size: Option<u64>,
438 ) -> Result<Self, Error> {
439 let ceiling = max_size.unwrap_or(u64::MAX);
440 if let Some(target) = checkpoint.clear_target() {
441 warn!(
442 clear_target = target,
443 "crash repair: completing interrupted clear"
444 );
445 let recovery = Self::complete_clear(context, cfg, checkpoint, target).await?;
446
447 if ceiling < target {
450 return Err(Error::ItemPruned(ceiling));
451 }
452 return Ok(recovery);
453 }
454 let items_per_blob = cfg.items_per_blob.get();
455
456 let (blob_partition, names) = Partition::select(&context, &cfg.partition).await?;
459 let partition = Partition::new(
460 context.child("blobs"),
461 blob_partition,
462 cfg.page_cache.clone(),
463 cfg.write_buffer,
464 );
465 let indices = Partition::<E>::indices(names)?;
466 let pruning_boundary = Inner::<E, A>::recover_pruning_boundary(
467 checkpoint.boundary_hint(),
468 indices.first().copied(),
469 items_per_blob,
470 )?;
471 if ceiling < pruning_boundary {
472 return Err(Error::ItemPruned(ceiling));
473 }
474
475 let mut pending = BTreeMap::new();
477 let mut discarded = Vec::new();
478 for blob in indices {
479 let first = first_in_blob(pruning_boundary, blob, items_per_blob)?;
480 if max_size.is_some() && first >= ceiling {
481 discarded.push(blob);
482 } else {
483 pending.insert(blob, partition.open_recovery(blob).await?);
484 }
485 }
486
487 let floor = checkpoint.watermark().unwrap_or(0).min(ceiling);
498 let floor_blob = super::position_to_blob(floor, items_per_blob);
499 let suspects: Vec<u64> = pending.keys().rev().take(2).copied().collect();
500 for blob in suspects {
501 if blob < floor_blob {
502 continue;
503 }
504
505 let acknowledged = if blob == floor_blob {
507 Inner::<E, A>::items_to_bytes(floor.saturating_sub(first_in_blob(
508 pruning_boundary,
509 blob,
510 items_per_blob,
511 )?))?
512 } else {
513 0
514 };
515 let writer = pending.get(&blob).expect("suspect blob is present");
516 let required_items = ceiling
517 .saturating_sub(first_in_blob(pruning_boundary, blob, items_per_blob)?)
518 .min(items_per_blob);
519
520 let limit = if max_size.is_some() {
527 let page = u64::from(cfg.page_cache.page_size().get());
528 required_items
529 .saturating_mul(Inner::<E, A>::CHUNK_SIZE_U64)
530 .div_ceil(page)
531 .saturating_mul(page)
532 .min(writer.size())
533 } else {
534 writer.size()
535 };
536 let recoverable = writer
537 .recoverable_prefix_len_at_most(
538 acknowledged,
539 limit,
540 cfg.replay_buffer,
541 ReadOptions::default(),
542 )
543 .await?;
544 let valid_items = recoverable / Inner::<E, A>::CHUNK_SIZE_U64;
545 let valid = Inner::<E, A>::items_to_bytes(valid_items)?;
546 if valid == writer.size()
547 || (max_size.is_some() && recoverable == limit && valid_items >= required_items)
548 {
549 continue;
550 }
551
552 if valid < acknowledged {
555 return Err(Error::Corruption(format!(
556 "blob {blob} no longer backs acknowledged items: well-formed prefix {valid} \
557 of size {}",
558 writer.size()
559 )));
560 }
561 warn!(
562 blob,
563 valid,
564 size = writer.size(),
565 "truncating to recoverable item prefix"
566 );
567 let writer = pending.remove(&blob).expect("suspect blob is present");
568 pending.insert(blob, writer.truncate(valid).await?);
569 }
570
571 let RecoveredBounds {
572 size,
573 recovery_watermark,
574 } = Inner::<E, A>::recover_bounds(
575 &pending,
576 items_per_blob,
577 pruning_boundary,
578 checkpoint
579 .watermark()
580 .map(|watermark| watermark.min(ceiling)),
581 )?;
582
583 Ok(Self {
584 context,
585 cfg,
586 checkpoint,
587 partition,
588 pending,
589 discarded,
590 bounds: pruning_boundary..size.min(ceiling),
591 watermark: recovery_watermark.min(ceiling),
592 bounded: max_size.is_some(),
593 _marker: PhantomData,
594 })
595 }
596
597 async fn complete_clear(
599 context: E,
600 cfg: Config,
601 checkpoint: Checkpoint<E>,
602 target: u64,
603 ) -> Result<Self, Error> {
604 let items_per_blob = cfg.items_per_blob.get();
605 let new_partition = format!("{}-blobs", cfg.partition);
606 Partition::<E>::remove_all(&context, &cfg.partition).await?;
607 Partition::<E>::remove_all(&context, &new_partition).await?;
608 let partition = Partition::new(
609 context.child("blobs"),
610 new_partition,
611 cfg.page_cache.clone(),
612 cfg.write_buffer,
613 );
614
615 let tail = super::position_to_blob(target, items_per_blob);
617 let mut pending = BTreeMap::new();
618 pending.insert(tail, partition.open_recovery(tail).await?);
619 let checkpoint = checkpoint.finish_clear(items_per_blob, target).await?;
620 Ok(Self {
621 context,
622 cfg,
623 checkpoint,
624 partition,
625 pending,
626 discarded: Vec::new(),
627 bounds: target..target,
628 watermark: target,
629
630 bounded: false,
633 _marker: PhantomData,
634 })
635 }
636
637 #[commonware_macros::stability(ALPHA)]
641 pub(super) async fn open_cleared<F, Fut>(
642 context: E,
643 cfg: Config,
644 checkpoint: Checkpoint<E>,
645 size: u64,
646 clear_dependents: F,
647 ) -> Result<Self, Error>
648 where
649 F: FnOnce() -> Fut,
650 Fut: Future<Output = Result<(), Error>>,
651 {
652 if size == u64::MAX {
655 return Err(Error::SizeOverflow);
656 }
657 let checkpoint = checkpoint.stage_clear(size).await?;
658 clear_dependents().await?;
659 Self::complete_clear(context, cfg, checkpoint, size).await
660 }
661
662 pub(super) async fn init_cleared<F, Fut>(
664 context: E,
665 cfg: Config,
666 max_size: Option<u64>,
667 clear_dependents: F,
668 ) -> Result<Self, Error>
669 where
670 F: FnOnce() -> Fut,
671 Fut: Future<Output = Result<(), Error>>,
672 {
673 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
674 if checkpoint.clear_target().is_some() {
675 clear_dependents().await?;
676 }
677 Self::open(context, cfg, checkpoint, max_size).await
678 }
679
680 #[commonware_macros::stability(ALPHA)]
684 pub(super) async fn span(
685 context: &E,
686 cfg: &Config,
687 checkpoint: &Checkpoint<E>,
688 ) -> Result<Range<u64>, Error> {
689 let per_blob = cfg.items_per_blob.get();
690 let (_, names) = Partition::select(context, &cfg.partition).await?;
691 let indices = Partition::<E>::indices(names)?;
692 let start = Inner::<E, A>::recover_pruning_boundary(
693 checkpoint.boundary_hint(),
694 indices.first().copied(),
695 per_blob,
696 )?;
697 let end = match indices.last() {
698 Some(&newest) => super::blob_first_position(newest, per_blob)?.saturating_add(per_blob),
699 None => start,
700 };
701 Ok(start..end)
702 }
703
704 pub(super) const fn size(&self) -> u64 {
706 self.bounds.end
707 }
708
709 pub(super) const fn pruning_boundary(&self) -> u64 {
711 self.bounds.start
712 }
713
714 pub(super) const fn recovery_watermark(&self) -> u64 {
716 self.watermark
717 }
718
719 pub(super) async fn item(&self, pos: u64) -> Result<A, Error> {
721 if pos < self.bounds.start {
722 return Err(Error::ItemPruned(pos));
723 }
724 if pos >= self.bounds.end {
725 return Err(Error::ItemOutOfRange(pos));
726 }
727 let blob = super::position_to_blob(pos, self.cfg.items_per_blob.get());
728 let first = first_in_blob(self.bounds.start, blob, self.cfg.items_per_blob.get())?;
729 let offset = Inner::<E, A>::items_to_bytes(pos - first)?;
730
731 let writer = self
734 .pending
735 .get(&blob)
736 .expect("positions inside bounds map to a pending recovery blob");
737 Ok(A::decode(
738 writer.read_at(offset, A::SIZE).await?.coalesce(),
739 )?)
740 }
741
742 pub(crate) async fn append(mut self: Box<Self>, item: &A) -> Result<Box<Self>, Error> {
746 let pos = self.bounds.end;
747 let end = pos.checked_add(1).ok_or(Error::SizeOverflow)?;
748 let blob = super::position_to_blob(pos, self.cfg.items_per_blob.get());
749
750 let appended = self
752 .pending
753 .get_mut(&blob)
754 .is_some_and(|writer| writer.try_append_value(item).is_some());
755
756 let completed = end.is_multiple_of(self.cfg.items_per_blob.get());
757
758 if !appended || completed {
761 let mut writer = match self.pending.remove(&blob) {
762 Some(writer) => writer,
763 None => self.partition.open_recovery(blob).await?,
764 };
765 if !appended && writer.try_append_value(item).is_none() {
766 (writer, _) = writer.append_owned(item.encode_mut().into()).await?;
767 }
768
769 if completed {
772 writer = writer.sync().await?;
773 }
774 self.pending.insert(blob, writer);
775 }
776 self.bounds.end = end;
777 Ok(self)
778 }
779
780 pub(crate) async fn truncate(mut self: Box<Self>, max_size: u64) -> Result<Box<Self>, Error> {
782 *self = self.repair_to(max_size).await?;
783 Ok(self)
784 }
785
786 async fn repair_to(mut self, max_size: u64) -> Result<Self, Error> {
789 let size = self.bounds.end.min(max_size);
790 if size < self.bounds.start {
791 return Err(Error::ItemPruned(size));
792 }
793 let items_per_blob = self.cfg.items_per_blob.get();
794 let tail_blob = super::position_to_blob(size, items_per_blob);
795 let bytes = Inner::<E, A>::items_to_bytes(
796 size - first_in_blob(self.bounds.start, tail_blob, items_per_blob)?,
797 )?;
798
799 self.watermark = self.watermark.min(size);
801 let boundary_hint =
802 (!self.bounds.start.is_multiple_of(items_per_blob)).then_some(self.bounds.start);
803 if self.checkpoint.watermark() != Some(self.watermark)
804 || self.checkpoint.boundary_hint() != boundary_hint
805 {
806 self.checkpoint = self
807 .checkpoint
808 .persist(items_per_blob, self.bounds.start, self.watermark)
809 .await?;
810 }
811 if size == self.bounds.start
812 && (!self.discarded.is_empty() || self.pending.keys().any(|&blob| blob > tail_blob))
813 {
814 if let Entry::Vacant(entry) = self.pending.entry(tail_blob) {
817 let writer = self.partition.open_recovery(tail_blob).await?;
818 entry.insert(writer.sync().await?);
819 }
820 self.discarded.retain(|&blob| blob != tail_blob);
821 }
822
823 while let Some(blob) = self.discarded.pop() {
826 self.partition.remove(blob).await?;
827 }
828 while let Some((&blob, _)) = self.pending.last_key_value() {
829 if blob <= tail_blob {
830 break;
831 }
832 let writer = self.pending.remove(&blob);
833 self.partition.remove(blob).await?;
834 drop(writer);
835 }
836
837 if let Some(writer) = self.pending.remove(&tail_blob) {
840 let writer = if bytes < writer.size() {
841 writer.truncate(bytes).await?
842 } else {
843 writer.sync().await?
844 };
845 self.pending.insert(tail_blob, writer);
846 }
847 self.bounds.end = size;
848 Ok(self)
849 }
850
851 async fn sync_pending(&mut self) -> Result<(), Error> {
854 let futures: Vec<_> = self
855 .pending
856 .extract_if(.., |_, writer| writer.needs_sync())
857 .map(|(blob, writer)| async move { writer.sync().await.map(|writer| (blob, writer)) })
858 .collect();
859 let synced = try_join_all(futures).await?;
860 self.pending.extend(synced);
861 Ok(())
862 }
863
864 pub(crate) async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
866 self.sync_pending().await?;
867 self.watermark = self.bounds.end;
868 self.checkpoint = self
869 .checkpoint
870 .persist(
871 self.cfg.items_per_blob.get(),
872 self.bounds.start,
873 self.watermark,
874 )
875 .await?;
876 Ok(self)
877 }
878
879 pub(crate) async fn prune(
881 mut self: Box<Self>,
882 position: u64,
883 ) -> Result<(Box<Self>, bool), Error> {
884 let per_blob = self.cfg.items_per_blob.get();
887 let blob = super::position_to_blob(position.min(self.bounds.end), per_blob);
888 let boundary = super::blob_first_position(blob, per_blob)?;
889 if boundary <= self.bounds.start {
890 return Ok((self, false));
891 }
892
893 if let Entry::Vacant(entry) = self.pending.entry(blob) {
896 entry.insert(self.partition.open_recovery(blob).await?);
897 }
898
899 self.sync_pending().await?;
901 while let Some((&oldest, _)) = self.pending.first_key_value() {
902 if oldest >= blob {
903 break;
904 }
905 drop(self.pending.remove(&oldest));
906 self.partition.remove(oldest).await?;
907 }
908 self.bounds.start = boundary;
909 Ok((self, true))
910 }
911
912 pub(crate) async fn clear_to_size(self: Box<Self>, size: u64) -> Result<Box<Self>, Error> {
914 self.clear_to_size_cleared(size, || async { Ok(()) }).await
915 }
916
917 pub(super) async fn clear_to_size_cleared<F, Fut>(
920 self: Box<Self>,
921 size: u64,
922 clear_dependents: F,
923 ) -> Result<Box<Self>, Error>
924 where
925 F: FnOnce() -> Fut,
926 Fut: Future<Output = Result<(), Error>>,
927 {
928 if size == u64::MAX {
931 return Err(Error::SizeOverflow);
932 }
933 let Self {
934 context,
935 cfg,
936 checkpoint,
937 partition,
938 pending,
939 ..
940 } = *self;
941
942 drop(pending);
944 drop(partition);
945
946 let checkpoint = checkpoint.stage_clear(size).await?;
948
949 clear_dependents().await?;
952 Ok(Box::new(
953 Self::complete_clear(context, cfg, checkpoint, size).await?,
954 ))
955 }
956
957 pub(super) async fn publish(mut self, max_size: u64) -> Result<Inner<E, A>, Error> {
959 self = self.repair_to(max_size).await?;
961 let size = self.bounds.end;
962 let items_per_blob = self.cfg.items_per_blob.get();
963 let tail_blob = super::position_to_blob(size, items_per_blob);
964 let blobs = Writable::recover(self.partition, self.pending, tail_blob).await?;
965
966 if self.bounded && self.checkpoint.watermark() != Some(size) {
968 self.checkpoint = self
969 .checkpoint
970 .persist(items_per_blob, self.bounds.start, size)
971 .await?;
972 }
973 let metrics = Metrics::new(self.context);
974 metrics.update(size, self.bounds.start, items_per_blob);
975 Ok(Inner::from_blobs(
976 blobs,
977 self.checkpoint,
978 self.bounds.start..size,
979 self.cfg.items_per_blob,
980 metrics,
981 ))
982 }
983}
984
985impl<E: Context, A: CodecFixedShared> Inner<E, A> {
986 pub const CHUNK_SIZE: NonZeroUsize = match NonZeroUsize::new(A::SIZE) {
989 Some(size) => size,
990 None => panic!("journal item size must be nonzero"),
991 };
992
993 pub const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE.get() as u64;
995
996 fn items_to_bytes(items: u64) -> Result<u64, Error> {
998 items
999 .checked_mul(Self::CHUNK_SIZE_U64)
1000 .ok_or(Error::OffsetOverflow)
1001 }
1002
1003 fn from_blobs(
1005 blobs: Writable<E>,
1006 checkpoint: Checkpoint<E>,
1007 bounds: Range<u64>,
1008 items_per_blob: NonZeroU64,
1009 metrics: Metrics<E>,
1010 ) -> Self {
1011 Self {
1012 blobs,
1013 barrier: Barrier::new(
1014 checkpoint
1015 .watermark()
1016 .expect("recovery watermark must exist after init"),
1017 ),
1018 checkpoint,
1019 bounds,
1020 items_per_blob,
1021 metrics: Arc::new(metrics),
1022 _phantom: PhantomData,
1023 }
1024 }
1025
1026 pub(crate) async fn init(context: E, cfg: Config) -> Result<Self, Error> {
1028 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
1029 Recovery::<E, A>::open(context, cfg, checkpoint, None)
1030 .await?
1031 .publish(u64::MAX)
1032 .await
1033 }
1034
1035 fn recover_bounds(
1041 pending: &BTreeMap<u64, PagedRecovery<E::Blob>>,
1042 items_per_blob: u64,
1043 pruning_boundary: u64,
1044 watermark_hint: Option<u64>,
1045 ) -> Result<RecoveredBounds, Error> {
1046 let (size, has_gap) =
1047 Self::recover_by_walking_lengths(pending, items_per_blob, pruning_boundary)?;
1048
1049 let recovery_watermark = match watermark_hint {
1050 Some(watermark) if watermark > size => {
1051 return Err(Error::Corruption(format!(
1055 "recovery watermark {watermark} exceeds recoverable size {size}"
1056 )));
1057 }
1058 Some(watermark) => watermark,
1059 None if has_gap => {
1060 return Err(Error::Corruption(
1063 "legacy journal has a short non-tail blob".into(),
1064 ));
1065 }
1066 None => first_in_blob(
1069 pruning_boundary,
1070 super::position_to_blob(size, items_per_blob),
1071 items_per_blob,
1072 )?,
1073 };
1074
1075 Ok(RecoveredBounds {
1076 size,
1077 recovery_watermark,
1078 })
1079 }
1080
1081 fn recover_pruning_boundary(
1087 boundary_hint: Option<u64>,
1088 oldest_blob: Option<u64>,
1089 items_per_blob: u64,
1090 ) -> Result<u64, Error> {
1091 let blob_boundary = match oldest_blob {
1092 Some(oldest) => super::blob_first_position(oldest, items_per_blob)?,
1093 None => 0,
1094 };
1095
1096 let Some(boundary_hint) = boundary_hint else {
1097 return Ok(blob_boundary);
1098 };
1099 if boundary_hint.is_multiple_of(items_per_blob) {
1100 return Ok(blob_boundary);
1101 }
1102
1103 let hint_blob = super::position_to_blob(boundary_hint, items_per_blob);
1104 match oldest_blob {
1105 Some(oldest_blob) if hint_blob == oldest_blob => Ok(boundary_hint),
1106 Some(oldest_blob) if hint_blob < oldest_blob => {
1107 warn!(
1108 hint_blob,
1109 oldest_blob, "crash repair: boundary hint stale, computing from blobs"
1110 );
1111 Ok(blob_boundary)
1112 }
1113 Some(oldest_blob) => {
1114 Err(Error::Corruption(format!(
1117 "boundary hint references blob {hint_blob} \
1118 but oldest blob is blob {oldest_blob}"
1119 )))
1120 }
1121 None => {
1122 Err(Error::Corruption(format!(
1126 "boundary hint references blob {hint_blob} but no blobs exist"
1127 )))
1128 }
1129 }
1130 }
1131
1132 fn classify_fill(
1135 pending: &BTreeMap<u64, PagedRecovery<E::Blob>>,
1136 items_per_blob: u64,
1137 pruning_boundary: u64,
1138 blob: u64,
1139 ) -> Result<BlobFill, Error> {
1140 let len = pending
1141 .get(&blob)
1142 .map_or(0, |writer| writer.size() / Self::CHUNK_SIZE_U64);
1143 let start = super::blob_first_position(blob, items_per_blob)?;
1146 let skipped = pruning_boundary.saturating_sub(start).min(items_per_blob);
1147 let capacity = items_per_blob - skipped;
1148 Ok(match len.cmp(&capacity) {
1149 std::cmp::Ordering::Less => BlobFill::Short { len },
1150 std::cmp::Ordering::Equal => BlobFill::Full { len },
1151 std::cmp::Ordering::Greater => BlobFill::Overfull { len, capacity },
1152 })
1153 }
1154
1155 fn recover_by_walking_lengths(
1162 pending: &BTreeMap<u64, PagedRecovery<E::Blob>>,
1163 items_per_blob: u64,
1164 pruning_boundary: u64,
1165 ) -> Result<(u64, bool), Error> {
1166 let oldest = pending.keys().next().copied();
1167 let newest = pending.keys().next_back().copied();
1168
1169 let (Some(oldest), Some(newest)) = (oldest, newest) else {
1170 return Ok((pruning_boundary, false));
1171 };
1172
1173 let mut size = pruning_boundary;
1174 for blob in oldest..=newest {
1175 let fill = Self::classify_fill(pending, items_per_blob, pruning_boundary, blob)?;
1176 match fill {
1177 BlobFill::Full { len } => {
1179 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
1180 }
1181 BlobFill::Short { len } if blob == newest => {
1183 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
1184 return Ok((size, false));
1185 }
1186 BlobFill::Short { len } => {
1189 size = size.checked_add(len).ok_or(Error::OffsetOverflow)?;
1190 return Ok((size, true));
1191 }
1192 BlobFill::Overfull { len, capacity } => {
1193 return Err(Error::Corruption(format!(
1194 "blob {blob} has too many items: expected at most {capacity}, got {len}"
1195 )));
1196 }
1197 }
1198 }
1199
1200 Ok((size, false))
1201 }
1202
1203 #[commonware_macros::stability(ALPHA)]
1205 pub(crate) async fn init_at_size(context: E, cfg: Config, size: u64) -> Result<Self, Error> {
1206 Partition::select(&context, &cfg.partition).await?;
1208
1209 if size == u64::MAX {
1212 return Err(Error::SizeOverflow);
1213 }
1214 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
1215 Recovery::<E, A>::open_cleared(context, cfg, checkpoint, size, || async { Ok(()) })
1216 .await?
1217 .publish(u64::MAX)
1218 .await
1219 }
1220
1221 pub(super) async fn start_data_sync(
1223 mut self: Box<Self>,
1224 ) -> Result<(Box<Self>, Handle<()>), Error> {
1225 let (blobs, handle) = self.blobs.start_sync().await?;
1226 self.blobs = blobs;
1227 let completion: SyncCompletion = handle.boxed().shared();
1228 self.barrier.record(self.bounds.end, completion.clone());
1229 Ok((self, Handle::from_future(completion)))
1230 }
1231
1232 pub(super) async fn start_watermark_sync(
1234 mut self: Box<Self>,
1235 size: u64,
1236 ) -> Result<(Box<Self>, Handle<()>), Error> {
1237 let size = size.min(self.barrier.boundary());
1238 let (checkpoint, handle) = self.checkpoint.start_watermark_sync(size).await?;
1239 self.checkpoint = checkpoint;
1240 Ok((self, handle))
1241 }
1242
1243 pub(crate) async fn start_sync(self: Box<Self>) -> Result<(Box<Self>, Handle<()>), Error> {
1245 self.metrics.start_sync_calls.inc();
1246
1247 let (mut journal, data) = self.start_data_sync().await?;
1249 let size = journal.barrier.boundary();
1250 let (journal, watermark) = journal.start_watermark_sync(size).await?;
1251
1252 let handle = Handle::from_future(async move {
1254 data.await?;
1255 watermark.await
1256 });
1257 Ok((journal, handle))
1258 }
1259
1260 pub(crate) async fn commit(mut self: Box<Self>) -> Result<Box<Self>, Error> {
1262 let _timer = self.metrics.commit_timer();
1263 self.metrics.commit_calls.inc();
1264 let size = self.bounds.end;
1265 let (blobs, handle) = self.blobs.start_sync().await?;
1266 self.blobs = blobs;
1267 handle.await?;
1268 self.barrier.mark_durable(size);
1269 Ok(self)
1270 }
1271
1272 pub(crate) async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
1274 let _timer = self.metrics.sync_timer();
1275 self.metrics.sync_calls.inc();
1276
1277 let size = self.bounds.end;
1279 let (blobs, handle) = self.blobs.start_sync().await?;
1280 self.blobs = blobs;
1281 handle.await?;
1282 self.barrier.mark_durable(size);
1283
1284 self.checkpoint = self
1286 .checkpoint
1287 .persist(self.items_per_blob.get(), self.bounds.start, size)
1288 .await?;
1289 Ok(self)
1290 }
1291
1292 pub(crate) async fn snapshot(
1294 mut self: Box<Self>,
1295 ) -> Result<(Box<Self>, Reader<'static, E, A>), Error> {
1296 let (blobs, snapshot) = self.blobs.snapshot().await?;
1297 self.blobs = blobs;
1298 let reader = Reader {
1299 blobs: snapshot,
1300 bounds: self.bounds.clone(),
1301 items_per_blob: self.items_per_blob,
1302 metrics: self.metrics.clone(),
1303 _phantom: PhantomData,
1304 };
1305 Ok((self, reader))
1306 }
1307
1308 pub(super) fn reader(&self) -> Reader<'_, E, A> {
1310 Reader {
1311 blobs: self.blobs.reader(),
1312 bounds: self.bounds.clone(),
1313 items_per_blob: self.items_per_blob,
1314 metrics: self.metrics.clone(),
1315 _phantom: PhantomData,
1316 }
1317 }
1318
1319 pub const fn size(&self) -> u64 {
1322 self.bounds.end
1323 }
1324
1325 pub(crate) async fn append(mut self: Box<Self>, item: &A) -> Result<(Box<Self>, u64), Error> {
1327 let _timer = self.metrics.append_timer();
1328 self.metrics.append_calls.inc();
1329 let new_size = self.bounds.end.checked_add(1).ok_or(Error::SizeOverflow)?;
1330 if self.blobs.try_append_value(item).is_none() {
1331 return self
1332 .append_many_inner(Many::Flat(std::slice::from_ref(item)))
1333 .await;
1334 }
1335 self = self.advance_tail(new_size).await?;
1336 let position = self.finish_append();
1337 Ok((self, position))
1338 }
1339
1340 pub(crate) async fn append_many(
1342 self: Box<Self>,
1343 items: Many<'_, A>,
1344 ) -> Result<(Box<Self>, u64), Error> {
1345 let _timer = self.metrics.append_many_timer();
1346 self.metrics.append_many_calls.inc();
1347 self.append_many_inner(items).await
1348 }
1349
1350 async fn append_many_inner(
1352 self: Box<Self>,
1353 items: Many<'_, A>,
1354 ) -> Result<(Box<Self>, u64), Error> {
1355 let prepared = self.prepare_append(items);
1356 self.write_encoded(prepared).await
1357 }
1358
1359 pub(crate) fn prepare_append(&self, items: Many<'_, A>) -> PreparedAppend<A> {
1361 let mut buf = Vec::with_capacity(items.len() * A::SIZE);
1364 match items {
1365 Many::Flat(items) => {
1366 for item in items {
1367 item.write(&mut buf);
1368 }
1369 }
1370 Many::Nested(nested_items) => {
1371 for items in nested_items {
1372 for item in *items {
1373 item.write(&mut buf);
1374 }
1375 }
1376 }
1377 }
1378 PreparedAppend {
1379 buf,
1380 _marker: PhantomData,
1381 }
1382 }
1383
1384 pub(crate) async fn append_prepared(
1386 self: Box<Self>,
1387 prepared: PreparedAppend<A>,
1388 ) -> Result<(Box<Self>, u64), Error> {
1389 let _timer = self.metrics.append_prepared_timer();
1390 self.metrics.append_prepared_calls.inc();
1391 self.write_encoded(prepared).await
1392 }
1393
1394 async fn write_encoded(
1396 mut self: Box<Self>,
1397 prepared: PreparedAppend<A>,
1398 ) -> Result<(Box<Self>, u64), Error> {
1399 let items_buf = prepared.buf;
1400 let items_count = items_buf.len() / A::SIZE;
1401 if items_count == 0 {
1402 return Err(Error::EmptyAppend);
1403 }
1404 let items_buf = IoBuf::from(items_buf);
1405
1406 self.bounds
1409 .end
1410 .checked_add(items_count as u64)
1411 .ok_or(Error::SizeOverflow)?;
1412
1413 let mut written = 0;
1415 while written < items_count {
1416 let batch_count = super::batch_count_to_blob_boundary(
1417 self.bounds.end,
1418 items_count - written,
1419 self.items_per_blob.get(),
1420 );
1421 let start = written * A::SIZE;
1422 let end = start + batch_count * A::SIZE;
1423 let new_size = self.bounds.end + batch_count as u64;
1424
1425 if self
1427 .blobs
1428 .try_append(&items_buf.as_ref()[start..end])
1429 .is_none()
1430 {
1431 (self.blobs, _) = self.blobs.append_owned(items_buf.slice(start..end)).await?;
1432 }
1433 self = self.advance_tail(new_size).await?;
1434 written += batch_count;
1435 }
1436
1437 let position = self.finish_append();
1438 Ok((self, position))
1439 }
1440
1441 async fn advance_tail(mut self: Box<Self>, new_size: u64) -> Result<Box<Self>, Error> {
1443 self.bounds.end = new_size;
1444 if new_size.is_multiple_of(self.items_per_blob.get()) {
1445 self.blobs = self.blobs.seal_tail().await?;
1446 }
1447 Ok(self)
1448 }
1449
1450 fn finish_append(&self) -> u64 {
1452 self.metrics.update(
1453 self.bounds.end,
1454 self.bounds.start,
1455 self.items_per_blob.get(),
1456 );
1457 self.bounds.end - 1
1458 }
1459
1460 pub const fn pruning_boundary(&self) -> u64 {
1462 self.bounds.start
1463 }
1464
1465 pub(crate) async fn prune(
1467 mut self: Box<Self>,
1468 min_item_pos: u64,
1469 ) -> Result<(Box<Self>, bool), Error> {
1470 let target_blob = super::position_to_blob(min_item_pos, self.items_per_blob.get());
1473 let tail_blob = super::position_to_blob(self.bounds.end, self.items_per_blob.get());
1474 let min_blob = std::cmp::min(target_blob, tail_blob);
1475
1476 if min_blob <= self.blobs.oldest_blob_index() {
1477 return Ok((self, false));
1478 }
1479
1480 let (blobs, sync) = self.blobs.start_sync().await?;
1488 self.blobs = blobs;
1489 sync.await?;
1490 self.barrier.mark_durable(self.bounds.end);
1491
1492 let new_boundary = super::blob_first_position(min_blob, self.items_per_blob.get())?;
1493 self.blobs = self.blobs.prune(min_blob).await?;
1494 self.bounds.start = new_boundary;
1495
1496 self.metrics.update(
1497 self.bounds.end,
1498 self.bounds.start,
1499 self.items_per_blob.get(),
1500 );
1501
1502 Ok((self, true))
1503 }
1504
1505 pub(super) async fn wait_for_sync(mut self) -> Result<Self, Error> {
1508 self.blobs = self.blobs.wait_for_sync().await?;
1509 self.checkpoint.wait_for_sync().await?;
1510 Ok(self)
1511 }
1512
1513 pub(crate) async fn destroy(self) -> Result<(), Error> {
1515 let mut journal = self.wait_for_sync().await?;
1518
1519 journal.checkpoint = journal.checkpoint.stage_clear(0).await?;
1523 journal.blobs.destroy().await?;
1524 journal.checkpoint.destroy().await
1525 }
1526
1527 #[commonware_macros::stability(ALPHA)]
1537 pub(crate) async fn clear_to_size(
1538 mut self: Box<Self>,
1539 new_size: u64,
1540 ) -> Result<Box<Self>, Error> {
1541 if new_size < self.bounds.end {
1543 return Err(Error::ItemOutOfRange(new_size));
1544 }
1545
1546 if new_size == u64::MAX {
1548 return Err(Error::SizeOverflow);
1549 }
1550
1551 self.checkpoint = self.checkpoint.stage_clear(new_size).await?;
1553
1554 self.blobs = self
1556 .blobs
1557 .clear(super::position_to_blob(new_size, self.items_per_blob.get()))
1558 .await?;
1559 self.bounds = new_size..new_size;
1560 self.barrier = Barrier::new(new_size);
1561
1562 self.checkpoint = self
1564 .checkpoint
1565 .finish_clear(self.items_per_blob.get(), new_size)
1566 .await?;
1567
1568 self.metrics.update(
1569 self.bounds.end,
1570 self.bounds.start,
1571 self.items_per_blob.get(),
1572 );
1573 Ok(self)
1574 }
1575
1576 #[commonware_macros::stability(ALPHA)]
1583 pub(super) async fn stage_clear_intent(
1584 mut self: Box<Self>,
1585 new_size: u64,
1586 ) -> Result<Box<Self>, Error> {
1587 if new_size == u64::MAX {
1589 return Err(Error::SizeOverflow);
1590 }
1591 if new_size < self.bounds.end {
1592 return Err(Error::ItemOutOfRange(new_size));
1593 }
1594 self.checkpoint = self.checkpoint.stage_clear(new_size).await?;
1595 Ok(self)
1596 }
1597}
1598
1599pub struct Journal<E: Context, A>(Box<Inner<E, A>>);
1616
1617impl<E: Context, A: CodecFixedShared> std::fmt::Debug for Journal<E, A> {
1618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1619 f.debug_struct("Journal")
1620 .field("bounds", &super::Contiguous::bounds(self))
1621 .finish_non_exhaustive()
1622 }
1623}
1624
1625impl<E: Context, A: CodecFixedShared> Journal<E, A> {
1626 pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
1633 Ok(Self(Box::new(Inner::init(context, cfg).await?)))
1634 }
1635
1636 pub async fn init_at_most(context: E, cfg: Config, max_size: u64) -> Result<Self, Error> {
1642 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
1643 let recovery = Recovery::<E, A>::open(context, cfg, checkpoint, Some(max_size)).await?;
1644 Ok(Self(Box::new(recovery.publish(max_size).await?)))
1645 }
1646
1647 #[commonware_macros::stability(ALPHA)]
1655 pub async fn init_at_size(context: E, cfg: Config, size: u64) -> Result<Self, Error> {
1656 Ok(Self(Box::new(
1657 Inner::init_at_size(context, cfg, size).await?,
1658 )))
1659 }
1660
1661 #[commonware_macros::stability(ALPHA)]
1663 pub(crate) async fn clear_to_size(mut self, new_size: u64) -> Result<Self, Error> {
1664 self.0 = self.0.clear_to_size(new_size).await?;
1665 Ok(self)
1666 }
1667
1668 pub async fn commit(mut self) -> Result<Self, Error> {
1674 self.0 = self.0.commit().await?;
1675 Ok(self)
1676 }
1677
1678 pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
1693 let (inner, handle) = self.0.start_sync().await?;
1694 self.0 = inner;
1695 Ok((self, handle))
1696 }
1697
1698 pub async fn sync(mut self) -> Result<Self, Error> {
1703 self.0 = self.0.sync().await?;
1704 Ok(self)
1705 }
1706
1707 pub async fn snapshot(mut self) -> Result<(Self, Reader<'static, E, A>), Error> {
1712 let (inner, reader) = self.0.snapshot().await?;
1713 self.0 = inner;
1714 Ok((self, reader))
1715 }
1716
1717 pub fn size(&self) -> u64 {
1720 self.0.size()
1721 }
1722
1723 pub async fn append(mut self, item: &A) -> Result<(Self, u64), Error> {
1729 let (inner, position) = self.0.append(item).await?;
1730 self.0 = inner;
1731 Ok((self, position))
1732 }
1733
1734 pub async fn append_many(mut self, items: Many<'_, A>) -> Result<(Self, u64), Error> {
1738 let (inner, position) = self.0.append_many(items).await?;
1739 self.0 = inner;
1740 Ok((self, position))
1741 }
1742
1743 pub fn prepare_append(&self, items: Many<'_, A>) -> PreparedAppend<A> {
1748 self.0.prepare_append(items)
1749 }
1750
1751 pub async fn append_prepared(
1756 mut self,
1757 prepared: PreparedAppend<A>,
1758 ) -> Result<(Self, u64), Error> {
1759 let (inner, position) = self.0.append_prepared(prepared).await?;
1760 self.0 = inner;
1761 Ok((self, position))
1762 }
1763
1764 pub fn pruning_boundary(&self) -> u64 {
1766 self.0.pruning_boundary()
1767 }
1768
1769 pub async fn prune(mut self, min_item_pos: u64) -> Result<(Self, bool), Error> {
1779 let (inner, pruned) = self.0.prune(min_item_pos).await?;
1780 self.0 = inner;
1781 Ok((self, pruned))
1782 }
1783
1784 pub async fn destroy(self) -> Result<(), Error> {
1792 self.0.destroy().await
1793 }
1794}
1795
1796pub struct Reader<'a, E: Context, A> {
1798 blobs: Blobs<'a, E::Blob>,
1799 bounds: Range<u64>,
1800 items_per_blob: NonZeroU64,
1801 metrics: Arc<Metrics<E>>,
1802 _phantom: PhantomData<A>,
1803}
1804
1805impl<E: Context, A: CodecFixedShared> Reader<'_, E, A> {
1806 const fn validate_readable(&self, pos: u64) -> Result<(), Error> {
1808 if pos >= self.bounds.end {
1809 return Err(Error::ItemOutOfRange(pos));
1810 }
1811 if pos < self.bounds.start {
1812 return Err(Error::ItemPruned(pos));
1813 }
1814 Ok(())
1815 }
1816
1817 fn locate_group(&self, group: &[u64]) -> Result<(u64, Vec<u64>), Error> {
1820 let items_per_blob = self.items_per_blob.get();
1821 let blob = super::position_to_blob(group[0], items_per_blob);
1822 let first_position = first_in_blob(self.bounds.start, blob, items_per_blob)?;
1823 let mut offsets = Vec::with_capacity(group.len());
1824 for &pos in group {
1825 offsets.push(Inner::<E, A>::items_to_bytes(pos - first_position)?);
1826 }
1827 Ok((blob, offsets))
1828 }
1829
1830 pub(super) async fn read_many_inner(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
1834 if positions.is_empty() {
1835 return Ok(Vec::new());
1836 }
1837 assert!(
1838 positions.is_sorted_by(|a, b| a < b),
1839 "positions must be strictly increasing"
1840 );
1841 for &pos in positions {
1842 self.validate_readable(pos)?;
1843 }
1844
1845 let items_per_blob = self.items_per_blob.get();
1846
1847 let mut result: Vec<A> = Vec::with_capacity(positions.len());
1852 let mut buf = vec![0u8; positions.len() * A::SIZE];
1853
1854 let mut reads = Vec::new();
1857 let mut remaining_buf = buf.as_mut_slice();
1858 for group in positions.chunk_by(|a, b| {
1859 super::position_to_blob(*a, items_per_blob)
1860 == super::position_to_blob(*b, items_per_blob)
1861 }) {
1862 let (blob_num, blob_offsets) = self.locate_group(group)?;
1863 let blob = self
1864 .blobs
1865 .get(blob_num)
1866 .expect("positions in bounds map to a retained blob");
1867 let (group_buf, rest) = remaining_buf.split_at_mut(group.len() * A::SIZE);
1868 remaining_buf = rest;
1869 reads.push(async move {
1870 blob.read_many_into(group_buf, &blob_offsets, Inner::<E, A>::CHUNK_SIZE)
1871 .await
1872 });
1873 }
1874 let hits: u64 = try_join_all(reads)
1875 .await?
1876 .into_iter()
1877 .map(|group_hits| group_hits as u64)
1878 .sum();
1879
1880 let mut bytes = Bytes::from(buf);
1881 for _ in positions {
1882 result.push(A::decode((&mut bytes).take(A::SIZE)).map_err(Error::Codec)?);
1883 }
1884
1885 self.metrics.cache_hits.inc_by(hits);
1886 self.metrics
1887 .cache_misses
1888 .inc_by(positions.len() as u64 - hits);
1889 self.metrics.items_read.inc_by(positions.len() as u64);
1890 Ok(result)
1891 }
1892
1893 fn locate(&self, pos: u64) -> Result<(Blob<'_, E::Blob>, u64), Error> {
1895 self.validate_readable(pos)?;
1896 let items_per_blob = self.items_per_blob.get();
1897 let blob = super::position_to_blob(pos, items_per_blob);
1898 let pos_in_blob = pos - first_in_blob(self.bounds.start, blob, items_per_blob)?;
1899 let offset = Inner::<E, A>::items_to_bytes(pos_in_blob)?;
1900 let blob = self
1901 .blobs
1902 .get(blob)
1903 .expect("position in bounds maps to a retained blob");
1904 Ok((blob, offset))
1905 }
1906
1907 pub(super) async fn read_miss(&self, pos: u64) -> Result<A, Error> {
1909 let _timer = self.metrics.read_timer();
1910 let (blob, offset) = self.locate(pos)?;
1911 self.metrics.cache_misses.inc();
1912 let bufs = blob.read_at(offset, A::SIZE).await?;
1913 let item = A::decode(bufs.coalesce()).map_err(Error::Codec)?;
1914 self.metrics.items_read.inc();
1915 Ok(item)
1916 }
1917
1918 pub(super) fn probe_items(&self, positions: &[u64]) -> Vec<Option<A>> {
1922 assert!(
1923 positions.is_sorted_by(|a, b| a < b),
1924 "positions must be strictly increasing"
1925 );
1926 let mut out: Vec<Option<A>> = (0..positions.len()).map(|_| None).collect();
1927
1928 let start = positions.partition_point(|&pos| pos < self.bounds.start);
1932 let end = positions.partition_point(|&pos| pos < self.bounds.end);
1933 let valid = &positions[start..end];
1934 if valid.is_empty() {
1935 return out;
1936 }
1937
1938 let items_per_blob = self.items_per_blob.get();
1941 let mut scratch =
1942 Cached::take(&PROBE_SCRATCH, || Ok::<_, ()>(Vec::new()), |_| Ok(())).unwrap();
1943 let need = valid.len() * A::SIZE;
1944 if scratch.len() < need {
1945 scratch.resize(need, 0);
1946 }
1947 let buf = &mut scratch[..need];
1948 let mut hits = 0u64;
1949 let mut group_base = start;
1950 for group in valid.chunk_by(|a, b| {
1951 super::position_to_blob(*a, items_per_blob)
1952 == super::position_to_blob(*b, items_per_blob)
1953 }) {
1954 let base = group_base;
1955 group_base += group.len();
1956 let Ok((blob_num, blob_offsets)) = self.locate_group(group) else {
1957 continue;
1958 };
1959 let Some(blob) = self.blobs.get(blob_num) else {
1960 continue;
1961 };
1962 let buf = &mut buf[..group.len() * A::SIZE];
1963 let misses =
1964 blob.try_read_many_sync_into(buf, &blob_offsets, Inner::<E, A>::CHUNK_SIZE);
1965 let mut misses = misses.into_iter().peekable();
1966 #[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
1967 for (idx, slice) in buf.chunks_exact(A::SIZE).enumerate() {
1968 if misses.peek() == Some(&idx) {
1969 misses.next();
1970 continue;
1971 }
1972 if let Ok(item) = A::decode(Copying(slice)) {
1975 out[base + idx] = Some(item);
1976 hits += 1;
1977 }
1978 }
1979 }
1980 self.metrics.cache_hits.inc_by(hits);
1981 self.metrics.items_read.inc_by(hits);
1982 out
1983 }
1984}
1985
1986impl<E: Context, A: CodecFixedShared> super::Contiguous for Reader<'_, E, A> {
1987 type Item = A;
1988
1989 fn bounds(&self) -> Range<u64> {
1990 self.bounds.clone()
1991 }
1992
1993 async fn read(&self, pos: u64) -> Result<A, Error> {
1994 self.metrics.read_calls.inc();
1995
1996 if let Some(item) = self.try_read_sync(pos) {
1998 return Ok(item);
1999 }
2000 self.read_miss(pos).await
2001 }
2002
2003 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
2004 if positions.is_empty() {
2005 return Ok(Vec::new());
2006 }
2007 let _timer = self.metrics.read_many_timer();
2008 self.metrics.read_many_calls.inc();
2009 self.read_many_inner(positions).await
2010 }
2011
2012 fn try_read_sync(&self, pos: u64) -> Option<A> {
2013 let (blob, offset) = self.locate(pos).ok()?;
2014 let mut scratch =
2015 Cached::take(&PROBE_SCRATCH, || Ok::<_, ()>(Vec::new()), |_| Ok(())).unwrap();
2016 if scratch.len() < A::SIZE {
2017 scratch.resize(A::SIZE, 0);
2018 }
2019 let buf = &mut scratch[..A::SIZE];
2020 let item = if blob.try_read_sync_into(buf, offset) {
2021 A::decode(Copying(buf)).ok()
2022 } else {
2023 None
2024 };
2025 if item.is_some() {
2026 self.metrics.cache_hits.inc();
2027 self.metrics.items_read.inc();
2028 }
2029 item
2030 }
2031
2032 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
2033 self.probe_items(positions)
2034 }
2035
2036 async fn replay(
2037 &self,
2038 range: Range<u64>,
2039 buffer: NonZeroUsize,
2040 read_options: ReadOptions,
2041 ) -> Result<impl Stream<Item = Result<Batch<A>, Error>> + Send, Error> {
2042 replay_stream(
2043 &self.blobs,
2044 self.bounds.clone(),
2045 self.items_per_blob,
2046 range,
2047 buffer,
2048 read_options,
2049 )
2050 }
2051}
2052
2053impl<E: Context, A: CodecFixedShared> super::Contiguous for Journal<E, A> {
2054 type Item = A;
2055
2056 fn bounds(&self) -> Range<u64> {
2057 self.0.bounds.clone()
2058 }
2059
2060 async fn read(&self, pos: u64) -> Result<A, Error> {
2061 self.0.reader().read(pos).await
2062 }
2063
2064 async fn read_many(&self, positions: &[u64]) -> Result<Vec<A>, Error> {
2065 self.0.reader().read_many(positions).await
2066 }
2067
2068 fn try_read_sync(&self, pos: u64) -> Option<A> {
2069 self.0.reader().try_read_sync(pos)
2070 }
2071
2072 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<A>> {
2073 self.0.reader().probe_items(positions)
2074 }
2075
2076 async fn replay(
2077 &self,
2078 range: Range<u64>,
2079 buffer: NonZeroUsize,
2080 read_options: ReadOptions,
2081 ) -> Result<impl Stream<Item = Result<Batch<A>, Error>> + Send, Error> {
2082 let blobs = self.0.blobs.reader();
2083 replay_stream(
2084 &blobs,
2085 self.0.bounds.clone(),
2086 self.0.items_per_blob,
2087 range,
2088 buffer,
2089 read_options,
2090 )
2091 }
2092}
2093
2094impl<E: Context, A: CodecFixedShared> Mutable for Journal<E, A> {
2095 async fn append(self, item: &Self::Item) -> Result<(Self, u64), Error> {
2096 Self::append(self, item).await
2097 }
2098
2099 async fn append_many(self, items: Many<'_, Self::Item>) -> Result<(Self, u64), Error> {
2100 Self::append_many(self, items).await
2101 }
2102
2103 async fn prune(self, min_position: u64) -> Result<(Self, bool), Error> {
2104 Self::prune(self, min_position).await
2105 }
2106
2107 async fn start_sync(self) -> Result<(Self, Handle<()>), Error> {
2108 Self::start_sync(self).await
2109 }
2110
2111 async fn commit(self) -> Result<Self, Error> {
2112 Self::commit(self).await
2113 }
2114
2115 async fn sync(self) -> Result<Self, Error> {
2116 Self::sync(self).await
2117 }
2118
2119 async fn destroy(self) -> Result<(), Error> {
2120 Self::destroy(self).await
2121 }
2122}
2123
2124#[commonware_macros::stability(ALPHA)]
2125impl<E: Context, A: CodecFixedShared> authenticated::BackingRecovery for Recovery<E, A> {
2126 type Journal = Journal<E, A>;
2127
2128 fn bounds(&self) -> Range<u64> {
2129 self.bounds.clone()
2130 }
2131
2132 async fn read(&self, pos: u64) -> Result<A, Error> {
2133 Self::item(self, pos).await
2134 }
2135
2136 async fn reset(self, size: u64) -> Result<Self, Error> {
2137 Ok(*Box::new(self).clear_to_size(size).await?)
2138 }
2139
2140 async fn finish(self, size: u64) -> Result<Self::Journal, Error> {
2141 Journal(Box::new(Self::publish(self, size).await?))
2142 .commit()
2143 .await
2144 }
2145}
2146
2147#[commonware_macros::stability(ALPHA)]
2148impl<E: Context, A: CodecFixedShared> authenticated::Backing<E> for Journal<E, A> {
2149 type Recovery = Recovery<E, A>;
2150
2151 async fn recover(
2152 context: E,
2153 cfg: Self::Config,
2154 max_size: Option<u64>,
2155 ) -> Result<Self::Recovery, Error> {
2156 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
2157 Recovery::open(context, cfg, checkpoint, max_size).await
2158 }
2159
2160 async fn clear(context: E, cfg: Self::Config, size: u64) -> Result<Self::Recovery, Error> {
2161 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
2162
2163 if checkpoint.clear_target().is_none() {
2166 Partition::select(&context, &cfg.partition).await?;
2167 }
2168 Recovery::open_cleared(context, cfg, checkpoint, size, || async { Ok(()) }).await
2169 }
2170
2171 async fn span(context: E, cfg: &Self::Config) -> Result<Range<u64>, Error> {
2172 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
2173 match checkpoint.clear_target() {
2174 Some(target) => Ok(target..target),
2175 None => Recovery::<E, A>::span(&context, cfg, &checkpoint).await,
2176 }
2177 }
2178
2179 type Config = Config;
2180}
2181
2182#[cfg(test)]
2183impl<E: crate::Context, A: CodecFixedShared> Inner<E, A> {
2184 pub(super) fn recovery_watermark(&self) -> u64 {
2186 self.checkpoint
2187 .watermark()
2188 .expect("recovery watermark must exist after init")
2189 }
2190
2191 pub(super) async fn test_truncate(self: Box<Self>, cap: u64) -> Result<Box<Self>, Error> {
2193 self.sync().await?.test_reopen_at_most(cap).await
2194 }
2195
2196 pub(super) async fn test_reopen_at_most(self: Box<Self>, cap: u64) -> Result<Box<Self>, Error> {
2199 let (context, name, page_cache, write_buffer) = self.blobs.test_configuration();
2200 let cfg = Config {
2201 partition: name.strip_suffix("-blobs").unwrap().to_owned(),
2202 items_per_blob: self.items_per_blob,
2203 page_cache,
2204 write_buffer,
2205 replay_buffer: write_buffer,
2206 };
2207 drop(self);
2208 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition).await?;
2209 let recovery = Recovery::<E, A>::open(context, cfg, checkpoint, Some(cap)).await?;
2210 Ok(Box::new(recovery.publish(cap).await?))
2211 }
2212
2213 pub(crate) const fn test_oldest_blob(&self) -> Option<u64> {
2215 Some(self.blobs.oldest_blob_index())
2216 }
2217
2218 pub(crate) fn test_newest_blob(&self) -> Option<u64> {
2220 Some(self.blobs.tail_blob_index())
2221 }
2222
2223 pub(crate) async fn test_sync_blob(mut self: Box<Self>, blob: u64) -> Result<Box<Self>, Error> {
2225 self.blobs = self.blobs.sync_blob(blob).await?;
2226 Ok(self)
2227 }
2228
2229 pub(crate) async fn test_set_recovery_watermark(
2231 mut self: Box<Self>,
2232 watermark: u64,
2233 ) -> Result<Box<Self>, Error> {
2234 self.checkpoint.set_watermark(Some(watermark));
2235 self.checkpoint = self.checkpoint.sync().await?;
2236 Ok(self)
2237 }
2238
2239 pub(crate) async fn test_stage_clear(
2241 context: E,
2242 partition: &str,
2243 target: u64,
2244 ) -> Result<(), Error> {
2245 let checkpoint = Checkpoint::open(context, partition).await?;
2246 checkpoint.stage_clear(target).await?;
2247 Ok(())
2248 }
2249}
2250
2251#[cfg(test)]
2252impl<E: crate::Context, A: CodecFixedShared> Journal<E, A> {
2253 pub(crate) async fn persisted_watermark(
2255 context: E,
2256 partition: &str,
2257 ) -> Result<Option<u64>, Error> {
2258 Ok(Checkpoint::open(context, partition).await?.watermark())
2259 }
2260
2261 pub(crate) fn test_oldest_blob(&self) -> Option<u64> {
2263 self.0.test_oldest_blob()
2264 }
2265
2266 pub(crate) fn test_newest_blob(&self) -> Option<u64> {
2268 self.0.test_newest_blob()
2269 }
2270
2271 pub(crate) async fn test_sync_blob(mut self, blob: u64) -> Result<Self, Error> {
2273 self.0 = self.0.test_sync_blob(blob).await?;
2274 Ok(self)
2275 }
2276
2277 pub(crate) async fn test_set_recovery_watermark(
2279 mut self,
2280 watermark: u64,
2281 ) -> Result<Self, Error> {
2282 self.0 = self.0.test_set_recovery_watermark(watermark).await?;
2283 Ok(self)
2284 }
2285
2286 pub(crate) async fn test_stage_clear(
2288 context: E,
2289 partition: &str,
2290 target: u64,
2291 ) -> Result<(), Error> {
2292 Inner::<E, A>::test_stage_clear(context, partition, target).await
2293 }
2294}
2295
2296#[cfg(test)]
2297mod tests {
2298 use super::*;
2299 use crate::{
2300 journal::{
2301 authenticated,
2302 contiguous::{
2303 Contiguous as _,
2304 tests::{replay_items, tear_page_rewrite},
2305 },
2306 },
2307 utils::codec::View,
2308 };
2309 use commonware_codec::FixedSize;
2310 use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
2311 use commonware_macros::test_traced;
2312 use commonware_runtime::{
2313 Blob, Error as RuntimeError, Metrics as _, Runner, Spawner as _, Storage, Supervisor as _,
2314 WriteOptions,
2315 buffer::paged::{Writer, corrupt_page},
2316 deterministic::{self, Context},
2317 mocks::{
2318 DelayedSyncContext, PendingSyncs, RecordingContext, WriteFaultContext, WriteFaults,
2319 drive_pending_syncs, fail_pending_syncs, next_pending_sync, release_pending_syncs,
2320 },
2321 };
2322 use commonware_utils::{NZU16, NZU64, NZUsize, probability};
2323 use futures::{StreamExt, pin_mut};
2324 use std::num::NonZeroU16;
2325
2326 const PAGE_SIZE: NonZeroU16 = NZU16!(44);
2327 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
2328
2329 fn test_digest(value: u64) -> Digest {
2331 Sha256::hash(&[&value.to_be_bytes()])
2332 }
2333
2334 fn test_cfg(items_per_blob: NonZeroU64) -> Config {
2335 Config {
2336 partition: "test-partition".into(),
2337 items_per_blob,
2338 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2339 write_buffer: NZUsize!(2048),
2340 replay_buffer: NZUsize!(2048),
2341 }
2342 }
2343
2344 fn blob_partition(cfg: &Config) -> String {
2345 format!("{}-blobs", cfg.partition)
2346 }
2347
2348 #[test]
2349 fn test_unbounded_fixed_repairs_hole_after_full_capacity() {
2350 deterministic::Runner::default().start(|context| async move {
2351 let cache = CacheRef::new(NZU16!(8), NZUsize!(8));
2352 let cfg = Config {
2353 partition: "initialization-extra-tail".into(),
2354 items_per_blob: NZU64!(2),
2355 page_cache: cache.clone(),
2356 write_buffer: NZUsize!(128),
2357 replay_buffer: NZUsize!(128),
2358 };
2359 let (blob, size) = context
2360 .open("initialization-extra-tail-blobs", &0u64.to_be_bytes())
2361 .await
2362 .unwrap();
2363 let mut writer = Writer::new(blob, size, 128, cache).await.unwrap();
2364 (writer, _) = writer.append(&[1; 32]).await.unwrap();
2365 writer = writer.sync().await.unwrap();
2366 drop(writer);
2367 corrupt_page(
2368 &context,
2369 "initialization-extra-tail-blobs",
2370 &0u64.to_be_bytes(),
2371 2,
2372 8,
2373 )
2374 .await;
2375 let result = Journal::<_, u64>::init(context.child("open"), cfg).await;
2376 assert!(
2377 result.is_ok(),
2378 "unbounded recovery must trim a hole after capacity: {result:?}"
2379 );
2380
2381 let journal = result.unwrap();
2382 assert_eq!(journal.bounds(), 0..2);
2383 let item = u64::from_be_bytes([1; 8]);
2384 assert_eq!(journal.read(0).await.unwrap(), item);
2385 assert_eq!(journal.read(1).await.unwrap(), item);
2386 });
2387 }
2388
2389 #[test]
2390 fn test_fixed_bounded_recovery_read_count() {
2391 for count in [257u64, 4097] {
2392 for capacity in [10_000, u64::MAX] {
2393 deterministic::Runner::default().start(|context| async move {
2394 let (context, recordings) = RecordingContext::new(context);
2395 let mut cfg = test_cfg(capacity.try_into().unwrap());
2396 cfg.page_cache = CacheRef::new(NZU16!(256), NZUsize!(3));
2397 cfg.replay_buffer = NZUsize!(1);
2398 let mut journal = Journal::<_, u64>::init(context.child("seed"), cfg.clone())
2399 .await
2400 .unwrap();
2401 for value in 0..count {
2402 (journal, _) = journal.append(&value).await.unwrap();
2403 }
2404 drop(journal.sync().await.unwrap());
2405 for cap in [
2406 None,
2407 Some(33),
2408 Some(32),
2409 Some(count),
2410 Some(count + 1),
2411 Some(u64::MAX),
2412 ] {
2413 let checkpoint =
2414 Checkpoint::open(context.child("checkpoint"), &cfg.partition)
2415 .await
2416 .unwrap();
2417 assert_eq!(checkpoint.watermark(), Some(count));
2418 recordings.clear();
2419 let recovery = Recovery::<_, u64>::open(
2420 context.child("recovery"),
2421 cfg.clone(),
2422 checkpoint,
2423 cap,
2424 )
2425 .await
2426 .unwrap();
2427 assert_eq!(recovery.size(), count.min(cap.unwrap_or(count)));
2428 assert_eq!(recovery.pending.get(&0).unwrap().size(), count * 8);
2429 let expected = if cap == Some(32) { 1 } else { 2 };
2430 assert_eq!(
2431 recordings.snapshot().reads.len(),
2432 expected,
2433 "count={count}, capacity={capacity}, cap={cap:?}"
2434 );
2435 }
2436 });
2437 }
2438 }
2439 }
2440
2441 #[test]
2442 fn test_fixed_recovery_large_capacity() {
2443 for count in [0, 1] {
2444 for cap in [
2445 None,
2446 Some(0),
2447 Some(1),
2448 Some(u64::MAX / 8 + 1),
2449 Some(u64::MAX),
2450 ] {
2451 deterministic::Runner::default().start(|context| async move {
2452 let cfg = test_cfg(NZU64!(u64::MAX));
2453 let mut journal = Journal::<_, u64>::init(context.child("seed"), cfg.clone())
2454 .await
2455 .unwrap();
2456 for value in 0..count {
2457 (journal, _) = journal.append(&value).await.unwrap();
2458 }
2459 drop(journal.sync().await.unwrap());
2460 let journal = match cap {
2461 None => Journal::<_, u64>::init(context.child("reopen"), cfg).await,
2462 Some(cap) => {
2463 Journal::<_, u64>::init_at_most(context.child("reopen"), cfg, cap).await
2464 }
2465 }
2466 .unwrap();
2467 let retained = count.min(cap.unwrap_or(u64::MAX));
2468 assert_eq!(journal.bounds(), 0..retained);
2469 if retained != 0 {
2470 assert_eq!(journal.read(0).await.unwrap(), 0);
2471 }
2472 });
2473 }
2474 }
2475 }
2476
2477 #[test_traced]
2478 fn test_fixed_append_buffer_fallback_rotation_and_snapshot() {
2479 for write_buffer in [NZUsize!(44), NZUsize!(2048)] {
2480 deterministic::Runner::default().start(|context| async move {
2481 let mut cfg = test_cfg(NZU64!(7));
2482 cfg.write_buffer = write_buffer;
2483 let mut journal = Journal::<_, Digest>::init(context.child("initial"), cfg.clone())
2484 .await
2485 .unwrap();
2486 for i in 0..3 {
2487 let position;
2488 (journal, position) = journal.append(&test_digest(i)).await.unwrap();
2489 assert_eq!(position, i);
2490 }
2491 let snapshot;
2492 (journal, snapshot) = journal.snapshot().await.unwrap();
2493
2494 for i in 3..20 {
2496 let position;
2497 if i % 3 == 0 {
2498 let item = test_digest(i);
2499 let prepared = journal.prepare_append(Many::Flat(&[item]));
2500 (journal, position) = journal.append_prepared(prepared).await.unwrap();
2501 } else {
2502 (journal, position) = journal.append(&test_digest(i)).await.unwrap();
2503 }
2504 assert_eq!(position, i);
2505 assert_eq!(journal.size(), i + 1);
2506 }
2507 for i in 0..3 {
2508 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
2509 }
2510 journal = journal.sync().await.unwrap();
2511 drop(snapshot);
2512 drop(journal);
2513
2514 let journal = Journal::<_, Digest>::init(context, cfg).await.unwrap();
2515 assert_eq!(journal.size(), 20);
2516 for i in 0..20 {
2517 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
2518 }
2519 journal.destroy().await.unwrap();
2520 });
2521 }
2522 }
2523
2524 #[test_traced]
2525 fn test_fixed_cached_read_preserves_owned_byte_fields() {
2526 deterministic::Runner::default().start(|context| async move {
2527 let cfg = test_cfg(NZU64!(3));
2528 let mut journal = Journal::init(context, cfg).await.unwrap();
2529 (journal, _) = journal.append(&View::new(7)).await.unwrap();
2530 let decoded = journal.try_read_sync(0).unwrap();
2531 (journal, _) = journal.append(&View::new(8)).await.unwrap();
2532 let next = journal.try_read_sync(1).unwrap();
2533 journal.destroy().await.unwrap();
2534 assert_eq!(decoded.bytes.as_ref(), &7u64.to_be_bytes());
2535 assert_eq!(next.bytes.as_ref(), &8u64.to_be_bytes());
2536 });
2537 }
2538
2539 #[test_traced]
2540 fn test_fixed_batch_read_preserves_owned_byte_fields() {
2541 deterministic::Runner::default().start(|context| async move {
2542 let cfg = test_cfg(NZU64!(3));
2543 let mut journal = Journal::init(context, cfg).await.unwrap();
2544 for i in 0..5 {
2545 (journal, _) = journal.append(&View::new(i)).await.unwrap();
2546 }
2547 let decoded = journal.read_many(&[0, 2, 3, 4]).await.unwrap();
2548 let probed = journal.try_read_many_sync(&[0, 2, 3, 4]);
2549 journal.destroy().await.unwrap();
2550 for (value, expected) in decoded.iter().zip([0u64, 2, 3, 4]) {
2551 assert_eq!(value.bytes.as_ref(), &expected.to_be_bytes());
2552 value.assert_shared();
2553 }
2554 for (value, expected) in probed.iter().zip([0u64, 2, 3, 4]) {
2555 let value = value.as_ref().expect("probe should hit after read_many");
2556 assert_eq!(value.bytes.as_ref(), &expected.to_be_bytes());
2557 }
2558 });
2559 }
2560
2561 #[test]
2562 fn test_bounded_empty_prefix_at_maximum_position() {
2563 deterministic::Runner::default().start(|context| async move {
2564 let cfg = test_cfg(NZU64!(1));
2565 let mut journal =
2566 Journal::<_, u64>::init_at_size(context.child("seed"), cfg.clone(), u64::MAX - 1)
2567 .await
2568 .unwrap();
2569 (journal, _) = journal.append(&99).await.unwrap();
2570 (journal, _) = journal.prune(u64::MAX).await.unwrap();
2571 _ = journal.sync().await.unwrap();
2572 let journal =
2573 Journal::<_, u64>::init_at_most(context.child("capped"), cfg.clone(), u64::MAX)
2574 .await
2575 .unwrap();
2576 assert_eq!(journal.bounds(), u64::MAX..u64::MAX);
2577 drop(journal);
2578 let journal = Journal::<_, u64>::init(context.child("ordinary"), cfg)
2579 .await
2580 .unwrap();
2581 assert_eq!(journal.bounds(), u64::MAX..u64::MAX);
2582 });
2583 }
2584
2585 #[test]
2586 fn test_bounded_empty_prefix_interrupted_repair() {
2587 deterministic::Runner::default().start(|context| async move {
2588 let cfg = test_cfg(NZU64!(5));
2589 let mut journal =
2590 Journal::<_, Digest>::init_at_size(context.child("seed"), cfg.clone(), 7)
2591 .await
2592 .unwrap();
2593 for i in 7..13 {
2594 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2595 }
2596 _ = journal.sync().await.unwrap();
2597 let checkpoint = Checkpoint::open(context.child("checkpoint"), &cfg.partition)
2598 .await
2599 .unwrap();
2600 let recovery =
2601 Recovery::<_, Digest>::open(context.child("cap"), cfg.clone(), checkpoint, Some(7))
2602 .await
2603 .unwrap();
2604
2605 drop(recovery.repair_to(7).await.unwrap());
2607 let journal = Journal::<_, Digest>::init(context.child("retry"), cfg)
2608 .await
2609 .unwrap();
2610 assert_eq!(journal.bounds(), 7..7);
2611 });
2612 }
2613
2614 #[test]
2615 fn test_recovery_rebuilds_pending_gap() {
2616 deterministic::Runner::default().start(|context| async move {
2617 let mut cfg = test_cfg(NZU64!(5));
2620 cfg.write_buffer = NZUsize!(88);
2621 let mut journal = Journal::<_, Digest>::init(context.child("seed"), cfg.clone())
2622 .await
2623 .unwrap();
2624 for i in 0..13 {
2625 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
2626 }
2627 for blob in 0..3 {
2628 journal = journal.test_sync_blob(blob).await.unwrap();
2629 }
2630 drop(journal);
2631
2632 let (blob, _) = context
2634 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
2635 .await
2636 .unwrap();
2637 blob.resize(0).await.unwrap();
2638 blob.sync().await.unwrap();
2639 drop(blob);
2640 let checkpoint = Checkpoint::open(context.child("checkpoint"), &cfg.partition)
2641 .await
2642 .unwrap();
2643 let mut recovery = Box::new(
2644 Recovery::<_, Digest>::open(context.child("repair"), cfg.clone(), checkpoint, None)
2645 .await
2646 .unwrap(),
2647 );
2648 assert_eq!(recovery.size(), 0);
2649 assert_eq!(recovery.recovery_watermark(), 0);
2650 recovery = recovery.truncate(0).await.unwrap();
2651
2652 for i in 0..13 {
2655 recovery = recovery.append(&test_digest(100 + i)).await.unwrap();
2656 }
2657 let journal = recovery.publish(13).await.unwrap();
2658 _ = Box::new(journal).sync().await.unwrap();
2659 let journal = Journal::<_, Digest>::init(context.child("retry"), cfg)
2660 .await
2661 .unwrap();
2662 assert_eq!(journal.bounds(), 0..13);
2663 for value in 0..13 {
2664 assert_eq!(journal.read(value).await.unwrap(), test_digest(100 + value));
2665 }
2666 });
2667 }
2668
2669 #[test]
2672 fn test_bounded_publish_syncs_rebuilt_tail_before_watermark_raise() {
2673 let ((), checkpoint) =
2674 deterministic::Runner::default().start_and_recover(|context| async move {
2675 let cfg = test_cfg(NZU64!(5));
2676 let checkpoint = Checkpoint::open(context.child("checkpoint"), &cfg.partition)
2677 .await
2678 .unwrap();
2679 let mut recovery = Box::new(
2680 Recovery::<_, Digest>::open(
2681 context.child("rebuild"),
2682 cfg.clone(),
2683 checkpoint,
2684 Some(7),
2685 )
2686 .await
2687 .unwrap(),
2688 );
2689 recovery = recovery.truncate(0).await.unwrap();
2690 for i in 0..7 {
2691 recovery = recovery.append(&test_digest(i)).await.unwrap();
2692 }
2693 let journal = recovery.publish(7).await.unwrap();
2694 assert_eq!(journal.size(), 7);
2695 assert_eq!(journal.recovery_watermark(), 7);
2696 drop(journal);
2697 });
2698 deterministic::Runner::from(checkpoint).start(|context| async move {
2699 let cfg = test_cfg(NZU64!(5));
2700 let journal = Journal::<_, Digest>::init(context.child("reopen"), cfg)
2701 .await
2702 .unwrap();
2703 assert_eq!(journal.bounds(), 0..7);
2704 for i in 0..7 {
2705 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
2706 }
2707 });
2708 }
2709
2710 #[test]
2711 fn test_bounded_initialization_retries_after_storage_faults() {
2712 for start in [0, 7] {
2713 for kind in 0..3 {
2714 for numerator in [1, 3, 7, 10] {
2715 let (succeeded, checkpoint) = deterministic::Runner::default()
2716 .start_and_recover(|context| async move {
2717 let cfg = test_cfg(NZU64!(5));
2718 let mut journal = Journal::<_, u64>::init_at_size(
2719 context.child("seed"),
2720 cfg.clone(),
2721 start,
2722 )
2723 .await
2724 .unwrap();
2725 for value in start..13 {
2726 (journal, _) = journal.append(&value).await.unwrap();
2727 }
2728 _ = journal.sync().await.unwrap();
2729 let rate = commonware_utils::Probability::new(numerator, 10).unwrap();
2730 *context.storage_fault_config().write() = deterministic::FaultConfig {
2731 sync_rate: (kind == 0).then_some(rate),
2732 remove_rate: (kind == 1).then_some(rate),
2733 resize_rate: (kind == 2).then_some(deterministic::ResizeConfig {
2734 failure_rate: rate,
2735 partial_rate: rate,
2736 }),
2737 ..Default::default()
2738 };
2739
2740 Journal::<_, u64>::init_at_most(context.child("faulted"), cfg, 7)
2743 .await
2744 .is_ok()
2745 });
2746 deterministic::Runner::from(checkpoint).start(|context| async move {
2747 *context.storage_fault_config().write() =
2748 deterministic::FaultConfig::default();
2749 let cfg = test_cfg(NZU64!(5));
2750 if succeeded {
2751 let journal =
2752 Journal::<_, u64>::init(context.child("ordinary"), cfg.clone())
2753 .await
2754 .unwrap();
2755 assert_eq!(journal.bounds(), start..7);
2756 drop(journal);
2757 }
2758 let journal =
2759 Journal::<_, u64>::init_at_most(context.child("retry"), cfg.clone(), 7)
2760 .await
2761 .unwrap();
2762 assert_eq!(journal.bounds(), start..7);
2763 for value in start..7 {
2764 assert_eq!(journal.read(value).await.unwrap(), value);
2765 }
2766 drop(journal);
2767 let mut journal =
2768 Journal::<_, u64>::init(context.child("restart"), cfg.clone())
2769 .await
2770 .unwrap();
2771 assert_eq!(journal.bounds(), start..7);
2772 (journal, _) = journal.append(&99).await.unwrap();
2773 _ = journal.sync().await.unwrap();
2774 let journal = Journal::<_, u64>::init(context.child("verify"), cfg)
2775 .await
2776 .unwrap();
2777 assert_eq!(journal.bounds(), start..8);
2778 assert_eq!(journal.read(7).await.unwrap(), 99);
2779 });
2780 }
2781 }
2782 }
2783 }
2784
2785 #[test]
2786 fn test_init_at_most_restart_and_append() {
2787 for start in [0, 7] {
2788 for cap in [0, 1, 5, 7, 8, 10, 13, 20, u64::MAX] {
2789 deterministic::Runner::default().start(|context| async move {
2790 let cfg = test_cfg(NZU64!(5));
2791 let mut journal = Journal::<_, u64>::init_at_size(
2792 context.child("storage"),
2793 cfg.clone(),
2794 start,
2795 )
2796 .await
2797 .unwrap();
2798 for value in start..13 {
2799 (journal, _) = journal.append(&value).await.unwrap();
2800 }
2801 journal = journal.sync().await.unwrap();
2802 drop(journal);
2803
2804 let opened =
2805 Journal::<_, u64>::init_at_most(context.child("storage"), cfg.clone(), cap)
2806 .await;
2807 if cap < start {
2808 assert!(matches!(opened, Err(Error::ItemPruned(value)) if value == cap));
2809 let journal = Journal::<_, u64>::init(context.child("storage"), cfg)
2810 .await
2811 .unwrap();
2812 assert_eq!(journal.bounds(), start..13);
2813 return;
2814 }
2815 let end = cap.min(13);
2816 let journal = opened.unwrap();
2817 assert_eq!(journal.bounds(), start..end);
2818 for value in start..end {
2819 assert_eq!(journal.read(value).await.unwrap(), value);
2820 }
2821 drop(journal);
2822
2823 let mut journal =
2825 Journal::<_, u64>::init(context.child("storage"), cfg.clone())
2826 .await
2827 .unwrap();
2828 assert_eq!(journal.bounds(), start..end);
2829 for value in end..end + 8 {
2830 let pos;
2831 (journal, pos) = journal.append(&(value + 100)).await.unwrap();
2832 assert_eq!(pos, value);
2833 }
2834 journal = journal.sync().await.unwrap();
2835 drop(journal);
2836 let journal = Journal::<_, u64>::init(context.child("storage"), cfg)
2837 .await
2838 .unwrap();
2839 for value in start..end + 8 {
2840 let expected = if value < end { value } else { value + 100 };
2841 assert_eq!(journal.read(value).await.unwrap(), expected);
2842 }
2843 });
2844 }
2845 }
2846 }
2847
2848 #[test]
2849 fn test_init_at_most_skips_discarded_blobs() {
2850 deterministic::Runner::default().start(|context| async move {
2851 let cfg = test_cfg(NZU64!(5));
2852 let mut journal = Journal::<_, u64>::init(context.child("storage"), cfg.clone())
2853 .await
2854 .unwrap();
2855 for value in 0..23 {
2856 (journal, _) = journal.append(&value).await.unwrap();
2857 }
2858 journal = journal.sync().await.unwrap();
2859 drop(journal);
2860 let (blob, _) = context
2861 .open(&blob_partition(&cfg), &3u64.to_be_bytes())
2862 .await
2863 .unwrap();
2864 blob.resize(1).await.unwrap();
2865 blob.sync().await.unwrap();
2866 drop(blob);
2867 let journal = Journal::<_, u64>::init_at_most(context.child("storage"), cfg.clone(), 7)
2868 .await
2869 .unwrap();
2870 assert_eq!(journal.bounds(), 0..7);
2871 drop(journal);
2872 let journal = Journal::<_, u64>::init(context.child("storage"), cfg)
2873 .await
2874 .unwrap();
2875 assert_eq!(journal.bounds(), 0..7);
2876 });
2877 }
2878
2879 #[test]
2880 fn test_start_sync_keeps_predecessor_sync() {
2881 let executor = deterministic::Runner::default();
2882 executor.start(|context| async move {
2883 let cfg = test_cfg(NZU64!(3));
2884 let mut journal = Box::new(Inner::<_, u64>::init(context, cfg).await.unwrap());
2885
2886 (journal, _) = journal
2888 .append_many(Many::Flat(&[1, 2, 3, 4]))
2889 .await
2890 .unwrap();
2891 assert!(journal.blobs.has_tail_predecessor_sync());
2892
2893 let (journal, handle) = journal.start_sync().await.unwrap();
2895 assert!(journal.blobs.has_tail_predecessor_sync());
2896 handle.await.unwrap();
2897 assert!(journal.blobs.has_tail_predecessor_sync());
2898
2899 journal.destroy().await.unwrap();
2900 });
2901 }
2902
2903 #[test_traced]
2904 fn test_start_sync_advances_watermark_lagged() {
2905 let executor = deterministic::Runner::default();
2906 executor.start(|context| async move {
2907 let pending = PendingSyncs::default();
2908 let cfg = test_cfg(NZU64!(100));
2909 let make = |pending: PendingSyncs| {
2910 Inner::<_, u64>::init(
2911 DelayedSyncContext {
2912 inner: context.child("journal"),
2913 pending,
2914 },
2915 cfg.clone(),
2916 )
2917 };
2918 let mut journal = Box::new(make(pending.clone()).await.unwrap());
2919
2920 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2922 let (mut journal, h1) = journal.start_sync().await.unwrap();
2923 assert_eq!(journal.recovery_watermark(), 0);
2924
2925 release_pending_syncs(&pending);
2926 h1.await.unwrap();
2927
2928 (journal, _) = journal.append(&4).await.unwrap();
2931 let (journal, h2) = journal.start_sync().await.unwrap();
2932 assert_eq!(journal.recovery_watermark(), 3);
2933 drive_pending_syncs(&pending, h2).await.unwrap();
2934
2935 let (journal, h3) = drive_pending_syncs(&pending, journal.start_sync())
2937 .await
2938 .unwrap();
2939 assert_eq!(journal.recovery_watermark(), 4);
2940 drive_pending_syncs(&pending, h3).await.unwrap();
2941
2942 pending.unblock();
2944 drop(journal);
2945 let journal = make(pending.clone()).await.unwrap();
2946 assert_eq!(journal.recovery_watermark(), 4);
2947 assert_eq!(journal.bounds, 0..4);
2948 journal.destroy().await.unwrap();
2949 });
2950 }
2951
2952 #[test_traced]
2955 fn test_start_sync_failure_blocks_watermark() {
2956 let executor = deterministic::Runner::default();
2957 executor.start(|context| async move {
2958 let pending = PendingSyncs::default();
2959 let cfg = test_cfg(NZU64!(3));
2960 let mut journal = Box::new(
2961 Inner::<_, u64>::init(
2962 DelayedSyncContext {
2963 inner: context.child("journal"),
2964 pending: pending.clone(),
2965 },
2966 cfg,
2967 )
2968 .await
2969 .unwrap(),
2970 );
2971
2972 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2975 let (mut journal, h1) = journal.start_sync().await.unwrap();
2976
2977 next_pending_sync(&pending)
2979 .release
2980 .send(Err(RuntimeError::Io(
2981 std::io::Error::other("injected sync failure").into(),
2982 )))
2983 .unwrap();
2984 release_pending_syncs(&pending);
2985 assert!(h1.await.is_err());
2986
2987 assert_eq!(journal.barrier.boundary(), 0);
2990 let (mut journal, h2) = journal.start_sync().await.unwrap();
2991 assert_eq!(journal.recovery_watermark(), 0);
2992 assert!(h2.await.is_err());
2993
2994 (journal, _) = journal.append(&4).await.unwrap();
2996 let (journal, _) = journal.start_sync().await.unwrap();
2997 fail_pending_syncs(&pending);
2998 assert!(journal.start_sync().await.is_err());
2999 });
3000 }
3001
3002 #[test_traced]
3003 fn test_truncate_drains_parked_watermark_advance() {
3004 let executor = deterministic::Runner::default();
3005 executor.start(|context| async move {
3006 let pending = PendingSyncs::default();
3007 let cfg = test_cfg(NZU64!(100));
3008 let make = |pending: PendingSyncs| {
3009 Inner::<_, u64>::init(
3010 DelayedSyncContext {
3011 inner: context.child("journal"),
3012 pending,
3013 },
3014 cfg.clone(),
3015 )
3016 };
3017 let mut journal = Box::new(make(pending.clone()).await.unwrap());
3018
3019 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
3020 let (mut journal, h1) = journal.start_sync().await.unwrap();
3021 release_pending_syncs(&pending);
3022 h1.await.unwrap();
3023
3024 (journal, _) = journal.append(&4).await.unwrap();
3026 let (journal, h2) = journal.start_sync().await.unwrap();
3027 assert_eq!(journal.recovery_watermark(), 3);
3028
3029 let journal = drive_pending_syncs(&pending, journal.test_truncate(2))
3031 .await
3032 .unwrap();
3033 assert_eq!(journal.recovery_watermark(), 2);
3034 drop(h2);
3035
3036 pending.unblock();
3038 drop(journal);
3039 let journal = make(pending.clone()).await.unwrap();
3040 assert_eq!(journal.recovery_watermark(), 2);
3041 assert_eq!(journal.bounds, 0..2);
3042 journal.destroy().await.unwrap();
3043 });
3044 }
3045
3046 #[test_traced]
3047 fn test_start_sync_watermark_advance_inline_failure() {
3048 let executor = deterministic::Runner::default();
3049 executor.start(|context| async move {
3050 let faults = WriteFaults::default();
3051 let cfg = test_cfg(NZU64!(100));
3052 let make = |faults: WriteFaults| {
3053 Inner::<_, u64>::init(
3054 WriteFaultContext {
3055 inner: context.child("journal"),
3056 faults,
3057 },
3058 cfg.clone(),
3059 )
3060 };
3061 let mut journal = Box::new(make(faults.clone()).await.unwrap());
3062
3063 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
3066 let (mut journal, h1) = journal.start_sync().await.unwrap();
3067 h1.await.unwrap();
3068 (journal, _) = journal.append(&4).await.unwrap();
3069 let journal = journal.commit().await.unwrap();
3070
3071 faults.arm();
3074 assert!(journal.start_sync().await.is_err());
3075 faults.disarm();
3076
3077 let journal = Box::new(make(faults).await.unwrap());
3080 assert_eq!(journal.bounds, 0..4);
3081 let journal = journal.sync().await.unwrap();
3082 assert_eq!(journal.recovery_watermark(), 4);
3083 journal.destroy().await.unwrap();
3084 });
3085 }
3086
3087 #[test_traced]
3088 fn test_start_sync_watermark_advance_deferred_failure() {
3089 let executor = deterministic::Runner::default();
3090 executor.start(|context| async move {
3091 let pending = PendingSyncs::default();
3092 let cfg = test_cfg(NZU64!(100));
3093 let mut journal = Box::new(
3094 Inner::<_, u64>::init(
3095 DelayedSyncContext {
3096 inner: context.child("journal"),
3097 pending: pending.clone(),
3098 },
3099 cfg,
3100 )
3101 .await
3102 .unwrap(),
3103 );
3104
3105 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
3106 let (journal, h1) = journal.start_sync().await.unwrap();
3107 release_pending_syncs(&pending);
3108 h1.await.unwrap();
3109
3110 let (mut journal, h2) = journal.start_sync().await.unwrap();
3113 fail_pending_syncs(&pending);
3114 assert!(h2.await.is_err());
3115
3116 (journal, _) = journal.append(&4).await.unwrap();
3119 let (journal, h3) = journal.start_sync().await.unwrap();
3120 drive_pending_syncs(&pending, h3).await.unwrap();
3121
3122 let journal = drive_pending_syncs(&pending, journal.commit())
3126 .await
3127 .unwrap();
3128 assert!(drive_pending_syncs(&pending, journal.sync()).await.is_err());
3129 });
3130 }
3131
3132 #[test_traced]
3133 fn test_truncate_watermark_lowering_failure_keeps_blobs() {
3134 let executor = deterministic::Runner::default();
3135 executor.start(|context| async move {
3136 let faults = WriteFaults::default();
3137
3138 let cfg = Config {
3141 page_cache: CacheRef::new(NZU16!(16), PAGE_CACHE_SIZE),
3142 ..test_cfg(NZU64!(100))
3143 };
3144 let make = |faults: WriteFaults| {
3145 Inner::<_, u64>::init(
3146 WriteFaultContext {
3147 inner: context.child("journal"),
3148 faults,
3149 },
3150 cfg.clone(),
3151 )
3152 };
3153 let mut journal = Box::new(make(faults.clone()).await.unwrap());
3154 (journal, _) = journal
3155 .append_many(Many::Flat(&[1, 2, 3, 4]))
3156 .await
3157 .unwrap();
3158 let journal = journal.sync().await.unwrap();
3159 assert_eq!(journal.recovery_watermark(), 4);
3160
3161 faults.arm();
3164 assert!(journal.test_reopen_at_most(2).await.is_err());
3165 faults.disarm();
3166
3167 let journal = make(faults).await.unwrap();
3168 assert_eq!(journal.recovery_watermark(), 4);
3169 assert_eq!(journal.bounds, 0..4);
3170 journal.destroy().await.unwrap();
3171 });
3172 }
3173
3174 #[test_traced]
3175 fn test_truncate_truncates_durable_size() {
3176 let executor = deterministic::Runner::default();
3177 executor.start(|context| async move {
3178 let pending = PendingSyncs::default();
3179 let cfg = test_cfg(NZU64!(100));
3180 let mut journal = Box::new(
3181 Inner::<_, u64>::init(
3182 DelayedSyncContext {
3183 inner: context.child("journal"),
3184 pending: pending.clone(),
3185 },
3186 cfg,
3187 )
3188 .await
3189 .unwrap(),
3190 );
3191 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
3192 let journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
3193 assert_eq!(journal.recovery_watermark(), 3);
3194
3195 let mut journal = drive_pending_syncs(&pending, journal.test_truncate(2))
3198 .await
3199 .unwrap();
3200 (journal, _) = journal.append(&9).await.unwrap();
3201 let (journal, handle) = journal.start_sync().await.unwrap();
3202 assert_eq!(journal.recovery_watermark(), 2);
3203
3204 pending.unblock();
3205 handle.await.unwrap();
3206 journal.destroy().await.unwrap();
3207 });
3208 }
3209
3210 #[test_traced]
3212 fn test_fixed_start_sync_flush_failure_returns_err() {
3213 let executor = deterministic::Runner::default();
3214 executor.start(|context| async move {
3215 let cfg = test_cfg(NZU64!(3));
3216 let mut journal = Box::new(
3217 Inner::<_, u64>::init(context.child("journal"), cfg)
3218 .await
3219 .unwrap(),
3220 );
3221
3222 (journal, _) = journal.append(&0).await.unwrap();
3224
3225 *context.storage_fault_config().write() = deterministic::FaultConfig {
3227 write_rate: Some(deterministic::WriteConfig {
3228 failure_rate: probability!(1.0),
3229 retention_rate: probability!(0.0),
3230 mode: deterministic::PartialWriteMode::Prefix,
3231 }),
3232 ..Default::default()
3233 };
3234 assert!(matches!(journal.start_sync().await, Err(Error::Runtime(_))));
3235 });
3236 }
3237
3238 enum Boundary {
3240 Rollover,
3241 Clear,
3242 Destroy,
3243 }
3244
3245 enum FailedSync {
3247 Tail,
3249 Predecessor,
3251 }
3252
3253 #[rstest::rstest]
3255 #[test_traced]
3256 fn test_fixed_dropped_failed_start_sync_blocks_blob_changes(
3257 #[values(FailedSync::Tail, FailedSync::Predecessor)] failed: FailedSync,
3258 #[values(Boundary::Rollover, Boundary::Clear, Boundary::Destroy)] boundary: Boundary,
3259 ) {
3260 let executor = deterministic::Runner::default();
3261 executor.start(|context| async move {
3262 let cfg = test_cfg(NZU64!(3));
3263 let data_partition = blob_partition(&cfg);
3264 let mut journal = Box::new(
3265 Inner::<_, u64>::init(context.child("journal"), cfg)
3266 .await
3267 .unwrap(),
3268 );
3269 let fail_syncs = |rate| {
3270 *context.storage_fault_config().write() = deterministic::FaultConfig {
3271 sync_rate: Some(rate),
3272 ..Default::default()
3273 };
3274 };
3275
3276 let blobs = match failed {
3279 FailedSync::Tail => {
3280 (journal, _) = journal.append(&0).await.unwrap();
3281 fail_syncs(probability!(1.0));
3282 let handle;
3283 (journal, handle) = journal.start_sync().await.unwrap();
3284 drop(handle);
3285 1
3286 }
3287 FailedSync::Predecessor => {
3288 (journal, _) = journal.append_many(Many::Flat(&[0, 1])).await.unwrap();
3289 fail_syncs(probability!(1.0));
3290 (journal, _) = journal.append(&2).await.unwrap();
3291 2
3292 }
3293 };
3294 fail_syncs(probability!(0.0));
3295
3296 let size = journal.size();
3299 let result = match boundary {
3300 Boundary::Rollover => journal
3301 .append_many(Many::Flat(&[size, size + 1, size + 2]))
3302 .await
3303 .map(|_| ()),
3304 Boundary::Clear => journal.clear_to_size(size).await.map(|_| ()),
3305 Boundary::Destroy => journal.destroy().await,
3306 };
3307 assert!(matches!(result, Err(Error::Runtime(_))));
3308 let mut names = scan_partition(&context, &data_partition).await;
3309 names.sort();
3310 let expected: Vec<_> = (0..blobs)
3311 .map(|blob: u64| blob.to_be_bytes().to_vec())
3312 .collect();
3313 assert_eq!(names, expected);
3314 });
3315 }
3316
3317 #[test_traced]
3319 fn test_fixed_dropped_failed_start_sync_surfaces_before_rollover() {
3320 let executor = deterministic::Runner::default();
3321 executor.start(|context| async move {
3322 let cfg = test_cfg(NZU64!(1000));
3323 let mut journal = Box::new(
3324 Inner::<_, u64>::init(context.child("journal"), cfg)
3325 .await
3326 .unwrap(),
3327 );
3328
3329 (journal, _) = journal.append(&0).await.unwrap();
3332 journal = journal.sync().await.unwrap();
3333 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
3334 *context.storage_fault_config().write() = deterministic::FaultConfig {
3335 sync_rate: Some(probability!(1.0)),
3336 ..Default::default()
3337 };
3338 let (journal, handle) = journal.start_sync().await.unwrap();
3339 drop(handle);
3340 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
3341
3342 let overflow = vec![0u64; 300];
3345 assert!(matches!(
3346 journal.append_many(Many::Flat(&overflow[..])).await,
3347 Err(Error::Runtime(_))
3348 ));
3349 });
3350 }
3351
3352 fn counter(buffer: &str, name: &str) -> u64 {
3354 buffer
3355 .lines()
3356 .find(|l| l.contains(name) && !l.starts_with('#'))
3357 .and_then(|l| l.split_whitespace().last())
3358 .and_then(|v| v.parse().ok())
3359 .expect("counter missing")
3360 }
3361
3362 async fn scan_partition(context: &Context, partition: &str) -> Vec<Vec<u8>> {
3363 match context.scan(partition).await {
3364 Ok(blobs) => blobs,
3365 Err(RuntimeError::PartitionMissing(_)) => Vec::new(),
3366 Err(err) => panic!("Failed to scan partition {partition}: {err}"),
3367 }
3368 }
3369
3370 #[test_traced]
3371 fn test_fixed_journal_init_conflicting_partitions() {
3372 let executor = deterministic::Runner::default();
3373 executor.start(|context| async move {
3374 let cfg = test_cfg(NZU64!(2));
3375 let legacy_partition = cfg.partition.clone();
3376 let blobs_partition = blob_partition(&cfg);
3377
3378 let (legacy_blob, _) = context
3379 .open(&legacy_partition, &0u64.to_be_bytes())
3380 .await
3381 .expect("Failed to open legacy blob");
3382 legacy_blob
3383 .write_at(0, vec![0u8; 1], WriteOptions::SYNC)
3384 .await
3385 .expect("Failed to write legacy blob");
3386
3387 let (new_blob, _) = context
3388 .open(&blobs_partition, &0u64.to_be_bytes())
3389 .await
3390 .expect("Failed to open new blob");
3391 new_blob
3392 .write_at(0, vec![0u8; 1], WriteOptions::SYNC)
3393 .await
3394 .expect("Failed to write new blob");
3395
3396 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
3397 assert!(matches!(result, Err(Error::Corruption(_))));
3398 });
3399 }
3400
3401 #[test_traced]
3402 fn test_fixed_journal_init_prefers_legacy_partition() {
3403 let executor = deterministic::Runner::default();
3404 executor.start(|context| async move {
3405 let cfg = test_cfg(NZU64!(2));
3406 let legacy_partition = cfg.partition.clone();
3407 let blobs_partition = blob_partition(&cfg);
3408
3409 let (legacy_blob, _) = context
3411 .open(&legacy_partition, &0u64.to_be_bytes())
3412 .await
3413 .expect("Failed to open legacy blob");
3414 legacy_blob
3415 .write_at(0, vec![0u8; 1], WriteOptions::SYNC)
3416 .await
3417 .expect("Failed to write legacy blob");
3418
3419 drop(legacy_blob);
3420 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3421 .await
3422 .expect("failed to initialize journal");
3423 (journal, _) = journal.append(&test_digest(1)).await.unwrap();
3424 let journal = journal.sync().await.unwrap();
3425 drop(journal);
3426
3427 let legacy_blobs = scan_partition(&context, &legacy_partition).await;
3428 let new_blobs = scan_partition(&context, &blobs_partition).await;
3429 assert!(!legacy_blobs.is_empty());
3430 assert!(new_blobs.is_empty());
3431 });
3432 }
3433
3434 #[test_traced]
3435 fn test_fixed_journal_init_defaults_to_blobs_partition() {
3436 let executor = deterministic::Runner::default();
3437 executor.start(|context| async move {
3438 let cfg = test_cfg(NZU64!(2));
3439 let legacy_partition = cfg.partition.clone();
3440 let blobs_partition = blob_partition(&cfg);
3441
3442 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
3443 .await
3444 .expect("failed to initialize journal");
3445 (journal, _) = journal.append(&test_digest(1)).await.unwrap();
3446 let journal = journal.sync().await.unwrap();
3447 drop(journal);
3448
3449 let legacy_blobs = scan_partition(&context, &legacy_partition).await;
3450 let new_blobs = scan_partition(&context, &blobs_partition).await;
3451 assert!(legacy_blobs.is_empty());
3452 assert!(!new_blobs.is_empty());
3453 });
3454 }
3455
3456 #[test_traced]
3457 fn test_fixed_journal_append_and_prune() {
3458 let executor = deterministic::Runner::default();
3460
3461 executor.start(|context| async move {
3463 let cfg = test_cfg(NZU64!(2));
3465 let mut journal = Journal::init(context.child("first"), cfg.clone())
3466 .await
3467 .expect("failed to initialize journal");
3468
3469 let mut pos;
3471 (journal, pos) = journal
3472 .append(&test_digest(0))
3473 .await
3474 .expect("failed to append data 0");
3475 assert_eq!(pos, 0);
3476
3477 let journal = journal.sync().await.expect("Failed to sync journal");
3479 drop(journal);
3480
3481 let cfg = test_cfg(NZU64!(2));
3482 let mut journal = Journal::init(context.child("second"), cfg.clone())
3483 .await
3484 .expect("failed to re-initialize journal");
3485 assert_eq!(journal.size(), 1);
3486
3487 (journal, pos) = journal
3489 .append(&test_digest(1))
3490 .await
3491 .expect("failed to append data 1");
3492 assert_eq!(pos, 1);
3493 (journal, pos) = journal
3494 .append(&test_digest(2))
3495 .await
3496 .expect("failed to append data 2");
3497 assert_eq!(pos, 2);
3498
3499 let item0 = journal.read(0).await.expect("failed to read data 0");
3501 assert_eq!(item0, test_digest(0));
3502 let item1 = journal.read(1).await.expect("failed to read data 1");
3503 assert_eq!(item1, test_digest(1));
3504 let item2 = journal.read(2).await.expect("failed to read data 2");
3505 assert_eq!(item2, test_digest(2));
3506 let err = journal.read(3).await.expect_err("expected read to fail");
3507 assert!(matches!(err, Error::ItemOutOfRange(3)));
3508
3509 journal = journal.sync().await.expect("failed to sync journal");
3511
3512 (journal, _) = journal.prune(1).await.expect("failed to prune journal 1");
3514
3515 (journal, _) = journal.prune(2).await.expect("failed to prune journal 2");
3517 assert_eq!(journal.bounds().start, 2);
3518
3519 let result0 = journal.read(0).await;
3521 assert!(matches!(result0, Err(Error::ItemPruned(0))));
3522 let result1 = journal.read(1).await;
3523 assert!(matches!(result1, Err(Error::ItemPruned(1))));
3524
3525 let result2 = journal.read(2).await.unwrap();
3527 assert_eq!(result2, test_digest(2));
3528
3529 for i in 3..10 {
3531 let pos;
3532 (journal, pos) = journal
3533 .append(&test_digest(i))
3534 .await
3535 .expect("failed to append data");
3536 assert_eq!(pos, i);
3537 }
3538
3539 (journal, _) = journal.prune(0).await.expect("no-op pruning failed");
3541 assert_eq!(journal.test_oldest_blob(), Some(1));
3542 assert_eq!(journal.test_newest_blob(), Some(5));
3543 assert_eq!(journal.bounds().start, 2);
3544
3545 (journal, _) = journal
3547 .prune(3 * cfg.items_per_blob.get())
3548 .await
3549 .expect("failed to prune journal 2");
3550 assert_eq!(journal.test_oldest_blob(), Some(3));
3551 assert_eq!(journal.test_newest_blob(), Some(5));
3552 assert_eq!(journal.bounds().start, 6);
3553
3554 (journal, _) = journal
3556 .prune(10000)
3557 .await
3558 .expect("failed to max-prune journal");
3559 let size = journal.size();
3560 assert_eq!(size, 10);
3561 assert_eq!(journal.test_oldest_blob(), Some(5));
3562 assert_eq!(journal.test_newest_blob(), Some(5));
3563 let bounds = journal.bounds();
3566 assert!(bounds.is_empty());
3567 assert_eq!(bounds.start, size);
3569
3570 {
3572 let reader;
3573 (journal, reader) = journal.snapshot().await.unwrap();
3574 let result = replay_items(
3575 &reader,
3576 0..reader.bounds().end,
3577 NZUsize!(1024),
3578 ReadOptions::default(),
3579 )
3580 .await;
3581 assert!(matches!(result, Err(Error::ItemPruned(0))));
3582 }
3583
3584 {
3586 let reader;
3587 (journal, reader) = journal.snapshot().await.unwrap();
3588 let res = replay_items(
3589 &reader,
3590 0..reader.bounds().end,
3591 NZUsize!(1024),
3592 ReadOptions::default(),
3593 )
3594 .await;
3595 assert!(matches!(res, Err(Error::ItemPruned(_))));
3596
3597 let reader;
3598 (journal, reader) = journal.snapshot().await.unwrap();
3599 let stream = replay_items(
3600 &reader,
3601 journal.bounds().start..reader.bounds().end,
3602 NZUsize!(1024),
3603 ReadOptions::default(),
3604 )
3605 .await
3606 .expect("failed to replay journal from pruning boundary");
3607 pin_mut!(stream);
3608 let mut items = Vec::new();
3609 while let Some(result) = stream.next().await {
3610 match result {
3611 Ok((pos, item)) => {
3612 assert_eq!(test_digest(pos), item);
3613 items.push(pos);
3614 }
3615 Err(err) => panic!("Failed to read item: {err}"),
3616 }
3617 }
3618 assert_eq!(items, Vec::<u64>::new());
3619 }
3620
3621 journal.destroy().await.unwrap();
3622 });
3623 }
3624
3625 #[test_traced]
3627 fn test_fixed_journal_append_a_lot_of_data() {
3628 let executor = deterministic::Runner::default();
3630 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(10000);
3631 executor.start(|context| async move {
3632 let cfg = test_cfg(ITEMS_PER_BLOB);
3633 let mut journal = Journal::init(context.child("first"), cfg.clone())
3634 .await
3635 .expect("failed to initialize journal");
3636 for i in 0u64..ITEMS_PER_BLOB.get() * 2 - 1 {
3638 (journal, _) = journal
3639 .append(&test_digest(i))
3640 .await
3641 .expect("failed to append data");
3642 }
3643 journal.sync().await.expect("failed to sync journal");
3645 let journal = Journal::init(context.child("second"), cfg.clone())
3646 .await
3647 .expect("failed to re-initialize journal");
3648 for i in 0u64..10000 {
3649 let item: Digest = journal.read(i).await.expect("failed to read data");
3650 assert_eq!(item, test_digest(i));
3651 }
3652 journal.destroy().await.expect("failed to destroy journal");
3653 });
3654 }
3655
3656 #[test_traced]
3657 fn test_fixed_journal_replay() {
3658 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
3659 let executor = deterministic::Runner::default();
3661
3662 executor.start(|context| async move {
3664 let cfg = test_cfg(ITEMS_PER_BLOB);
3666 let mut journal = Journal::init(context.child("first"), cfg.clone())
3667 .await
3668 .expect("failed to initialize journal");
3669
3670 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
3672 let pos;
3673 (journal, pos) = journal
3674 .append(&test_digest(i))
3675 .await
3676 .expect("failed to append data");
3677 assert_eq!(pos, i);
3678 }
3679
3680 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
3682 let item: Digest = journal.read(i).await.expect("failed to read data");
3683 assert_eq!(item, test_digest(i), "i={i}");
3684 }
3685
3686 {
3688 let reader;
3689 (journal, reader) = journal.snapshot().await.unwrap();
3690 let stream = replay_items(
3691 &reader,
3692 0..reader.bounds().end,
3693 NZUsize!(1024),
3694 ReadOptions::default(),
3695 )
3696 .await
3697 .expect("failed to replay journal");
3698 let mut items = Vec::new();
3699 pin_mut!(stream);
3700 while let Some(result) = stream.next().await {
3701 match result {
3702 Ok((pos, item)) => {
3703 assert_eq!(test_digest(pos), item, "pos={pos}, item={item:?}");
3704 items.push(pos);
3705 }
3706 Err(err) => panic!("Failed to read item: {err}"),
3707 }
3708 }
3709
3710 assert_eq!(
3712 items.len(),
3713 ITEMS_PER_BLOB.get() as usize * 100 + ITEMS_PER_BLOB.get() as usize / 2
3714 );
3715 items.sort();
3716 for (i, pos) in items.iter().enumerate() {
3717 assert_eq!(i as u64, *pos);
3718 }
3719 }
3720
3721 let journal = journal.sync().await.expect("Failed to sync journal");
3722 drop(journal);
3723
3724 let (blob, _) = context
3726 .open(&blob_partition(&cfg), &40u64.to_be_bytes())
3727 .await
3728 .expect("Failed to open blob");
3729 let bad_bytes = 123456789u32;
3731 blob.write_at(1, bad_bytes.to_be_bytes().to_vec(), WriteOptions::SYNC)
3732 .await
3733 .expect("Failed to write bad bytes");
3734
3735 drop(blob);
3737 let journal = Journal::init(context.child("second"), cfg.clone())
3738 .await
3739 .expect("Failed to re-initialize journal");
3740
3741 let err = journal
3743 .read(40 * ITEMS_PER_BLOB.get() + 1)
3744 .await
3745 .unwrap_err();
3746 assert!(matches!(err, Error::Runtime(_)));
3747
3748 {
3750 let mut error_found = false;
3751 let (_journal, reader) = journal.snapshot().await.unwrap();
3752 let stream = replay_items(
3753 &reader,
3754 0..reader.bounds().end,
3755 NZUsize!(1024),
3756 ReadOptions::default(),
3757 )
3758 .await
3759 .expect("failed to replay journal");
3760 let mut items = Vec::new();
3761 pin_mut!(stream);
3762 while let Some(result) = stream.next().await {
3763 match result {
3764 Ok((pos, item)) => {
3765 assert_eq!(test_digest(pos), item);
3766 items.push(pos);
3767 }
3768 Err(err) => {
3769 error_found = true;
3770 assert!(matches!(err, Error::Runtime(_)));
3771 assert!(stream.next().await.is_none());
3772 break;
3773 }
3774 }
3775 }
3776 assert!(error_found); }
3778 });
3779 }
3780
3781 #[test_traced]
3782 fn test_replay_read_options_reach_history_and_tip() {
3783 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(6);
3784
3785 let executor = deterministic::Runner::default();
3786 executor.start(|context| async move {
3787 let (context, recordings) = RecordingContext::new(context);
3788 let cfg = test_cfg(ITEMS_PER_BLOB);
3789 let page_cache = cfg.page_cache.clone();
3790 let mut journal = Journal::init(context.child("journal"), cfg)
3791 .await
3792 .expect("failed to initialize journal");
3793
3794 for i in 0..9 {
3796 (journal, _) = journal
3797 .append(&test_digest(i))
3798 .await
3799 .expect("failed to append");
3800 }
3801 journal = journal.sync().await.expect("failed to sync journal");
3802
3803 page_cache.clear();
3805 recordings.clear();
3806 {
3807 let stream = replay_items(
3808 &journal,
3809 0..journal.bounds().end,
3810 NZUsize!(56),
3811 ReadOptions::DONT_CACHE,
3812 )
3813 .await
3814 .expect("failed to replay sealed history");
3815 pin_mut!(stream);
3816 let (position, item) = stream
3817 .next()
3818 .await
3819 .expect("missing sealed replay item")
3820 .expect("failed to replay sealed item");
3821 assert_eq!(position, 0);
3822 assert_eq!(item, test_digest(0));
3823
3824 let reads = recordings.snapshot().reads;
3825 assert!(!reads.is_empty());
3826 assert!(
3827 reads
3828 .iter()
3829 .all(|options| *options == ReadOptions::DONT_CACHE)
3830 );
3831 }
3832
3833 for options in [ReadOptions::default(), ReadOptions::DONT_CACHE] {
3836 page_cache.clear();
3837 recordings.clear();
3838 {
3839 let stream =
3840 replay_items(&journal, 6..journal.bounds().end, NZUsize!(2048), options)
3841 .await
3842 .expect("failed to replay writable tip");
3843 pin_mut!(stream);
3844 for i in 6..9 {
3845 let (position, item) = stream
3846 .next()
3847 .await
3848 .expect("missing writable replay item")
3849 .expect("failed to replay writable item");
3850 assert_eq!(position, i);
3851 assert_eq!(item, test_digest(i));
3852 }
3853 assert!(stream.next().await.is_none());
3854 assert_eq!(recordings.snapshot().reads, vec![options]);
3855 }
3856 recordings.clear();
3857 assert_eq!(journal.read(6).await.unwrap(), test_digest(6));
3858 assert!(!recordings.snapshot().reads.is_empty());
3859 recordings.clear();
3860 assert_eq!(journal.read(6).await.unwrap(), test_digest(6));
3861 assert!(recordings.snapshot().reads.is_empty());
3862 }
3863
3864 for i in 9..11 {
3868 (journal, _) = journal
3869 .append(&test_digest(i))
3870 .await
3871 .expect("failed to append");
3872 }
3873 page_cache.clear();
3874 recordings.clear();
3875 {
3876 let stream = replay_items(
3877 &journal,
3878 6..journal.bounds().end,
3879 NZUsize!(2048),
3880 ReadOptions::default(),
3881 )
3882 .await
3883 .expect("failed to replay buffered tip");
3884 pin_mut!(stream);
3885 let (position, item) = stream
3886 .next()
3887 .await
3888 .expect("missing buffered replay item")
3889 .expect("failed to replay buffered item");
3890 assert_eq!(position, 6);
3891 assert_eq!(item, test_digest(6));
3892 assert_eq!(
3893 recordings.snapshot().reads,
3894 vec![ReadOptions::DONT_CACHE, ReadOptions::DONT_CACHE]
3895 );
3896 }
3897
3898 journal.destroy().await.expect("failed to destroy journal");
3899 });
3900 }
3901
3902 #[test_traced]
3903 fn test_fixed_replay_stops_after_error() {
3904 let executor = deterministic::Runner::default();
3905 executor.start(|context| async move {
3906 let cfg = test_cfg(NZU64!(10));
3907 let mut journal = Journal::init(context.child("first"), cfg.clone())
3908 .await
3909 .unwrap();
3910
3911 for i in 0u64..30 {
3912 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
3913 }
3914 let journal = journal.sync().await.unwrap();
3915 drop(journal);
3916
3917 let (blob, _) = context
3918 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
3919 .await
3920 .unwrap();
3921 blob.write_at(1, 123456789u32.to_be_bytes().to_vec(), WriteOptions::SYNC)
3922 .await
3923 .unwrap();
3924
3925 drop(blob);
3926 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3927 .await
3928 .unwrap();
3929 let reader;
3930 (journal, reader) = journal.snapshot().await.unwrap();
3931 let stream = replay_items(
3932 &reader,
3933 0..reader.bounds().end,
3934 NZUsize!(1024),
3935 ReadOptions::default(),
3936 )
3937 .await
3938 .unwrap();
3939 pin_mut!(stream);
3940
3941 for i in 0u64..10 {
3942 let (pos, item) = stream.next().await.unwrap().unwrap();
3943 assert_eq!(pos, i);
3944 assert_eq!(item, test_digest(i));
3945 }
3946 assert!(matches!(
3947 stream.next().await.unwrap(),
3948 Err(Error::Runtime(_))
3949 ));
3950 assert!(stream.next().await.is_none());
3951
3952 journal.destroy().await.unwrap();
3953 });
3954 }
3955
3956 #[test_traced]
3957 fn test_fixed_journal_replay_with_missing_historical_blob() {
3958 let executor = deterministic::Runner::default();
3959 executor.start(|context| async move {
3960 let cfg = test_cfg(NZU64!(2));
3961 let mut journal = Journal::init(context.child("first"), cfg.clone())
3962 .await
3963 .unwrap();
3964 for i in 0u64..5 {
3965 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
3966 }
3967 let journal = journal.sync().await.unwrap();
3968 drop(journal);
3969
3970 context
3973 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
3974 .await
3975 .unwrap();
3976
3977 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
3978 assert!(matches!(result, Err(Error::Corruption(_))));
3979 });
3980 }
3981
3982 #[test_traced]
3983 fn test_fixed_journal_partial_replay() {
3984 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
3985 const START_POS: u64 = 53;
3988
3989 let executor = deterministic::Runner::default();
3991 executor.start(|context| async move {
3993 let cfg = test_cfg(ITEMS_PER_BLOB);
3995 let mut journal = Journal::init(context.child("storage"), cfg.clone())
3996 .await
3997 .expect("failed to initialize journal");
3998
3999 for i in 0u64..(ITEMS_PER_BLOB.get() * 100 + ITEMS_PER_BLOB.get() / 2) {
4001 let pos;
4002 (journal, pos) = journal
4003 .append(&test_digest(i))
4004 .await
4005 .expect("failed to append data");
4006 assert_eq!(pos, i);
4007 }
4008
4009 {
4011 let reader;
4012 (journal, reader) = journal.snapshot().await.unwrap();
4013 let stream = replay_items(
4014 &reader,
4015 START_POS..reader.bounds().end,
4016 NZUsize!(1024),
4017 ReadOptions::default(),
4018 )
4019 .await
4020 .expect("failed to replay journal");
4021 let mut items = Vec::new();
4022 pin_mut!(stream);
4023 while let Some(result) = stream.next().await {
4024 match result {
4025 Ok((pos, item)) => {
4026 assert!(pos >= START_POS, "pos={pos}, expected >= {START_POS}");
4027 assert_eq!(
4028 test_digest(pos),
4029 item,
4030 "Item at position {pos} did not match expected digest"
4031 );
4032 items.push(pos);
4033 }
4034 Err(err) => panic!("Failed to read item: {err}"),
4035 }
4036 }
4037
4038 assert_eq!(
4040 items.len(),
4041 ITEMS_PER_BLOB.get() as usize * 100 + ITEMS_PER_BLOB.get() as usize / 2
4042 - START_POS as usize
4043 );
4044 items.sort();
4045 for (i, pos) in items.iter().enumerate() {
4046 assert_eq!(i as u64, *pos - START_POS);
4047 }
4048 }
4049
4050 journal.destroy().await.unwrap();
4051 });
4052 }
4053
4054 #[test_traced]
4058 fn test_fixed_journal_replay_batches() {
4059 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
4060
4061 let executor = deterministic::Runner::default();
4062 executor.start(|context| async move {
4063 let cfg = test_cfg(ITEMS_PER_BLOB);
4064 let mut journal = Journal::init(context.child("storage"), cfg.clone())
4065 .await
4066 .expect("failed to initialize journal");
4067
4068 let total = ITEMS_PER_BLOB.get() * 10 + 3;
4070 for i in 0u64..total {
4071 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4072 }
4073
4074 for buffer in [Digest::SIZE, 100, 2048] {
4075 for range in [0..total, 12..40, 20..21, 30..30, 70..total] {
4076 crate::journal::contiguous::tests::check_replay_batches(
4077 &journal,
4078 range,
4079 buffer,
4080 ITEMS_PER_BLOB.get(),
4081 Digest::SIZE,
4082 test_digest,
4083 )
4084 .await;
4085 }
4086 }
4087
4088 let journal = journal.sync().await.unwrap();
4090 crate::journal::contiguous::tests::check_replay_batches(
4091 &journal,
4092 65..total,
4093 100,
4094 ITEMS_PER_BLOB.get(),
4095 Digest::SIZE,
4096 test_digest,
4097 )
4098 .await;
4099
4100 journal.destroy().await.unwrap();
4101 });
4102 }
4103
4104 #[test]
4105 fn test_fixed_replay_bulk_retains_original_allocation() {
4106 deterministic::Runner::default().start(|context| async move {
4107 let data: Vec<u8> = (0u64..16).flat_map(u64::to_be_bytes).collect();
4108 let cache = CacheRef::new(NZU16!(64), NZUsize!(4));
4109 let (blob, size) = context
4110 .open("fixed-replay-sharing", b"synced")
4111 .await
4112 .unwrap();
4113 let writer = Writer::new(blob, size, 256, cache.clone()).await.unwrap();
4114 let (writer, _) = writer.append(&data).await.unwrap();
4115 let (writer, snapshot) = writer.snapshot().await.unwrap();
4116 let (blob, size) = context
4117 .open("fixed-replay-sharing", b"pending")
4118 .await
4119 .unwrap();
4120 let pending = Writer::new(blob, size, 256, cache).await.unwrap();
4121 let (pending, _) = pending.append(&data).await.unwrap();
4122 assert!(pending.try_snapshot().is_none());
4123
4124 for source in [
4125 super::super::blobs::Blob::Sealed(snapshot),
4126 super::super::blobs::Blob::Writer(&writer),
4127 super::super::blobs::Blob::Writer(&pending),
4128 ] {
4129 let mut state = FixedReplayState::<_, View> {
4130 replay: source
4131 .replay_from(0, NZUsize!(256), ReadOptions::DONT_CACHE)
4132 .unwrap(),
4133 pos: 0,
4134 end_pos: 16,
4135 items_per_batch: 32,
4136 _marker: PhantomData,
4137 };
4138 assert!(state.replay.ensure(View::SIZE).await.unwrap());
4139 let first_chunk = state.replay.chunk();
4140 let source = first_chunk.as_ptr() as usize;
4141 let count = first_chunk.len() / View::SIZE;
4142 let batch = {
4143 let stream = super::super::replay_stream_from_states(vec![state]);
4144 pin_mut!(stream);
4145 stream.next().await.unwrap().unwrap()
4146 };
4147 assert!(!batch.items.is_empty());
4148 for (index, item) in batch.items.iter().enumerate() {
4149 assert_eq!(item.bytes.as_ref(), &(index as u64).to_be_bytes());
4150 if index < count {
4151 assert_eq!(item.bytes.as_ptr() as usize, source + index * View::SIZE);
4152 }
4153 item.assert_shared();
4154 }
4155 }
4156 });
4157 }
4158
4159 #[test_traced]
4164 fn test_bad_page_in_batch_read_ahead_ends_replay() {
4165 const PAGE: usize = 64;
4166 const ITEMS: u64 = 1280;
4167 let executor = deterministic::Runner::default();
4168 executor.start(|context| async move {
4169 let physical_page = PAGE + commonware_runtime::buffer::paged::CHECKSUM_SIZE as usize;
4170 let items_per_page = PAGE / u64::SIZE;
4171 for (buffer_pages, within_batch) in [(8, 0), (8, 1), (64, 0), (64, 1)] {
4172 let batch_pages = buffer_pages / 4;
4174 let bad_page = 2 * batch_pages + within_batch;
4175 let expected_items = (2 * batch_pages * items_per_page) as u64;
4176 let cfg = Config {
4177 partition: format!("bad-page-{buffer_pages}-{within_batch}"),
4178 items_per_blob: NZU64!(ITEMS),
4179 page_cache: CacheRef::new(NZU16!(PAGE as u16), NZUsize!(4)),
4180 write_buffer: NZUsize!(2048),
4181 replay_buffer: NZUsize!(2048),
4182 };
4183 let mut journal = Journal::<_, u64>::init(context.child("write"), cfg.clone())
4184 .await
4185 .unwrap();
4186 for item in 0..ITEMS {
4187 (journal, _) = journal.append(&item).await.unwrap();
4188 }
4189 drop(journal.sync().await.unwrap());
4190 corrupt_page(
4191 &context,
4192 &format!("{}-blobs", cfg.partition),
4193 &0u64.to_be_bytes(),
4194 bad_page as u64,
4195 PAGE as u64,
4196 )
4197 .await;
4198 cfg.page_cache.clear();
4199
4200 let journal = Journal::<_, u64>::init(context.child("replay"), cfg)
4201 .await
4202 .unwrap();
4203 let (journal, reader) = journal.snapshot().await.unwrap();
4204 let stream = reader
4205 .replay(
4206 0..ITEMS,
4207 NZUsize!(buffer_pages * physical_page),
4208 ReadOptions::DONT_CACHE,
4209 )
4210 .await
4211 .unwrap();
4212 pin_mut!(stream);
4213 let mut next = 0u64;
4214 loop {
4215 match stream.next().await.expect("checksum error before end") {
4216 Ok(batch) => {
4217 for (position, item) in batch {
4218 assert_eq!(position, next);
4219 assert_eq!(item, next);
4220 next += 1;
4221 }
4222 }
4223 Err(error) => {
4224 assert!(matches!(
4225 error,
4226 Error::Runtime(RuntimeError::InvalidChecksum)
4227 ));
4228 break;
4229 }
4230 }
4231 }
4232 assert_eq!(
4233 next, expected_items,
4234 "{buffer_pages} pages, page {bad_page}"
4235 );
4236 assert!(stream.next().await.is_none());
4237 drop(journal);
4238 }
4239 });
4240 }
4241
4242 #[test_traced]
4246 fn test_fixed_journal_replay_batches_stop_at_codec_error() {
4247 const ITEMS: u64 = 100;
4248
4249 let executor = deterministic::Runner::default();
4250 executor.start(|context| async move {
4251 for invalid in [5u64, 14, 20] {
4254 let mut cfg = test_cfg(NZU64!(1000));
4255 cfg.partition = format!("codec-error-{invalid}");
4256
4257 let mut journal = Journal::<_, [u8; 3]>::init(context.child("writer"), cfg.clone())
4259 .await
4260 .unwrap();
4261 for i in 0..ITEMS {
4262 let item = if i == invalid { [0, 2, 0] } else { [0, 1, 0] };
4263 (journal, _) = journal.append(&item).await.unwrap();
4264 }
4265 let journal = journal.sync().await.unwrap();
4266 drop(journal);
4267
4268 let journal = Journal::<_, [bool; 3]>::init(context.child("reader"), cfg)
4270 .await
4271 .unwrap();
4272 {
4273 let stream = journal
4274 .replay(0..ITEMS, NZUsize!(2048), ReadOptions::default())
4275 .await
4276 .unwrap();
4277 pin_mut!(stream);
4278 let mut decoded = 0u64;
4279 loop {
4280 match stream.next().await.unwrap() {
4281 Ok(batch) => {
4282 assert_eq!(batch.start, decoded, "invalid item {invalid}");
4283 assert!(
4284 batch.items.iter().all(|item| *item == [false, true, false])
4285 );
4286 decoded += batch.items.len() as u64;
4287 }
4288 Err(Error::Codec(_)) => break,
4289 Err(err) => panic!("unexpected error for item {invalid}: {err}"),
4290 }
4291 }
4292 assert_eq!(decoded, invalid, "invalid item {invalid}");
4293 assert!(stream.next().await.is_none());
4294 }
4295
4296 journal.destroy().await.unwrap();
4297 }
4298 });
4299 }
4300
4301 #[test_traced]
4304 fn test_fixed_journal_replay_range() {
4305 const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(7);
4306
4307 let executor = deterministic::Runner::default();
4308 executor.start(|context| async move {
4309 let cfg = test_cfg(ITEMS_PER_BLOB);
4310 let mut journal = Journal::init(context.child("storage"), cfg.clone())
4311 .await
4312 .expect("failed to initialize journal");
4313
4314 let total = ITEMS_PER_BLOB.get() * 10 + 3;
4316 for i in 0u64..total {
4317 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4318 }
4319
4320 let (start, end) = (12u64, 40u64);
4322 {
4323 let stream =
4324 replay_items(&journal, start..end, NZUsize!(1024), ReadOptions::default())
4325 .await
4326 .expect("failed to replay range");
4327 pin_mut!(stream);
4328 let mut next = start;
4329 while let Some(result) = stream.next().await {
4330 let (pos, item) = result.expect("failed to read item");
4331 assert_eq!(pos, next);
4332 assert_eq!(item, test_digest(pos));
4333 next += 1;
4334 }
4335 assert_eq!(next, end);
4336 }
4337
4338 {
4340 let stream = replay_items(&journal, 5..5, NZUsize!(1024), ReadOptions::default())
4341 .await
4342 .expect("failed to replay empty range");
4343 pin_mut!(stream);
4344 assert!(stream.next().await.is_none());
4345 }
4346
4347 let err = replay_items(
4349 &journal,
4350 0..total + 1,
4351 NZUsize!(1024),
4352 ReadOptions::default(),
4353 )
4354 .await
4355 .map(|_| ())
4356 .unwrap_err();
4357 assert!(matches!(err, Error::ItemOutOfRange(pos) if pos == total + 1));
4358
4359 #[allow(clippy::reversed_empty_ranges)]
4361 let err = replay_items(&journal, 10..5, NZUsize!(1024), ReadOptions::default())
4362 .await
4363 .map(|_| ())
4364 .unwrap_err();
4365 assert!(matches!(err, Error::ItemOutOfRange(10)));
4366
4367 journal.destroy().await.unwrap();
4368 });
4369 }
4370
4371 #[test_traced]
4372 fn test_fixed_journal_rejects_corrupted_tail_blob() {
4373 let executor = deterministic::Runner::default();
4374 executor.start(|context| async move {
4375 let cfg = test_cfg(NZU64!(3));
4376 let mut journal = Journal::init(context.child("first"), cfg.clone())
4377 .await
4378 .unwrap();
4379 for i in 0..5 {
4380 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4381 }
4382 let journal = journal.sync().await.unwrap();
4383 drop(journal);
4384
4385 let (blob, size) = context
4388 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4389 .await
4390 .unwrap();
4391 blob.resize(size - 1).await.unwrap();
4392 blob.sync().await.unwrap();
4393
4394 drop(blob);
4395 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
4396 assert!(matches!(result, Err(Error::Corruption(_))));
4397 });
4398 }
4399
4400 #[test_traced]
4403 fn test_interrupted_destroy_reopens() {
4404 let (mut unchanged, mut emptied) = (0, 0);
4405 for seed in 0..32u64 {
4406 let executor = deterministic::Runner::seeded(seed);
4407 let outcome = executor.start(|context| async move {
4408 let cfg = test_cfg(NZU64!(2));
4411 let mut journal =
4412 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 5)
4413 .await
4414 .unwrap();
4415 for i in 5..11 {
4416 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4417 }
4418 let journal = journal.sync().await.unwrap();
4419
4420 context.storage_fault_config().write().sync_rate = Some(probability!(0.3));
4423 context.storage_fault_config().write().remove_rate = Some(probability!(0.1));
4424 let result = journal.destroy().await;
4425 context.storage_fault_config().write().sync_rate = None;
4426 context.storage_fault_config().write().remove_rate = None;
4427 if result.is_ok() {
4428 return 0;
4429 }
4430
4431 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4433 .await
4434 .unwrap();
4435 let bounds = journal.bounds();
4436 let outcome = if bounds == (5..11) {
4437 for pos in bounds {
4438 assert_eq!(journal.read(pos).await.unwrap(), test_digest(pos));
4439 }
4440 1
4441 } else {
4442 assert_eq!(bounds, 0..0, "seed {seed} reopened with {bounds:?}");
4443 2
4444 };
4445
4446 journal.destroy().await.unwrap();
4448 let journal = Journal::<_, Digest>::init(context.child("third"), cfg)
4449 .await
4450 .unwrap();
4451 assert_eq!(journal.bounds(), 0..0);
4452 outcome
4453 });
4454 match outcome {
4455 1 => unchanged += 1,
4456 2 => emptied += 1,
4457 _ => {}
4458 }
4459 }
4460 assert!(
4461 unchanged > 0 && emptied > 0,
4462 "unchanged {unchanged} emptied {emptied}"
4463 );
4464 }
4465
4466 #[test_traced]
4471 fn test_fixed_journal_crash_during_recovery_repair() {
4472 let executor = deterministic::Runner::default();
4473 executor.start(|context| async move {
4474 let cfg = test_cfg(NZU64!(5));
4475 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4476 .await
4477 .unwrap();
4478
4479 for i in 0..15u64 {
4481 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4482 }
4483 let mut journal = journal.sync().await.unwrap();
4484 assert_eq!(journal.0.recovery_watermark(), 15);
4485
4486 journal.0.checkpoint = journal
4490 .0
4491 .checkpoint
4492 .persist(cfg.items_per_blob.get(), 0, 9)
4493 .await
4494 .unwrap();
4495 drop(journal);
4496
4497 {
4500 let cache_ref = CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE);
4501 let (blob, blob_size) = context
4502 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4503 .await
4504 .expect("failed to open blob 1");
4505 let mut append = PagedRecovery::open(blob, blob_size, 2048, cache_ref)
4506 .await
4507 .expect("failed to wrap blob 1");
4508 append = append
4509 .truncate(4 * Digest::SIZE as u64)
4510 .await
4511 .expect("failed to shorten blob 1");
4512 append
4513 .sync()
4514 .await
4515 .expect("failed to sync shortened blob 1");
4516 }
4517
4518 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4521 .await
4522 .expect("init should succeed after crash during recovery repair");
4523 assert_eq!(journal.bounds(), 0..9);
4524 assert_eq!(journal.0.recovery_watermark(), 9);
4525 assert_eq!(journal.read(8).await.unwrap(), test_digest(8));
4526 assert!(matches!(
4527 journal.read(9).await,
4528 Err(Error::ItemOutOfRange(9))
4529 ));
4530 assert_eq!(
4531 journal.test_newest_blob(),
4532 Some(1),
4533 "stale blobs beyond the repair point should be removed"
4534 );
4535
4536 journal.destroy().await.unwrap();
4537 });
4538 }
4539
4540 #[test_traced]
4541 fn test_fixed_journal_recover_accepts_clean_short_tail() {
4542 let executor = deterministic::Runner::default();
4543 executor.start(|context| async move {
4544 let cfg = test_cfg(NZU64!(5));
4545
4546 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4549 .await
4550 .unwrap();
4551 for i in 0..7 {
4552 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4553 }
4554 journal.sync().await.unwrap();
4555
4556 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4558 .await
4559 .unwrap();
4560 assert_eq!(journal.size(), 7);
4561 for i in 0..7u64 {
4563 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4564 }
4565 journal.destroy().await.unwrap();
4566 });
4567 }
4568
4569 #[test_traced]
4570 fn test_fixed_journal_recover_accepts_clean_empty_tail() {
4571 let executor = deterministic::Runner::default();
4572 executor.start(|context| async move {
4573 let cfg = test_cfg(NZU64!(5));
4574
4575 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4578 .await
4579 .unwrap();
4580 for i in 0..5 {
4581 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4582 }
4583 journal.sync().await.unwrap();
4584
4585 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4587 .await
4588 .unwrap();
4589 assert_eq!(journal.size(), 5);
4590 for i in 0..5u64 {
4591 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
4592 }
4593 assert_eq!(journal.test_newest_blob(), Some(1));
4594 journal.destroy().await.unwrap();
4595 });
4596 }
4597
4598 #[test_traced]
4599 fn test_fixed_journal_recover_sparse_blob_ids_repairs_at_gap() {
4600 let executor = deterministic::Runner::default();
4601 executor.start(|context| async move {
4602 let cfg = test_cfg(NZU64!(1));
4603 let blob_partition = blob_partition(&cfg);
4604
4605 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4606 .await
4607 .unwrap();
4608 (journal, _) = journal.append(&test_digest(0)).await.unwrap();
4609 let journal = journal.sync().await.unwrap();
4610 drop(journal);
4611
4612 let cache_ref = CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE);
4615 let (blob, blob_size) = context
4616 .open(&blob_partition, &u64::MAX.to_be_bytes())
4617 .await
4618 .unwrap();
4619 let mut append = Writer::new(blob, blob_size, 2048, cache_ref).await.unwrap();
4620 let extra = test_digest(999);
4621 (append, _) = append.append(extra.as_ref()).await.unwrap();
4622 append = append.sync().await.unwrap();
4623 drop(append);
4624
4625 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4626 .await
4627 .unwrap();
4628 assert_eq!(journal.bounds(), 0..1);
4629 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
4630 assert!(matches!(
4631 journal.read(1).await,
4632 Err(Error::ItemOutOfRange(1))
4633 ));
4634 assert_eq!(journal.test_newest_blob(), Some(1));
4635
4636 journal.destroy().await.unwrap();
4637 });
4638 }
4639
4640 #[test_traced]
4641 fn test_fixed_journal_recover_fallback_truncates_after_short_oldest_blob() {
4642 let executor = deterministic::Runner::default();
4643 executor.start(|context| async move {
4644 let cfg = test_cfg(NZU64!(5));
4645 let mut journal =
4646 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4647 .await
4648 .expect("failed to initialize journal at size");
4649
4650 for i in 0..8u64 {
4651 (journal, _) = journal
4652 .append(&test_digest(100 + i))
4653 .await
4654 .expect("failed to append data");
4655 }
4656 let journal = journal.sync().await.expect("failed to sync journal");
4657 assert_eq!(journal.bounds(), 7..15);
4658
4659 let journal = journal
4660 .test_set_recovery_watermark(6)
4661 .await
4662 .expect("failed to sync lower recovery watermark");
4663 drop(journal);
4664
4665 let (blob, size) = context
4666 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
4667 .await
4668 .expect("failed to open oldest blob");
4669 blob.resize(size - 1).await.expect("failed to corrupt blob");
4670 blob.sync().await.expect("failed to sync blob");
4671
4672 drop(blob);
4673 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4674 .await
4675 .expect("failed to recover journal");
4676 assert_eq!(journal.bounds(), 7..9);
4677 assert_eq!(journal.read(7).await.unwrap(), test_digest(100));
4678 assert_eq!(journal.read(8).await.unwrap(), test_digest(101));
4679 assert!(matches!(
4680 journal.read(9).await,
4681 Err(Error::ItemOutOfRange(9))
4682 ));
4683 assert_eq!(journal.test_oldest_blob(), Some(1));
4684 assert_eq!(journal.test_newest_blob(), Some(1));
4685
4686 journal.destroy().await.unwrap();
4687 });
4688 }
4689
4690 #[test_traced]
4691 fn test_fixed_journal_stale_pruning_metadata_preserves_watermark() {
4692 let executor = deterministic::Runner::default();
4693 executor.start(|context| async move {
4694 let cfg = test_cfg(NZU64!(5));
4695 let mut journal =
4696 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4697 .await
4698 .expect("failed to initialize journal at size");
4699
4700 for i in 0..10u64 {
4701 (journal, _) = journal
4702 .append(&test_digest(i))
4703 .await
4704 .expect("failed to append data");
4705 }
4706 let journal = journal.sync().await.expect("failed to sync journal");
4707 assert_eq!(journal.bounds(), 7..17);
4708
4709 let journal = journal
4712 .test_set_recovery_watermark(12)
4713 .await
4714 .expect("failed to sync recovery watermark");
4715 drop(journal);
4716
4717 {
4720 let cache_ref = CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE);
4721 let (blob, blob_size) = context
4722 .open(&blob_partition(&cfg), &2u64.to_be_bytes())
4723 .await
4724 .expect("failed to open blob 2");
4725 let mut append = PagedRecovery::open(blob, blob_size, 2048, cache_ref)
4726 .await
4727 .expect("failed to wrap blob 2");
4728 append = append
4729 .truncate(2 * Digest::SIZE as u64)
4730 .await
4731 .expect("failed to shorten anchored blob");
4732 append.sync().await.expect("failed to sync blob 2");
4733 }
4734
4735 context
4738 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
4739 .await
4740 .expect("failed to remove stale oldest blob");
4741
4742 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4743 .await
4744 .expect("failed to recover journal");
4745 assert_eq!(journal.bounds(), 10..12);
4746 assert_eq!(journal.0.recovery_watermark(), 12);
4747 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
4748 assert_eq!(journal.read(11).await.unwrap(), test_digest(4));
4749 assert!(matches!(
4750 journal.read(12).await,
4751 Err(Error::ItemOutOfRange(12))
4752 ));
4753
4754 journal.destroy().await.unwrap();
4755 });
4756 }
4757
4758 #[test_traced]
4759 fn test_fixed_journal_stale_pruning_metadata_without_watermark_walks_lengths() {
4760 let executor = deterministic::Runner::default();
4761 executor.start(|context| async move {
4762 let cfg = test_cfg(NZU64!(5));
4763 let mut journal =
4764 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4765 .await
4766 .expect("failed to initialize journal at size");
4767
4768 for i in 0..10u64 {
4769 (journal, _) = journal
4770 .append(&test_digest(i))
4771 .await
4772 .expect("failed to append data");
4773 }
4774 let mut journal = journal.sync().await.expect("failed to sync journal");
4775 assert_eq!(journal.bounds(), 7..17);
4776
4777 {
4778 journal.0.checkpoint.set_watermark(None);
4779 journal.0.checkpoint = journal
4780 .0
4781 .checkpoint
4782 .sync()
4783 .await
4784 .expect("failed to remove recovery watermark");
4785 }
4786 drop(journal);
4787
4788 context
4791 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
4792 .await
4793 .expect("failed to remove stale oldest blob");
4794
4795 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4796 .await
4797 .expect("failed to recover journal");
4798 assert_eq!(journal.bounds(), 10..17);
4799 assert_eq!(journal.0.recovery_watermark(), 15);
4801 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
4802 assert_eq!(journal.read(16).await.unwrap(), test_digest(9));
4803
4804 journal = journal.sync().await.expect("failed to sync");
4806 assert_eq!(journal.0.recovery_watermark(), 17);
4807
4808 journal.destroy().await.unwrap();
4809 });
4810 }
4811
4812 #[test_traced]
4816 fn test_fixed_journal_boundary_hint_ahead_of_blobs_is_corruption() {
4817 let executor = deterministic::Runner::default();
4818 executor.start(|context| async move {
4819 let cfg = test_cfg(NZU64!(5));
4820 let mut journal =
4821 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 3)
4822 .await
4823 .unwrap();
4824
4825 for i in 0..12u64 {
4827 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4828 }
4829 let mut journal = journal.sync().await.unwrap();
4830 assert_eq!(journal.bounds(), 3..15);
4831
4832 {
4837 journal.0.checkpoint.set_boundary_hint(8);
4838 journal.0.checkpoint.set_watermark(Some(3));
4839 journal.0.checkpoint = journal.0.checkpoint.sync().await.unwrap();
4840 }
4841 drop(journal);
4842
4843 context
4844 .remove(&blob_partition(&cfg), Some(&1u64.to_be_bytes()))
4845 .await
4846 .unwrap();
4847
4848 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
4849 assert!(matches!(result, Err(Error::Corruption(_))));
4850 });
4851 }
4852
4853 #[test_traced]
4856 fn test_fixed_journal_boundary_hint_with_no_blobs_is_corruption() {
4857 let executor = deterministic::Runner::default();
4858 executor.start(|context| async move {
4859 let cfg = test_cfg(NZU64!(5));
4860 let mut journal =
4861 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4862 .await
4863 .unwrap();
4864
4865 for i in 0..3u64 {
4866 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
4867 }
4868 let journal = journal.sync().await.unwrap();
4869 drop(journal);
4870
4871 for name in scan_partition(&context, &blob_partition(&cfg)).await {
4873 context
4874 .remove(&blob_partition(&cfg), Some(&name))
4875 .await
4876 .unwrap();
4877 }
4878
4879 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
4880 assert!(matches!(result, Err(Error::Corruption(_))));
4881 });
4882 }
4883
4884 #[test_traced]
4885 fn test_fixed_journal_legacy_recovery_installs_watermark() {
4886 let executor = deterministic::Runner::default();
4887 executor.start(|context| async move {
4888 let cfg = test_cfg(NZU64!(5));
4889 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
4890 .await
4891 .expect("failed to initialize journal");
4892
4893 for i in 0..12u64 {
4894 (journal, _) = journal
4895 .append(&test_digest(i))
4896 .await
4897 .expect("failed to append data");
4898 }
4899 let mut journal = journal.sync().await.expect("failed to sync journal");
4900
4901 {
4902 journal.0.checkpoint.set_watermark(None);
4903 journal.0.checkpoint = journal
4904 .0
4905 .checkpoint
4906 .sync()
4907 .await
4908 .expect("failed to remove recovery watermark");
4909 }
4910 drop(journal);
4911
4912 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4915 .await
4916 .expect("failed to recover legacy journal");
4917 assert_eq!(journal.bounds(), 0..12);
4918 assert_eq!(journal.0.recovery_watermark(), 10);
4919
4920 journal = journal
4922 .sync()
4923 .await
4924 .expect("failed to sync after legacy recovery");
4925 assert_eq!(journal.0.recovery_watermark(), 12);
4926
4927 journal.destroy().await.unwrap();
4928 });
4929 }
4930
4931 #[test_traced]
4932 fn test_fixed_journal_commit_does_not_advance_recovery_watermark() {
4933 let executor = deterministic::Runner::default();
4934 executor.start(|context| async move {
4935 let cfg = test_cfg(NZU64!(5));
4936 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
4937 .await
4938 .unwrap();
4939
4940 (journal, _) = journal.append(&test_digest(0)).await.unwrap();
4941 let mut journal = journal.sync().await.unwrap();
4942 assert_eq!(journal.0.recovery_watermark(), 1);
4943
4944 (journal, _) = journal.append(&test_digest(1)).await.unwrap();
4945 let mut journal = journal.commit().await.unwrap();
4946 assert_eq!(
4947 journal.0.recovery_watermark(),
4948 1,
4949 "commit must make new data durable without advancing the recovery watermark",
4950 );
4951
4952 journal = journal.sync().await.unwrap();
4953 assert_eq!(journal.0.recovery_watermark(), 2);
4954 journal.destroy().await.unwrap();
4955 });
4956 }
4957
4958 #[test_traced]
4959 fn test_fixed_journal_prune_to_blob_boundary_removes_pruning_metadata() {
4960 let executor = deterministic::Runner::default();
4961 executor.start(|context| async move {
4962 let cfg = test_cfg(NZU64!(5));
4963 let mut journal =
4964 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
4965 .await
4966 .expect("failed to initialize journal at size");
4967
4968 for i in 0..8u64 {
4969 (journal, _) = journal
4970 .append(&test_digest(i))
4971 .await
4972 .expect("failed to append data");
4973 }
4974 let mut journal = journal.sync().await.expect("failed to sync journal");
4975 assert_eq!(journal.bounds(), 7..15);
4976
4977 (journal, _) = journal.prune(10).await.expect("failed to prune journal");
4978 let journal = journal.sync().await.expect("failed to sync pruned journal");
4979 assert_eq!(journal.bounds(), 10..15);
4980 drop(journal);
4981
4982 let checkpoint = Checkpoint::open(context.child("metadata"), &cfg.partition)
4983 .await
4984 .expect("failed to reopen checkpoint");
4985 assert!(checkpoint.boundary_hint().is_none());
4986 drop(checkpoint);
4987
4988 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
4989 .await
4990 .expect("failed to reopen journal");
4991 assert_eq!(journal.bounds(), 10..15);
4992 assert_eq!(journal.read(10).await.unwrap(), test_digest(3));
4993 journal.destroy().await.unwrap();
4994 });
4995 }
4996
4997 #[test_traced]
4998 fn test_fixed_journal_recover_rejects_overlong_blob() {
4999 let executor = deterministic::Runner::default();
5000 executor.start(|context| async move {
5001 let cfg = test_cfg(NZU64!(5));
5002 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5003 .await
5004 .expect("failed to initialize journal");
5005
5006 for i in 0..5u64 {
5007 (journal, _) = journal
5008 .append(&test_digest(i))
5009 .await
5010 .expect("failed to append data");
5011 }
5012 let journal = journal.sync().await.expect("failed to sync journal");
5013 drop(journal);
5014
5015 {
5018 let extra = test_digest(99);
5019 let cache_ref = CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE);
5020 let (blob, blob_size) = context
5021 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
5022 .await
5023 .expect("failed to open blob 0");
5024 let mut append = Writer::new(blob, blob_size, 2048, cache_ref)
5025 .await
5026 .expect("failed to wrap blob 0");
5027 (append, _) = append
5028 .append(extra.as_ref())
5029 .await
5030 .expect("failed to append extra item");
5031 append.sync().await.expect("failed to sync corrupted blob");
5032 }
5033
5034 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
5035 assert!(matches!(result, Err(Error::Corruption(_))));
5036
5037 for cap in [3, 5] {
5040 let result =
5041 Journal::<_, Digest>::init_at_most(context.child("capped"), cfg.clone(), cap)
5042 .await;
5043 assert!(matches!(result, Err(Error::Corruption(_))), "cap {cap}");
5044 }
5045 });
5046 }
5047
5048 #[test_traced("DEBUG")]
5049 fn test_fixed_journal_recover_from_unwritten_data() {
5050 let executor = deterministic::Runner::default();
5051 executor.start(|context| async move {
5052 let cfg = test_cfg(NZU64!(10));
5054 let mut journal = Journal::init(context.child("first"), cfg.clone())
5055 .await
5056 .expect("failed to initialize journal");
5057
5058 (journal, _) = journal
5060 .append(&test_digest(0))
5061 .await
5062 .expect("failed to append data");
5063 assert_eq!(journal.size(), 1);
5064 let journal = journal.sync().await.expect("Failed to sync journal");
5065 drop(journal);
5066
5067 let (blob, size) = context
5070 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
5071 .await
5072 .expect("Failed to open blob");
5073 blob.write_at(
5074 size,
5075 vec![0u8; PAGE_SIZE.get() as usize * 3],
5076 WriteOptions::SYNC,
5077 )
5078 .await
5079 .expect("Failed to extend blob");
5080
5081 drop(blob);
5083 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5084 .await
5085 .expect("Failed to re-initialize journal");
5086
5087 assert_eq!(journal.size(), 1);
5090
5091 (journal, _) = journal
5093 .append(&test_digest(1))
5094 .await
5095 .expect("failed to append data");
5096
5097 journal.destroy().await.unwrap();
5098 });
5099 }
5100
5101 #[test_traced]
5102 fn test_fixed_journal_init_at_most() {
5103 let executor = deterministic::Runner::default();
5104 executor.start(|context| async move {
5105 let cfg = test_cfg(NZU64!(2));
5107 let journal: Journal<_, Digest> = Journal::init(context.child("first"), cfg.clone())
5108 .await
5109 .expect("failed to initialize journal");
5110 let journal = {
5111 _ = journal.sync().await.unwrap();
5112 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 0).await
5113 }
5114 .unwrap();
5115 drop(journal);
5116 let journal =
5117 Journal::<_, Digest>::init_at_most(context.child("above"), cfg.clone(), 1)
5118 .await
5119 .unwrap();
5120 assert_eq!(journal.size(), 0);
5121 drop(journal);
5122 let mut journal: Journal<_, Digest> =
5123 Journal::init(context.child("reopen"), cfg.clone())
5124 .await
5125 .expect("failed to re-initialize journal");
5126
5127 (journal, _) = journal
5129 .append(&test_digest(0))
5130 .await
5131 .expect("failed to append data 0");
5132 assert_eq!(journal.size(), 1);
5133 journal = {
5134 _ = journal.sync().await.unwrap();
5135 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 1).await
5136 }
5137 .unwrap(); journal = {
5139 _ = journal.sync().await.unwrap();
5140 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 0).await
5141 }
5142 .unwrap();
5143 assert_eq!(journal.size(), 0);
5144
5145 for i in 0..7 {
5147 let pos;
5148 (journal, pos) = journal
5149 .append(&test_digest(i))
5150 .await
5151 .expect("failed to append data");
5152 assert_eq!(pos, i);
5153 }
5154 assert_eq!(journal.size(), 7);
5155
5156 journal = {
5158 _ = journal.sync().await.unwrap();
5159 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 4).await
5160 }
5161 .unwrap();
5162 assert_eq!(journal.size(), 4);
5163
5164 journal = {
5166 _ = journal.sync().await.unwrap();
5167 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 0).await
5168 }
5169 .unwrap();
5170 assert_eq!(journal.size(), 0);
5171
5172 for _ in 0..10 {
5174 for i in 0..100 {
5175 (journal, _) = journal
5176 .append(&test_digest(i))
5177 .await
5178 .expect("failed to append data");
5179 }
5180 let size = journal.size();
5181 journal = {
5182 _ = journal.sync().await.unwrap();
5183 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), size - 49)
5184 .await
5185 }
5186 .unwrap();
5187 }
5188 const ITEMS_REMAINING: u64 = 10 * (100 - 49);
5189 assert_eq!(journal.size(), ITEMS_REMAINING);
5190
5191 let journal = journal.sync().await.expect("Failed to sync journal");
5192 drop(journal);
5193
5194 let mut cfg = test_cfg(NZU64!(3));
5196 cfg.partition = "test-partition-2".into();
5197 let mut journal = Journal::init(context.child("second"), cfg.clone())
5198 .await
5199 .expect("failed to initialize journal");
5200 for _ in 0..10 {
5201 for i in 0..100 {
5202 (journal, _) = journal
5203 .append(&test_digest(i))
5204 .await
5205 .expect("failed to append data");
5206 }
5207 let size = journal.size();
5208 journal = {
5209 _ = journal.sync().await.unwrap();
5210 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), size - 49)
5211 .await
5212 }
5213 .unwrap();
5214 }
5215 assert_eq!(journal.size(), ITEMS_REMAINING);
5216
5217 journal.sync().await.expect("Failed to sync journal");
5218
5219 let mut journal: Journal<_, Digest> =
5221 Journal::init(context.child("third"), cfg.clone())
5222 .await
5223 .expect("failed to re-initialize journal");
5224 assert_eq!(journal.size(), 10 * (100 - 49));
5225
5226 (journal, _) = journal.prune(300).await.expect("pruning failed");
5228 assert_eq!(journal.size(), ITEMS_REMAINING);
5229
5230 journal = {
5232 _ = journal.sync().await.unwrap();
5233 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 300).await
5234 }
5235 .unwrap();
5236 let bounds = journal.bounds();
5237 assert_eq!(bounds.end, 300);
5238 assert!(bounds.is_empty());
5239
5240 assert!(matches!(
5242 {
5243 _ = journal.sync().await.unwrap();
5244 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 299).await
5245 },
5246 Err(Error::ItemPruned(299))
5247 ));
5248 });
5249 }
5250
5251 #[test_traced]
5252 fn test_fixed_journal_init_at_most_commit_reopen() {
5253 let executor = deterministic::Runner::default();
5254 executor.start(|context| async move {
5255 let cfg = test_cfg(NZU64!(5));
5256 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5257 .await
5258 .expect("failed to initialize journal");
5259
5260 for i in 0..12u64 {
5261 (journal, _) = journal
5262 .append(&test_digest(i))
5263 .await
5264 .expect("failed to append data");
5265 }
5266 let journal = journal.sync().await.expect("failed to sync journal");
5267
5268 let journal = {
5269 _ = journal.sync().await.unwrap();
5270 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 7).await
5271 }
5272 .expect("failed to initialize journal prefix");
5273 journal.commit().await.expect("failed to commit journal");
5274
5275 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5276 .await
5277 .expect("failed to re-initialize journal");
5278 assert_eq!(journal.bounds(), 0..7);
5279 for i in 0..7u64 {
5280 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5281 }
5282 assert!(matches!(
5283 journal.read(7).await,
5284 Err(Error::ItemOutOfRange(7))
5285 ));
5286
5287 journal.destroy().await.unwrap();
5288 });
5289 }
5290
5291 #[test_traced]
5292 fn test_fixed_journal_init_at_most_persists_lower_watermark() {
5293 let executor = deterministic::Runner::default();
5294 executor.start(|context| async move {
5295 let cfg = test_cfg(NZU64!(5));
5296 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5297 .await
5298 .expect("failed to initialize journal");
5299
5300 for i in 0..12u64 {
5301 (journal, _) = journal
5302 .append(&test_digest(i))
5303 .await
5304 .expect("failed to append data");
5305 }
5306 let journal = journal.sync().await.expect("failed to sync journal");
5307 {
5308 _ = journal.sync().await.unwrap();
5309 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 7).await
5310 }
5311 .expect("failed to truncate journal");
5312
5313 let checkpoint = Checkpoint::open(context.child("metadata"), &cfg.partition)
5314 .await
5315 .expect("failed to reopen checkpoint");
5316 let persisted_watermark = checkpoint
5317 .watermark()
5318 .expect("missing recovery watermark after truncate");
5319 assert_eq!(persisted_watermark, 7);
5320 drop(checkpoint);
5321
5322 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5323 .await
5324 .expect("failed to re-initialize journal");
5325 journal.destroy().await.unwrap();
5326 });
5327 }
5328
5329 #[test_traced]
5333 fn test_fixed_journal_init_at_most_truncation_survives_crash() {
5334 const PAGE_SIZE: NonZeroU16 = NZU16!(64);
5338 const BOUND: u64 = 4;
5339
5340 fn cfg() -> Config {
5341 Config {
5342 partition: "init-at-most-truncation-crash".into(),
5343 items_per_blob: NZU64!(100),
5344 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
5345 write_buffer: NZUsize!(1),
5346 replay_buffer: NZUsize!(2048),
5347 }
5348 }
5349 let appended: Vec<Digest> = (100..105u64).map(test_digest).collect();
5350
5351 let executor = deterministic::Runner::default();
5352 let (appended, checkpoint) = executor.start_and_recover(|context| async move {
5353 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg())
5354 .await
5355 .unwrap();
5356 for i in 0..12u64 {
5357 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5358 }
5359 let journal = journal.sync().await.unwrap();
5360 drop(journal);
5361
5362 *context.storage_fault_config().write() = deterministic::FaultConfig {
5364 write_rate: Some(deterministic::WriteConfig {
5365 failure_rate: probability!(0.0),
5366 retention_rate: probability!(1.0),
5367 mode: deterministic::PartialWriteMode::Prefix,
5368 }),
5369 resize_rate: Some(deterministic::ResizeConfig {
5370 failure_rate: probability!(0.0),
5371 partial_rate: probability!(0.0),
5372 }),
5373 ..Default::default()
5374 };
5375 let journal = Journal::<_, Digest>::init_at_most(context.child("cap"), cfg(), BOUND)
5376 .await
5377 .unwrap();
5378 assert_eq!(journal.bounds(), 0..BOUND);
5379 let (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5380 drop(journal);
5381 appended
5382 });
5383
5384 deterministic::Runner::from(checkpoint).start(|context| async move {
5385 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
5386 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg())
5387 .await
5388 .unwrap();
5389 let size = journal.size();
5390 assert!(
5391 (BOUND..=BOUND + appended.len() as u64).contains(&size),
5392 "recovered size {size} is not a prefix of the new history"
5393 );
5394 for pos in 0..BOUND {
5395 assert_eq!(journal.read(pos).await.unwrap(), test_digest(pos));
5396 }
5397 for pos in BOUND..size {
5398 assert_eq!(
5399 journal.read(pos).await.unwrap(),
5400 appended[(pos - BOUND) as usize],
5401 "content at retained position {pos} was never written there"
5402 );
5403 }
5404 journal.destroy().await.unwrap();
5405 });
5406 }
5407
5408 #[test_traced]
5413 fn test_fixed_journal_repair_truncation_survives_crash() {
5414 const PAGE_SIZE: NonZeroU16 = NZU16!(64);
5418 const REPAIRED: u64 = 4;
5419
5420 fn cfg() -> Config {
5421 Config {
5422 partition: "repair-truncation-crash".into(),
5423 items_per_blob: NZU64!(100),
5424 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
5425 write_buffer: NZUsize!(1),
5426 replay_buffer: NZUsize!(2048),
5427 }
5428 }
5429
5430 for retained in [true, false] {
5431 let appended: Vec<Digest> = (100..105u64).map(test_digest).collect();
5432
5433 let executor = deterministic::Runner::default();
5434 let (appended, checkpoint) = executor.start_and_recover(|context| async move {
5435 let cfg = cfg();
5436 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5437 .await
5438 .unwrap();
5439 for i in 0..12u64 {
5440 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5441 }
5442
5443 let journal = journal.commit().await.unwrap();
5445 drop(journal);
5446
5447 corrupt_page(
5450 &context,
5451 &blob_partition(&cfg),
5452 &0u64.to_be_bytes(),
5453 2,
5454 PAGE_SIZE.get() as u64,
5455 )
5456 .await;
5457
5458 *context.storage_fault_config().write() = if retained {
5461 deterministic::FaultConfig {
5462 write_rate: Some(deterministic::WriteConfig {
5463 failure_rate: probability!(0.0),
5464 retention_rate: probability!(1.0),
5465 mode: deterministic::PartialWriteMode::Prefix,
5466 }),
5467 resize_rate: Some(deterministic::ResizeConfig {
5468 failure_rate: probability!(0.0),
5469 partial_rate: probability!(0.0),
5470 }),
5471 ..Default::default()
5472 }
5473 } else {
5474 deterministic::FaultConfig::default()
5475 };
5476 let journal = Journal::<_, Digest>::init(context.child("repair"), cfg)
5477 .await
5478 .unwrap();
5479 assert_eq!(journal.bounds(), 0..REPAIRED);
5480 let (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5481 drop(journal);
5482 appended
5483 });
5484
5485 deterministic::Runner::from(checkpoint).start(|context| async move {
5486 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
5487 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg())
5488 .await
5489 .unwrap();
5490 let size = journal.size();
5491 let kept = if retained { appended.len() as u64 } else { 0 };
5492 assert!(
5493 (REPAIRED..=REPAIRED + kept).contains(&size),
5494 "recovered size {size} is not a prefix of the new history"
5495 );
5496 for pos in 0..REPAIRED {
5497 assert_eq!(journal.read(pos).await.unwrap(), test_digest(pos));
5498 }
5499 for pos in REPAIRED..size {
5500 assert_eq!(
5501 journal.read(pos).await.unwrap(),
5502 appended[(pos - REPAIRED) as usize],
5503 "content at retained position {pos} was never written there"
5504 );
5505 }
5506 journal.destroy().await.unwrap();
5507 });
5508 }
5509 }
5510
5511 #[test_traced]
5512 fn test_fixed_journal_recover_after_watermark_lowered_before_truncate() {
5513 let executor = deterministic::Runner::default();
5514 executor.start(|context| async move {
5515 let cfg = test_cfg(NZU64!(5));
5516 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5517 .await
5518 .expect("failed to initialize journal");
5519
5520 for i in 0..12u64 {
5521 (journal, _) = journal
5522 .append(&test_digest(i))
5523 .await
5524 .expect("failed to append data");
5525 }
5526 let journal = journal.sync().await.expect("failed to sync journal");
5527
5528 let journal = journal
5529 .test_set_recovery_watermark(7)
5530 .await
5531 .expect("failed to lower recovery watermark");
5532 drop(journal);
5533
5534 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5535 .await
5536 .expect("failed to recover journal");
5537 assert_eq!(journal.bounds(), 0..12);
5538 assert_eq!(journal.0.recovery_watermark(), 7);
5539 assert_eq!(journal.read(11).await.unwrap(), test_digest(11));
5540 journal.destroy().await.unwrap();
5541 });
5542 }
5543
5544 #[test_traced]
5545 fn test_fixed_journal_truncate_append_commit_reopen() {
5546 let executor = deterministic::Runner::default();
5547 executor.start(|context| async move {
5548 let cfg = test_cfg(NZU64!(5));
5549 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5550 .await
5551 .expect("failed to initialize journal");
5552
5553 for i in 0..12u64 {
5554 (journal, _) = journal
5555 .append(&test_digest(i))
5556 .await
5557 .expect("failed to append data");
5558 }
5559 let journal = journal.sync().await.expect("failed to sync journal");
5560
5561 let mut journal = {
5562 _ = journal.sync().await.unwrap();
5563 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 7).await
5564 }
5565 .expect("failed to truncate journal");
5566 for i in 0..3u64 {
5567 (journal, _) = journal
5568 .append(&test_digest(100 + i))
5569 .await
5570 .expect("failed to append data");
5571 }
5572 journal.commit().await.expect("failed to commit journal");
5573
5574 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
5575 .await
5576 .expect("failed to re-initialize journal");
5577 assert_eq!(journal.bounds(), 0..10);
5578 assert_eq!(journal.0.recovery_watermark(), 7);
5579 for i in 0..7u64 {
5580 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5581 }
5582 for i in 0..3u64 {
5583 assert_eq!(journal.read(7 + i).await.unwrap(), test_digest(100 + i));
5584 }
5585 assert!(matches!(
5586 journal.read(10).await,
5587 Err(Error::ItemOutOfRange(10))
5588 ));
5589
5590 journal.destroy().await.unwrap();
5591 });
5592 }
5593
5594 #[test_traced]
5595 fn test_fixed_recovery_preserves_rolled_predecessors_without_commit() {
5596 let executor = deterministic::Runner::default();
5597 executor.start(|context| async move {
5598 let cfg = test_cfg(NZU64!(1));
5599 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
5600 .await
5601 .unwrap();
5602
5603 let appended;
5607 (journal, appended) = journal.append(&test_digest(10)).await.unwrap();
5608 assert_eq!(appended, 0);
5609 journal = journal.sync().await.unwrap();
5610 let appended;
5611 (journal, appended) = journal.append(&test_digest(20)).await.unwrap();
5612 assert_eq!(appended, 1);
5613 let appended;
5614 (journal, appended) = journal.append(&test_digest(30)).await.unwrap();
5615 assert_eq!(appended, 2);
5616 drop(journal);
5617
5618 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
5619 assert!(
5620 blobs.len() > 2,
5621 "expected multiple empty trailing blobs, got {}",
5622 blobs.len()
5623 );
5624
5625 let journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
5626 .await
5627 .unwrap();
5628 assert_eq!(journal.bounds(), 0..3);
5629 assert_eq!(journal.read(0).await.unwrap(), test_digest(10));
5630 assert_eq!(journal.read(1).await.unwrap(), test_digest(20));
5631 assert_eq!(journal.read(2).await.unwrap(), test_digest(30));
5632 drop(journal);
5633
5634 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
5636 assert_eq!(blobs.len(), 4);
5637
5638 let mut journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
5639 .await
5640 .unwrap();
5641 let appended;
5642 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
5643 assert_eq!(appended, 3);
5644 assert_eq!(journal.read(3).await.unwrap(), test_digest(42));
5645 journal.destroy().await.unwrap();
5646 });
5647 }
5648
5649 #[test_traced]
5650 fn test_fixed_recovery_preserves_first_rollover_without_commit() {
5651 let executor = deterministic::Runner::default();
5652 executor.start(|context| async move {
5653 let cfg = test_cfg(NZU64!(1));
5654 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
5655 .await
5656 .unwrap();
5657
5658 let appended;
5661 (journal, appended) = journal.append(&test_digest(10)).await.unwrap();
5662 assert_eq!(appended, 0);
5663 let appended;
5664 (journal, appended) = journal.append(&test_digest(20)).await.unwrap();
5665 assert_eq!(appended, 1);
5666 drop(journal);
5667
5668 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
5669 assert!(
5670 blobs.len() > 1,
5671 "expected multiple empty blobs, got {}",
5672 blobs.len()
5673 );
5674
5675 let journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
5676 .await
5677 .unwrap();
5678 assert_eq!(journal.bounds(), 0..2);
5679 drop(journal);
5680
5681 let blobs = scan_partition(&context, &blob_partition(&cfg)).await;
5683 assert_eq!(blobs.len(), 3);
5684
5685 let mut journal = Journal::<_, Digest>::init(context.child("recovered"), cfg.clone())
5686 .await
5687 .unwrap();
5688 let appended;
5689 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
5690 assert_eq!(appended, 2);
5691 assert_eq!(journal.read(2).await.unwrap(), test_digest(42));
5692 journal.destroy().await.unwrap();
5693 });
5694 }
5695
5696 #[test_traced]
5698 fn test_fixed_recovery_validates_pages_before_trailing_bytes() {
5699 let executor = deterministic::Runner::default();
5700 executor.start(|context| async move {
5701 const LOGICAL_PAGE_SIZE: u64 = 5;
5702
5703 let cfg = Config {
5704 partition: "fixed-validate-before-tail-trim".into(),
5705 items_per_blob: NZU64!(10),
5706 page_cache: CacheRef::new(NZU16!(LOGICAL_PAGE_SIZE as u16), NZUsize!(4)),
5707 write_buffer: NZUsize!(128),
5708 replay_buffer: NZUsize!(128),
5709 };
5710 let partition = blob_partition(&cfg);
5711 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
5712 let mut writer = PagedRecovery::open(blob, size, 128, cfg.page_cache.clone())
5713 .await
5714 .unwrap();
5715 let values = [11u64, 22, 33, 44];
5716 let mut bytes = Vec::new();
5717 for value in values {
5718 bytes.extend_from_slice(&value.to_be_bytes());
5719 }
5720 (writer, _) = writer.append(&bytes).await.unwrap();
5721 writer = writer.truncate(30).await.unwrap();
5722 writer = writer.sync().await.unwrap();
5723 drop(writer);
5724
5725 corrupt_page(
5737 &context,
5738 &partition,
5739 &0u64.to_be_bytes(),
5740 4,
5741 LOGICAL_PAGE_SIZE,
5742 )
5743 .await;
5744
5745 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
5746 .await
5747 .unwrap();
5748 assert_eq!(journal.bounds(), 0..2);
5749 assert_eq!(journal.read(0).await.unwrap(), 11);
5750 assert_eq!(journal.read(1).await.unwrap(), 22);
5751 journal.destroy().await.unwrap();
5752 });
5753 }
5754
5755 #[test_traced]
5758 fn test_fixed_recovery_watermark_uses_retained_blob_start() {
5759 let executor = deterministic::Runner::default();
5760 executor.start(|context| async move {
5761 let cfg = test_cfg(NZU64!(10));
5762 let mut journal =
5763 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
5764 .await
5765 .unwrap();
5766 for (offset, value) in [11u64, 22].into_iter().enumerate() {
5767 let position;
5768 (journal, position) = journal.append(&value).await.unwrap();
5769 assert_eq!(position, 7 + offset as u64);
5770 }
5771 journal = journal.sync().await.unwrap();
5772 drop(journal);
5773
5774 let partition = blob_partition(&cfg);
5775 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
5776 let mut writer =
5777 PagedRecovery::open(blob, size, cfg.write_buffer.get(), cfg.page_cache.clone())
5778 .await
5779 .unwrap();
5780 assert_eq!(writer.size(), 16);
5781 (writer, _) = writer.append(&[0; 4]).await.unwrap();
5782 assert_eq!(writer.size(), 20);
5783 writer = writer.sync().await.unwrap();
5784 drop(writer);
5785
5786 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
5787 .await
5788 .unwrap();
5789 assert_eq!(journal.bounds(), 7..9);
5790 assert_eq!(journal.read(7).await.unwrap(), 11);
5791 assert_eq!(journal.read(8).await.unwrap(), 22);
5792 journal.destroy().await.unwrap();
5793 });
5794 }
5795
5796 #[test_traced]
5800 fn test_fixed_recovery_truncates_partial_item_tail() {
5801 let executor = deterministic::Runner::default();
5802 executor.start(|context| async move {
5803 let cfg = test_cfg(NZU64!(10));
5804 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5805 .await
5806 .unwrap();
5807 for value in [11u64, 22, 33] {
5808 (journal, _) = journal.append(&value).await.unwrap();
5809 }
5810 journal = journal.sync().await.unwrap();
5811 drop(journal);
5812
5813 let partition = blob_partition(&cfg);
5816 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
5817 let mut writer =
5818 Writer::new(blob, size, cfg.write_buffer.get(), cfg.page_cache.clone())
5819 .await
5820 .unwrap();
5821 assert_eq!(writer.size(), 24);
5822 (writer, _) = writer.append(&44u64.to_be_bytes()[..3]).await.unwrap();
5823 writer = writer.sync().await.unwrap();
5824 drop(writer);
5825
5826 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg)
5828 .await
5829 .unwrap();
5830 assert_eq!(journal.bounds(), 0..3);
5831 assert_eq!(journal.read(2).await.unwrap(), 33);
5832 let position;
5833 (journal, position) = journal.append(&44u64).await.unwrap();
5834 assert_eq!(position, 3);
5835 let journal = journal.sync().await.unwrap();
5836 assert_eq!(journal.read(3).await.unwrap(), 44);
5837 journal.destroy().await.unwrap();
5838 });
5839 }
5840
5841 #[test_traced]
5845 fn test_fixed_recovery_truncates_above_mid_blob_watermark() {
5846 let executor = deterministic::Runner::default();
5847 executor.start(|context| async move {
5848 const LOGICAL_PAGE_SIZE: u64 = 5;
5849 let cfg = Config {
5850 partition: "fixed-truncate-above-watermark".into(),
5851 items_per_blob: NZU64!(10),
5852 page_cache: CacheRef::new(NZU16!(LOGICAL_PAGE_SIZE as u16), NZUsize!(4)),
5853 write_buffer: NZUsize!(128),
5854 replay_buffer: NZUsize!(128),
5855 };
5856 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5857 .await
5858 .unwrap();
5859 for value in [11u64, 22] {
5860 (journal, _) = journal.append(&value).await.unwrap();
5861 }
5862 journal.sync().await.unwrap();
5863
5864 let partition = blob_partition(&cfg);
5868 let (blob, size) = context.open(&partition, &0u64.to_be_bytes()).await.unwrap();
5869 let mut writer = Writer::new(blob, size, 128, cfg.page_cache.clone())
5870 .await
5871 .unwrap();
5872 assert_eq!(writer.size(), 16);
5873 let mut bytes = Vec::new();
5874 for value in [33u64, 44] {
5875 bytes.extend_from_slice(&value.to_be_bytes());
5876 }
5877 (writer, _) = writer.append(&bytes).await.unwrap();
5878 writer = writer.sync().await.unwrap();
5879 drop(writer);
5880 corrupt_page(
5881 &context,
5882 &partition,
5883 &0u64.to_be_bytes(),
5884 1,
5885 LOGICAL_PAGE_SIZE,
5886 )
5887 .await;
5888 corrupt_page(
5889 &context,
5890 &partition,
5891 &0u64.to_be_bytes(),
5892 5,
5893 LOGICAL_PAGE_SIZE,
5894 )
5895 .await;
5896
5897 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
5900 .await
5901 .unwrap();
5902 assert_eq!(journal.bounds(), 0..3);
5903 assert!(journal.read(0).await.is_err());
5904 assert!(journal.read(1).await.is_err());
5905 assert_eq!(journal.read(2).await.unwrap(), 33);
5906 journal.destroy().await.unwrap();
5907 });
5908 }
5909
5910 #[test_traced]
5914 fn test_fixed_recovery_truncates_torn_interior_page() {
5915 let executor = deterministic::Runner::default();
5916 executor.start(|context| async move {
5917 let cfg = test_cfg(NZU64!(10));
5918 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5919 .await
5920 .unwrap();
5921 for i in 0..15u64 {
5922 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5923 }
5924 journal.commit().await.unwrap();
5925
5926 corrupt_page(
5928 &context,
5929 &blob_partition(&cfg),
5930 &0u64.to_be_bytes(),
5931 3,
5932 PAGE_SIZE.get() as u64,
5933 )
5934 .await;
5935
5936 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
5938 .await
5939 .unwrap();
5940 assert_eq!(journal.bounds(), 0..4);
5941 for i in 0..4u64 {
5942 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5943 }
5944 let appended;
5945 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
5946 assert_eq!(appended, 4);
5947 journal.destroy().await.unwrap();
5948 });
5949 }
5950
5951 #[test_traced]
5954 fn test_fixed_recovery_truncates_torn_interior_page_in_tail() {
5955 let executor = deterministic::Runner::default();
5956 executor.start(|context| async move {
5957 let cfg = test_cfg(NZU64!(10));
5958 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
5959 .await
5960 .unwrap();
5961 for i in 0..15u64 {
5962 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
5963 }
5964 journal.commit().await.unwrap();
5965
5966 corrupt_page(
5968 &context,
5969 &blob_partition(&cfg),
5970 &1u64.to_be_bytes(),
5971 1,
5972 PAGE_SIZE.get() as u64,
5973 )
5974 .await;
5975
5976 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
5978 .await
5979 .unwrap();
5980 assert_eq!(journal.bounds(), 0..11);
5981 for i in 0..11u64 {
5982 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
5983 }
5984 let appended;
5985 (journal, appended) = journal.append(&test_digest(42)).await.unwrap();
5986 assert_eq!(appended, 11);
5987 journal.destroy().await.unwrap();
5988 });
5989 }
5990
5991 #[test_traced]
5997 fn test_fixed_bounded_recovery_repairs_torn_rewrite_at_cap() {
5998 let executor = deterministic::Runner::default();
5999 executor.start(|context| async move {
6000 const PAGE: u64 = 100;
6001 let cfg = Config {
6002 partition: "torn-rewrite-at-cap".into(),
6003 items_per_blob: NZU64!(64),
6004 page_cache: CacheRef::new(NZU16!(PAGE as u16), NZUsize!(3)),
6005 write_buffer: NZUsize!(2048),
6006 replay_buffer: NZUsize!(2048),
6007 };
6008 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6009 .await
6010 .unwrap();
6011 for i in 0..33u64 {
6012 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6013 }
6014
6015 journal = journal.sync().await.unwrap();
6017 for i in 33..38u64 {
6018 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6019 }
6020
6021 journal.commit().await.unwrap();
6024 tear_page_rewrite(
6025 &context,
6026 &blob_partition(&cfg),
6027 &0u64.to_be_bytes(),
6028 10,
6029 60,
6030 PAGE,
6031 )
6032 .await;
6033
6034 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
6036 .await
6037 .unwrap();
6038 let recovery = Recovery::<_, Digest>::open(
6039 context.child("capped"),
6040 cfg.clone(),
6041 checkpoint,
6042 Some(32),
6043 )
6044 .await
6045 .unwrap();
6046 assert_eq!(recovery.bounds, 0..32);
6047 assert_eq!(recovery.item(31).await.unwrap(), test_digest(31));
6048 assert_eq!(
6049 authenticated::BackingRecovery::last_matching(&recovery, 32, |_| true)
6050 .await
6051 .unwrap(),
6052 32
6053 );
6054 let journal = Journal(Box::new(recovery.publish(32).await.unwrap()));
6055 assert_eq!(journal.bounds(), 0..32);
6056 assert_eq!(journal.read(31).await.unwrap(), test_digest(31));
6057 journal.destroy().await.unwrap();
6058 });
6059 }
6060
6061 #[test_traced]
6066 fn test_fixed_recovery_adopts_torn_page_below_watermark() {
6067 let executor = deterministic::Runner::default();
6068 executor.start(|context| async move {
6069 let cfg = test_cfg(NZU64!(10));
6070 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6071 .await
6072 .unwrap();
6073 for i in 0..15u64 {
6074 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6075 }
6076 journal.sync().await.unwrap();
6077
6078 corrupt_page(
6079 &context,
6080 &blob_partition(&cfg),
6081 &0u64.to_be_bytes(),
6082 3,
6083 PAGE_SIZE.get() as u64,
6084 )
6085 .await;
6086 let size_before = context
6087 .logical_blob(&blob_partition(&cfg), &0u64.to_be_bytes())
6088 .unwrap()
6089 .len();
6090
6091 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
6092 .await
6093 .expect("acknowledged damage must not fail recovery");
6094
6095 let size_after = context
6098 .logical_blob(&blob_partition(&cfg), &0u64.to_be_bytes())
6099 .unwrap()
6100 .len();
6101 assert_eq!(
6102 size_after, size_before,
6103 "adoption must preserve the evidence"
6104 );
6105 let mut damaged = 0;
6106 for i in 0..10u64 {
6107 match journal.read(i).await {
6108 Ok(item) => assert_eq!(item, test_digest(i)),
6109 Err(_) => damaged += 1,
6110 }
6111 }
6112 assert!(damaged > 0, "the torn page must surface as read errors");
6113 for i in 10..15u64 {
6114 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6115 }
6116 drop(journal);
6117
6118 let _ = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
6120 .await
6121 .unwrap();
6122 let size_retry = context
6123 .logical_blob(&blob_partition(&cfg), &0u64.to_be_bytes())
6124 .unwrap()
6125 .len();
6126 assert_eq!(size_retry, size_before);
6127 });
6128 }
6129
6130 #[test_traced]
6135 fn test_fixed_recovery_skips_watermark_covered_blobs() {
6136 let executor = deterministic::Runner::default();
6137 executor.start(|context| async move {
6138 let cfg = test_cfg(NZU64!(10));
6139 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6140 .await
6141 .unwrap();
6142 for i in 0..15u64 {
6143 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6144 }
6145 journal.sync().await.unwrap();
6146
6147 corrupt_page(
6150 &context,
6151 &blob_partition(&cfg),
6152 &0u64.to_be_bytes(),
6153 2,
6154 u64::from(PAGE_SIZE.get()),
6155 )
6156 .await;
6157
6158 let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
6159 .await
6160 .unwrap();
6161 assert_eq!(journal.bounds(), 0..15);
6162 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
6163 assert!(journal.read(2).await.is_err());
6164 assert_eq!(journal.read(14).await.unwrap(), test_digest(14));
6165 journal.destroy().await.unwrap();
6166 });
6167 }
6168
6169 #[test_traced]
6172 fn test_fixed_recovery_adopts_torn_page_below_mid_blob_watermark() {
6173 let executor = deterministic::Runner::default();
6174 executor.start(|context| async move {
6175 let cfg = test_cfg(NZU64!(10));
6176 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6177 .await
6178 .unwrap();
6179 for i in 0..15u64 {
6180 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6181 }
6182 journal.sync().await.unwrap();
6184
6185 corrupt_page(
6187 &context,
6188 &blob_partition(&cfg),
6189 &1u64.to_be_bytes(),
6190 1,
6191 PAGE_SIZE.get() as u64,
6192 )
6193 .await;
6194 let (_, size_before) = context
6195 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
6196 .await
6197 .unwrap();
6198
6199 for child in ["second", "retry"] {
6200 let journal = Journal::<_, Digest>::init(context.child(child), cfg.clone())
6201 .await
6202 .expect("acknowledged damage must not fail recovery");
6203 let mut damaged = 0;
6204 for i in 10..15u64 {
6205 match journal.read(i).await {
6206 Ok(item) => assert_eq!(item, test_digest(i)),
6207 Err(_) => damaged += 1,
6208 }
6209 }
6210 assert!(damaged > 0, "the torn page must surface as read errors");
6211 for i in 0..10u64 {
6212 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6213 }
6214 }
6215
6216 let (_, size_after) = context
6218 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
6219 .await
6220 .unwrap();
6221 assert_eq!(size_after, size_before);
6222 });
6223 }
6224
6225 #[test_traced]
6230 fn test_fixed_recovery_rejects_torn_boundary_page() {
6231 let executor = deterministic::Runner::default();
6232 executor.start(|context| async move {
6233 let cfg = test_cfg(NZU64!(10));
6234 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6235 .await
6236 .unwrap();
6237 for i in 0..15u64 {
6238 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6239 }
6240
6241 journal.sync().await.unwrap();
6243
6244 let physical_page_size = PAGE_SIZE.get() as u64 + 12;
6246 let offset = 3 * physical_page_size + 5;
6247 let (blob, _) = context
6248 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
6249 .await
6250 .unwrap();
6251 let byte = blob
6252 .read_at(offset, 1, commonware_runtime::ReadOptions::default())
6253 .await
6254 .unwrap()
6255 .coalesce();
6256 blob.write_at(
6257 offset,
6258 vec![byte.as_ref()[0] ^ 0xFF],
6259 WriteOptions::default(),
6260 )
6261 .await
6262 .unwrap();
6263 blob.sync().await.unwrap();
6264 drop(blob);
6265
6266 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
6267 assert!(matches!(result, Err(Error::Corruption(_))));
6268 let (_, size_mid) = context
6269 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
6270 .await
6271 .unwrap();
6272
6273 let result = Journal::<_, Digest>::init(context.child("retry"), cfg.clone()).await;
6275 assert!(matches!(result, Err(Error::Corruption(_))));
6276 let (_, size_after) = context
6277 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
6278 .await
6279 .unwrap();
6280 assert_eq!(size_after, size_mid);
6281 });
6282 }
6283
6284 #[test_traced]
6287 fn test_fixed_recovery_unsynced_tail_keeps_contiguous_prefix() {
6288 let executor = deterministic::Runner::default();
6289 executor.start(|context| async move {
6290 let cfg = test_cfg(NZU64!(10));
6291 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6292 .await
6293 .unwrap();
6294
6295 for i in 0..25u64 {
6298 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6299 }
6300
6301 {
6304 journal = journal.test_sync_blob(0).await.unwrap();
6305 journal = journal.test_sync_blob(1).await.unwrap();
6306 }
6307 drop(journal);
6308
6309 let names = scan_partition(&context, &blob_partition(&cfg)).await;
6312 assert_eq!(names.len(), 3);
6313 for (blob, name) in names.iter().enumerate() {
6314 let (_, size) = context.open(&blob_partition(&cfg), name).await.unwrap();
6315 if blob < 2 {
6316 assert!(size > 0, "blob {blob} should be durable");
6317 } else {
6318 assert_eq!(size, 0, "blob {blob} should be empty");
6319 }
6320 }
6321
6322 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
6324 .await
6325 .unwrap();
6326 assert_eq!(journal.bounds(), 0..20);
6327 for i in 0..20u64 {
6328 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6329 }
6330 assert!(matches!(
6331 journal.read(20).await,
6332 Err(Error::ItemOutOfRange(20))
6333 ));
6334
6335 let appended;
6337 (journal, appended) = journal.append(&test_digest(999)).await.unwrap();
6338 assert_eq!(appended, 20);
6339 assert_eq!(journal.read(20).await.unwrap(), test_digest(999));
6340
6341 journal.destroy().await.unwrap();
6342 });
6343 }
6344
6345 #[test_traced]
6354 fn test_fixed_recovery_rolls_back_durable_blob_after_gap() {
6355 let executor = deterministic::Runner::default();
6356 executor.start(|context| async move {
6357 let cfg = test_cfg(NZU64!(10));
6358 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6359 .await
6360 .unwrap();
6361
6362 for i in 0..10u64 {
6364 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6365 }
6366 journal = journal.sync().await.unwrap();
6367
6368 for i in 10..28u64 {
6371 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6372 }
6373 {
6374 journal = journal.test_sync_blob(2).await.unwrap();
6375 }
6376 drop(journal);
6377 let (blob, _) = context
6378 .open(&blob_partition(&cfg), &1u64.to_be_bytes())
6379 .await
6380 .unwrap();
6381 blob.resize(0).await.unwrap();
6382 blob.sync().await.unwrap();
6383 drop(blob);
6384
6385 let names = scan_partition(&context, &blob_partition(&cfg)).await;
6387 assert_eq!(names.len(), 3);
6388 let mut sizes = Vec::new();
6389 for name in &names {
6390 let (_, size) = context.open(&blob_partition(&cfg), name).await.unwrap();
6391 sizes.push(size);
6392 }
6393 assert!(sizes[0] > 0, "blob 0 should be durable");
6394 assert_eq!(sizes[1], 0, "blob 1 should be the gap");
6395 assert!(sizes[2] > 0, "blob 2 should be incidentally durable");
6396
6397 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
6400 .await
6401 .unwrap();
6402 assert_eq!(journal.bounds(), 0..10);
6403 for i in 0..10u64 {
6404 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6405 }
6406 assert!(matches!(
6407 journal.read(10).await,
6408 Err(Error::ItemOutOfRange(10))
6409 ));
6410
6411 let names = scan_partition(&context, &blob_partition(&cfg)).await;
6413 assert_eq!(names.len(), 2);
6414
6415 let appended;
6417 (journal, appended) = journal.append(&test_digest(999)).await.unwrap();
6418 assert_eq!(appended, 10);
6419 assert_eq!(journal.read(10).await.unwrap(), test_digest(999));
6420
6421 journal.destroy().await.unwrap();
6422 });
6423 }
6424
6425 #[test_traced]
6430 fn test_fixed_recovery_prune_crash_retains_unsynced_tail() {
6431 let executor = deterministic::Runner::default();
6432 executor.start(|context| async move {
6433 let cfg = test_cfg(NZU64!(10));
6434 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6435 .await
6436 .unwrap();
6437
6438 for i in 0..10u64 {
6441 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6442 }
6443 journal = journal.sync().await.unwrap();
6444 for i in 10..25u64 {
6445 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6446 }
6447
6448 let (journal, pruned) = journal.prune(10).await.unwrap();
6450 assert!(pruned);
6451 drop(journal);
6452
6453 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
6454 .await
6455 .unwrap();
6456 assert_eq!(journal.bounds(), 10..25);
6457 for i in 10..25u64 {
6458 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6459 }
6460 journal.destroy().await.unwrap();
6461 });
6462 }
6463
6464 #[test_traced]
6472 fn test_fixed_recovery_empty_oldest_blob_orphaned_newer_blob() {
6473 let executor = deterministic::Runner::default();
6474 executor.start(|context| async move {
6475 let cfg = test_cfg(NZU64!(10));
6476
6477 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
6479 .await
6480 .unwrap();
6481 for i in 0..20u64 {
6482 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
6483 }
6484 let journal = journal.sync().await.unwrap();
6485 drop(journal);
6486
6487 let (blob0, size0) = context
6490 .open(&blob_partition(&cfg), &0u64.to_be_bytes())
6491 .await
6492 .unwrap();
6493 assert!(size0 > 0);
6494 blob0.resize(0).await.unwrap();
6495 blob0.sync().await.unwrap();
6496
6497 drop(blob0);
6498 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
6499 assert!(matches!(result, Err(Error::Corruption(_))));
6500 });
6501 }
6502
6503 fn single_item_per_blob_config() -> Config {
6505 Config {
6506 partition: "single-item-per-blob".into(),
6507 items_per_blob: NZU64!(1),
6508 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
6509 write_buffer: NZUsize!(2048),
6510 replay_buffer: NZUsize!(2048),
6511 }
6512 }
6513
6514 #[test_traced]
6518 fn test_single_item_per_blob() {
6519 let executor = deterministic::Runner::default();
6520 executor.start(|context| async move {
6521 let cfg = single_item_per_blob_config();
6522
6523 let mut journal = Journal::init(context.child("first"), cfg.clone())
6525 .await
6526 .expect("failed to initialize journal");
6527
6528 let bounds = journal.bounds();
6530 assert_eq!(bounds.end, 0);
6531 assert!(bounds.is_empty());
6532
6533 let pos;
6535 (journal, pos) = journal
6536 .append(&test_digest(0))
6537 .await
6538 .expect("failed to append");
6539 assert_eq!(pos, 0);
6540 assert_eq!(journal.size(), 1);
6541
6542 journal = journal.sync().await.expect("failed to sync");
6544
6545 let value = journal
6547 .read(journal.size() - 1)
6548 .await
6549 .expect("failed to read");
6550 assert_eq!(value, test_digest(0));
6551
6552 for i in 1..10u64 {
6554 let pos;
6555 (journal, pos) = journal
6556 .append(&test_digest(i))
6557 .await
6558 .expect("failed to append");
6559 assert_eq!(pos, i);
6560 assert_eq!(journal.size(), i + 1);
6561
6562 let value = journal
6564 .read(journal.size() - 1)
6565 .await
6566 .expect("failed to read");
6567 assert_eq!(value, test_digest(i));
6568 }
6569
6570 for i in 0..10u64 {
6572 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6573 }
6574
6575 journal = journal.sync().await.expect("failed to sync");
6578 (journal, _) = journal.prune(5).await.expect("failed to prune");
6579
6580 assert_eq!(journal.size(), 10);
6582
6583 assert_eq!(journal.bounds().start, 5);
6585
6586 let value = journal
6588 .read(journal.size() - 1)
6589 .await
6590 .expect("failed to read");
6591 assert_eq!(value, test_digest(9));
6592
6593 for i in 0..5 {
6595 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
6596 }
6597
6598 for i in 5..10u64 {
6600 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6601 }
6602
6603 for i in 10..15u64 {
6605 let pos;
6606 (journal, pos) = journal
6607 .append(&test_digest(i))
6608 .await
6609 .expect("failed to append");
6610 assert_eq!(pos, i);
6611
6612 let value = journal
6614 .read(journal.size() - 1)
6615 .await
6616 .expect("failed to read");
6617 assert_eq!(value, test_digest(i));
6618 }
6619
6620 journal.sync().await.expect("failed to sync");
6622 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
6623 .await
6624 .expect("failed to re-initialize journal");
6625
6626 assert_eq!(journal.size(), 15);
6628
6629 assert_eq!(journal.bounds().start, 5);
6631
6632 let value = journal
6634 .read(journal.size() - 1)
6635 .await
6636 .expect("failed to read");
6637 assert_eq!(value, test_digest(14));
6638
6639 for i in 5..15u64 {
6641 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
6642 }
6643
6644 journal.destroy().await.expect("failed to destroy journal");
6645
6646 let mut journal = Journal::<_, Digest>::init(context.child("after_destroy"), cfg)
6649 .await
6650 .expect("failed to re-initialize journal");
6651 assert!(journal.bounds().is_empty());
6652 assert_eq!(journal.size(), 0);
6653 for i in 0..10u64 {
6654 (journal, _) = journal.append(&test_digest(i + 1000)).await.unwrap();
6655 }
6656 journal = journal.sync().await.unwrap();
6657 assert_eq!(journal.bounds().end, 10);
6658 for i in 0..10u64 {
6659 assert_eq!(journal.read(i).await.unwrap(), test_digest(i + 1000));
6660 }
6661 journal.destroy().await.expect("failed to destroy journal");
6662 });
6663 }
6664
6665 #[test_traced]
6668 fn test_single_item_per_blob_restart_after_prune() {
6669 let executor = deterministic::Runner::default();
6670 executor.start(|context| async move {
6671 let cfg = single_item_per_blob_config();
6672
6673 let mut journal = Journal::init(context.child("third"), cfg.clone())
6674 .await
6675 .expect("failed to initialize journal");
6676
6677 for i in 0..10u64 {
6679 (journal, _) = journal.append(&test_digest(i + 100)).await.unwrap();
6680 }
6681
6682 (journal, _) = journal.prune(5).await.unwrap();
6684 let bounds = journal.bounds();
6685 assert_eq!(bounds.end, 10);
6686 assert_eq!(bounds.start, 5);
6687
6688 journal.sync().await.unwrap();
6690
6691 let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
6693 .await
6694 .expect("failed to re-initialize journal");
6695
6696 let bounds = journal.bounds();
6698 assert_eq!(bounds.end, 10);
6699 assert_eq!(bounds.start, 5);
6700
6701 let value = journal.read(journal.size() - 1).await.unwrap();
6703 assert_eq!(value, test_digest(109));
6704
6705 for i in 5..10u64 {
6707 assert_eq!(journal.read(i).await.unwrap(), test_digest(i + 100));
6708 }
6709
6710 journal.destroy().await.expect("failed to destroy journal");
6711
6712 let mut journal = Journal::<_, Digest>::init(context.child("after_destroy"), cfg)
6715 .await
6716 .expect("failed to re-initialize journal");
6717 assert!(journal.bounds().is_empty());
6718 assert_eq!(journal.size(), 0);
6719 for i in 0..10u64 {
6720 (journal, _) = journal.append(&test_digest(i + 1000)).await.unwrap();
6721 }
6722 journal = journal.sync().await.unwrap();
6723 assert_eq!(journal.bounds().end, 10);
6724 for i in 0..10u64 {
6725 assert_eq!(journal.read(i).await.unwrap(), test_digest(i + 1000));
6726 }
6727 journal.destroy().await.expect("failed to destroy journal");
6728 });
6729 }
6730
6731 #[test_traced]
6734 fn test_single_item_per_blob_prune_all() {
6735 let executor = deterministic::Runner::default();
6736 executor.start(|context| async move {
6737 let cfg = single_item_per_blob_config();
6738
6739 let mut journal = Journal::init(context.child("storage"), cfg.clone())
6740 .await
6741 .expect("failed to initialize journal");
6742
6743 for i in 0..5u64 {
6744 (journal, _) = journal.append(&test_digest(i + 200)).await.unwrap();
6745 }
6746 journal = journal.sync().await.unwrap();
6747
6748 (journal, _) = journal.prune(5).await.unwrap();
6750 let bounds = journal.bounds();
6751 assert_eq!(bounds.end, 5); assert!(bounds.is_empty()); let result = journal.read(journal.size() - 1).await;
6756 assert!(matches!(result, Err(Error::ItemPruned(4))));
6757
6758 (journal, _) = journal.append(&test_digest(205)).await.unwrap();
6760 assert_eq!(journal.bounds().start, 5);
6761 assert_eq!(
6762 journal.read(journal.size() - 1).await.unwrap(),
6763 test_digest(205)
6764 );
6765
6766 journal.destroy().await.expect("failed to destroy journal");
6767 });
6768 }
6769
6770 #[test_traced]
6771 fn test_fixed_journal_init_at_size_zero() {
6772 let executor = deterministic::Runner::default();
6773 executor.start(|context| async move {
6774 let cfg = test_cfg(NZU64!(5));
6775 let mut journal =
6776 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 0)
6777 .await
6778 .unwrap();
6779
6780 let bounds = journal.bounds();
6781 assert_eq!(bounds.end, 0);
6782 assert!(bounds.is_empty());
6783
6784 let pos;
6786 (journal, pos) = journal.append(&test_digest(100)).await.unwrap();
6787 assert_eq!(pos, 0);
6788 assert_eq!(journal.size(), 1);
6789 assert_eq!(journal.read(0).await.unwrap(), test_digest(100));
6790
6791 journal.destroy().await.unwrap();
6792 });
6793 }
6794
6795 #[test_traced]
6796 fn test_fixed_journal_init_at_max_size_rejected() {
6797 let executor = deterministic::Runner::default();
6798 executor.start(|context| async move {
6799 let mut cfg = test_cfg(NZU64!(1));
6800 cfg.partition = "max-size-rejected".into();
6801
6802 assert!(matches!(
6804 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg, u64::MAX).await,
6805 Err(Error::SizeOverflow)
6806 ));
6807 });
6808 }
6809
6810 #[test_traced]
6811 fn test_fixed_journal_append_size_overflow() {
6812 let executor = deterministic::Runner::default();
6813 executor.start(|context| async move {
6814 let mut cfg = test_cfg(NZU64!(1));
6815 cfg.partition = "append-size-overflow".into();
6816
6817 let mut journal =
6819 Journal::<_, Digest>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
6820 .await
6821 .unwrap();
6822
6823 let appended;
6825 (journal, appended) = journal.append(&test_digest(7)).await.unwrap();
6826 assert_eq!(appended, u64::MAX - 1);
6827 assert_eq!(journal.size(), u64::MAX);
6828
6829 assert!(matches!(
6832 journal.append(&test_digest(8)).await,
6833 Err(Error::SizeOverflow)
6834 ));
6835 });
6836 }
6837
6838 #[test_traced]
6839 fn test_fixed_journal_replay_near_max_size() {
6840 let executor = deterministic::Runner::default();
6841 executor.start(|context| async move {
6842 let mut cfg = test_cfg(NZU64!(10));
6843 cfg.partition = "replay-near-max-size".into();
6844
6845 let mut journal =
6846 Journal::<_, Digest>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
6847 .await
6848 .unwrap();
6849 let expected = test_digest(7);
6850 let appended;
6851 (journal, appended) = journal.append(&expected).await.unwrap();
6852 assert_eq!(appended, u64::MAX - 1);
6853
6854 {
6855 let reader;
6856 (journal, reader) = journal.snapshot().await.unwrap();
6857 let stream = replay_items(
6858 &reader,
6859 u64::MAX - 1..reader.bounds().end,
6860 NZUsize!(1024),
6861 ReadOptions::default(),
6862 )
6863 .await
6864 .unwrap();
6865 pin_mut!(stream);
6866 let (pos, item) = stream.next().await.unwrap().unwrap();
6867 assert_eq!(pos, u64::MAX - 1);
6868 assert_eq!(item, expected);
6869 assert!(stream.next().await.is_none());
6870 }
6871
6872 journal.destroy().await.unwrap();
6873 });
6874 }
6875
6876 #[test_traced]
6877 fn test_fixed_journal_init_at_size_blob_boundary() {
6878 let executor = deterministic::Runner::default();
6879 executor.start(|context| async move {
6880 let cfg = test_cfg(NZU64!(5));
6881
6882 let mut journal =
6884 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10)
6885 .await
6886 .unwrap();
6887
6888 let bounds = journal.bounds();
6889 assert_eq!(bounds.end, 10);
6890 assert!(bounds.is_empty());
6891
6892 let pos;
6894 (journal, pos) = journal.append(&test_digest(1000)).await.unwrap();
6895 assert_eq!(pos, 10);
6896 assert_eq!(journal.size(), 11);
6897 assert_eq!(journal.read(10).await.unwrap(), test_digest(1000));
6898
6899 let pos;
6901 (journal, pos) = journal.append(&test_digest(1001)).await.unwrap();
6902 assert_eq!(pos, 11);
6903 assert_eq!(journal.read(11).await.unwrap(), test_digest(1001));
6904
6905 journal.destroy().await.unwrap();
6906 });
6907 }
6908
6909 #[test_traced]
6910 fn test_fixed_journal_init_at_size_mid_blob() {
6911 let executor = deterministic::Runner::default();
6912 executor.start(|context| async move {
6913 let cfg = test_cfg(NZU64!(5));
6914
6915 let mut journal =
6917 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7)
6918 .await
6919 .unwrap();
6920
6921 let bounds = journal.bounds();
6922 assert_eq!(bounds.end, 7);
6923 assert!(bounds.is_empty());
6925
6926 assert!(matches!(journal.read(5).await, Err(Error::ItemPruned(5))));
6928 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
6929
6930 let pos;
6932 (journal, pos) = journal.append(&test_digest(700)).await.unwrap();
6933 assert_eq!(pos, 7);
6934 assert_eq!(journal.size(), 8);
6935 assert_eq!(journal.read(7).await.unwrap(), test_digest(700));
6936 assert_eq!(journal.bounds().start, 7);
6938
6939 journal.destroy().await.unwrap();
6940 });
6941 }
6942
6943 #[test_traced]
6944 fn test_fixed_journal_append_many_after_mid_blob_start() {
6945 let executor = deterministic::Runner::default();
6946 executor.start(|context| async move {
6947 let cfg = test_cfg(NZU64!(100));
6948 let mut journal =
6949 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 150)
6950 .await
6951 .unwrap();
6952
6953 let items: Vec<_> = (0..100u64).map(|i| test_digest(1500 + i)).collect();
6954 let last;
6955 (journal, last) = journal.append_many(Many::Flat(&items)).await.unwrap();
6956 assert_eq!(last, 249);
6957 assert_eq!(journal.bounds(), 150..250);
6958
6959 for (position, index) in [(150, 0), (199, 49), (200, 50), (249, 99)] {
6960 assert_eq!(
6961 journal.read(position).await.unwrap(),
6962 items[index],
6963 "item at position {position} did not match"
6964 );
6965 }
6966
6967 journal.sync().await.unwrap();
6968
6969 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
6970 .await
6971 .unwrap();
6972 assert_eq!(journal.bounds(), 150..250);
6973 for (position, index) in [(150, 0), (199, 49), (200, 50), (249, 99)] {
6974 assert_eq!(
6975 journal.read(position).await.unwrap(),
6976 items[index],
6977 "item at position {position} did not match after reopen"
6978 );
6979 }
6980
6981 journal.destroy().await.unwrap();
6982 });
6983 }
6984
6985 #[test_traced]
6986 fn test_fixed_journal_init_at_size_persistence() {
6987 let executor = deterministic::Runner::default();
6988 executor.start(|context| async move {
6989 let cfg = test_cfg(NZU64!(5));
6990
6991 let mut journal =
6993 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15)
6994 .await
6995 .unwrap();
6996
6997 for i in 0..5u64 {
6999 let pos;
7000 (journal, pos) = journal.append(&test_digest(1500 + i)).await.unwrap();
7001 assert_eq!(pos, 15 + i);
7002 }
7003
7004 assert_eq!(journal.size(), 20);
7005
7006 journal.sync().await.unwrap();
7008
7009 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7010 .await
7011 .unwrap();
7012
7013 let bounds = journal.bounds();
7015 assert_eq!(bounds.end, 20);
7016 assert_eq!(bounds.start, 15);
7017
7018 for i in 0..5u64 {
7020 assert_eq!(journal.read(15 + i).await.unwrap(), test_digest(1500 + i));
7021 }
7022
7023 let pos;
7025 (journal, pos) = journal.append(&test_digest(9999)).await.unwrap();
7026 assert_eq!(pos, 20);
7027 assert_eq!(journal.read(20).await.unwrap(), test_digest(9999));
7028
7029 journal.destroy().await.unwrap();
7030 });
7031 }
7032
7033 #[test_traced]
7034 fn test_fixed_journal_init_at_size_persistence_without_data() {
7035 let executor = deterministic::Runner::default();
7036 executor.start(|context| async move {
7037 let cfg = test_cfg(NZU64!(5));
7038
7039 let journal =
7041 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 15)
7042 .await
7043 .unwrap();
7044
7045 let bounds = journal.bounds();
7046 assert_eq!(bounds.end, 15);
7047 assert!(bounds.is_empty());
7048
7049 drop(journal);
7051
7052 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7054 .await
7055 .unwrap();
7056
7057 let bounds = journal.bounds();
7058 assert_eq!(bounds.end, 15);
7059 assert!(bounds.is_empty());
7060
7061 let pos;
7063 (journal, pos) = journal.append(&test_digest(1500)).await.unwrap();
7064 assert_eq!(pos, 15);
7065 assert_eq!(journal.read(15).await.unwrap(), test_digest(1500));
7066
7067 journal.destroy().await.unwrap();
7068 });
7069 }
7070
7071 #[test_traced]
7072 fn test_fixed_journal_init_at_size_large_offset() {
7073 let executor = deterministic::Runner::default();
7074 executor.start(|context| async move {
7075 let cfg = test_cfg(NZU64!(5));
7076
7077 let mut journal =
7079 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 1000)
7080 .await
7081 .unwrap();
7082
7083 let bounds = journal.bounds();
7084 assert_eq!(bounds.end, 1000);
7085 assert!(bounds.is_empty());
7086
7087 let pos;
7089 (journal, pos) = journal.append(&test_digest(100000)).await.unwrap();
7090 assert_eq!(pos, 1000);
7091 assert_eq!(journal.read(1000).await.unwrap(), test_digest(100000));
7092
7093 journal.destroy().await.unwrap();
7094 });
7095 }
7096
7097 #[test_traced]
7098 fn test_fixed_journal_init_at_size_prune_and_append() {
7099 let executor = deterministic::Runner::default();
7100 executor.start(|context| async move {
7101 let cfg = test_cfg(NZU64!(5));
7102
7103 let mut journal =
7105 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 20)
7106 .await
7107 .unwrap();
7108
7109 for i in 0..10u64 {
7111 (journal, _) = journal.append(&test_digest(2000 + i)).await.unwrap();
7112 }
7113
7114 assert_eq!(journal.size(), 30);
7115
7116 (journal, _) = journal.prune(25).await.unwrap();
7118
7119 let bounds = journal.bounds();
7120 assert_eq!(bounds.end, 30);
7121 assert_eq!(bounds.start, 25);
7122
7123 for i in 25..30u64 {
7125 assert_eq!(journal.read(i).await.unwrap(), test_digest(2000 + (i - 20)));
7126 }
7127
7128 let pos;
7130 (journal, pos) = journal.append(&test_digest(3000)).await.unwrap();
7131 assert_eq!(pos, 30);
7132
7133 journal.destroy().await.unwrap();
7134 });
7135 }
7136
7137 #[test_traced]
7138 fn test_fixed_journal_clear_to_size() {
7139 let executor = deterministic::Runner::default();
7140 executor.start(|context| async move {
7141 let cfg = test_cfg(NZU64!(10));
7142 let mut journal = Journal::init(context.child("journal"), cfg.clone())
7143 .await
7144 .expect("failed to initialize journal");
7145
7146 for i in 0..25u64 {
7148 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7149 }
7150 assert_eq!(journal.size(), 25);
7151 journal = journal.sync().await.unwrap();
7152
7153 journal.0 = journal.0.clear_to_size(100).await.unwrap();
7155 assert_eq!(journal.size(), 100);
7156
7157 for i in 0..25 {
7159 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
7160 }
7161
7162 drop(journal);
7164 let mut journal =
7165 Journal::<_, Digest>::init(context.child("journal_after_clear"), cfg.clone())
7166 .await
7167 .expect("failed to re-initialize journal after clear");
7168 assert_eq!(journal.size(), 100);
7169
7170 for i in 100..105u64 {
7172 let pos;
7173 (journal, pos) = journal.append(&test_digest(i)).await.unwrap();
7174 assert_eq!(pos, i);
7175 }
7176 assert_eq!(journal.size(), 105);
7177
7178 for i in 100..105u64 {
7180 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
7181 }
7182
7183 journal.sync().await.unwrap();
7185
7186 let journal = Journal::<_, Digest>::init(context.child("journal_reopened"), cfg)
7187 .await
7188 .expect("failed to re-initialize journal");
7189
7190 assert_eq!(journal.size(), 105);
7191 for i in 100..105u64 {
7192 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
7193 }
7194
7195 journal.destroy().await.unwrap();
7196 });
7197 }
7198
7199 #[test_traced]
7200 fn test_fixed_journal_clear_to_size_rejects_max() {
7201 let executor = deterministic::Runner::default();
7204 executor.start(|context| async move {
7205 let cfg = test_cfg(NZU64!(10));
7206 let journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
7207 .await
7208 .unwrap();
7209 assert!(matches!(
7210 journal.0.clear_to_size(u64::MAX).await,
7211 Err(Error::SizeOverflow)
7212 ));
7213 let journal = Journal::<_, Digest>::init(context.child("journal_intent"), cfg)
7215 .await
7216 .unwrap();
7217 assert!(matches!(
7218 journal.0.stage_clear_intent(u64::MAX).await,
7219 Err(Error::SizeOverflow)
7220 ));
7221 });
7222 }
7223
7224 #[test_traced]
7225 fn test_fixed_journal_clear_to_size_rejects_backward_without_mutation() {
7226 let executor = deterministic::Runner::default();
7227 executor.start(|context| async move {
7228 let cfg = test_cfg(NZU64!(10));
7229 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
7230 .await
7231 .unwrap();
7232 for i in 0..2 {
7233 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7234 }
7235 let journal = journal.sync().await.unwrap();
7236
7237 assert!(matches!(
7238 journal.0.clear_to_size(1).await,
7239 Err(Error::ItemOutOfRange(1))
7240 ));
7241
7242 let journal = Journal::<_, Digest>::init(context.child("after_clear"), cfg.clone())
7243 .await
7244 .unwrap();
7245 assert_eq!(journal.bounds(), 0..2);
7246 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
7247 assert_eq!(journal.read(1).await.unwrap(), test_digest(1));
7248 assert!(matches!(
7249 journal.0.stage_clear_intent(1).await,
7250 Err(Error::ItemOutOfRange(1))
7251 ));
7252
7253 let journal = Journal::<_, Digest>::init(context.child("after_intent"), cfg)
7254 .await
7255 .unwrap();
7256 assert_eq!(journal.bounds(), 0..2);
7257 assert_eq!(journal.read(0).await.unwrap(), test_digest(0));
7258 assert_eq!(journal.read(1).await.unwrap(), test_digest(1));
7259 journal.destroy().await.unwrap();
7260 });
7261 }
7262
7263 #[test_traced]
7264 fn test_fixed_journal_sync_crash_meta_none_boundary_aligned() {
7265 let executor = deterministic::Runner::default();
7267 executor.start(|context| async move {
7268 let cfg = test_cfg(NZU64!(5));
7269 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
7270 .await
7271 .unwrap();
7272
7273 for i in 0..5u64 {
7274 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7275 }
7276 journal.commit().await.unwrap();
7277
7278 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7279 .await
7280 .unwrap();
7281 let bounds = journal.bounds();
7282 assert_eq!(bounds.start, 0);
7283 assert_eq!(bounds.end, 5);
7284 journal.destroy().await.unwrap();
7285 });
7286 }
7287
7288 #[test_traced]
7289 fn test_fixed_journal_missing_metadata_with_short_blob_is_corruption() {
7290 let executor = deterministic::Runner::default();
7293 executor.start(|context| async move {
7294 let cfg = test_cfg(NZU64!(5));
7295 let mut journal =
7296 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
7297 .await
7298 .unwrap();
7299 for i in 0..3u64 {
7300 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7301 }
7302 let mut journal = journal.sync().await.unwrap();
7303
7304 journal.0.checkpoint.clear();
7306 journal.0.checkpoint = journal.0.checkpoint.sync().await.unwrap();
7307 drop(journal);
7308
7309 let result = Journal::<_, Digest>::init(context.child("second"), cfg.clone()).await;
7310 assert!(matches!(result, Err(Error::Corruption(_))));
7311 });
7312 }
7313
7314 #[test_traced]
7315 fn test_fixed_journal_sync_crash_meta_mid_boundary_unchanged() {
7316 let executor = deterministic::Runner::default();
7318 executor.start(|context| async move {
7319 let cfg = test_cfg(NZU64!(5));
7320 let mut journal =
7321 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
7322 .await
7323 .unwrap();
7324 for i in 0..3u64 {
7325 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7326 }
7327 journal.commit().await.unwrap();
7328
7329 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7330 .await
7331 .unwrap();
7332 let bounds = journal.bounds();
7333 assert_eq!(bounds.start, 7);
7334 assert_eq!(bounds.end, 10);
7335 journal.destroy().await.unwrap();
7336 });
7337 }
7338 #[test_traced]
7339 fn test_fixed_journal_sync_crash_meta_mid_to_aligned_becomes_stale() {
7340 let executor = deterministic::Runner::default();
7342 executor.start(|context| async move {
7343 let cfg = test_cfg(NZU64!(5));
7344 let mut journal =
7345 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
7346 .await
7347 .unwrap();
7348 for i in 0..10u64 {
7349 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7350 }
7351 assert_eq!(journal.size(), 17);
7352 (journal, _) = journal.prune(10).await.unwrap();
7353
7354 journal.commit().await.unwrap();
7355
7356 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7357 .await
7358 .unwrap();
7359 let bounds = journal.bounds();
7360 assert_eq!(bounds.start, 10);
7361 assert_eq!(bounds.end, 17);
7362 journal.destroy().await.unwrap();
7363 });
7364 }
7365
7366 #[test_traced]
7367 fn test_fixed_journal_prune_does_not_move_boundary_backwards() {
7368 let executor = deterministic::Runner::default();
7371 executor.start(|context| async move {
7372 let cfg = test_cfg(NZU64!(5));
7373 let mut journal =
7375 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
7376 .await
7377 .unwrap();
7378 for i in 0..5u64 {
7380 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7381 }
7382 let (journal, _) = journal.prune(5).await.unwrap();
7384 assert_eq!(journal.bounds().start, 7);
7385 journal.destroy().await.unwrap();
7386 });
7387 }
7388
7389 #[test_traced]
7392 fn test_fixed_journal_prune_durability_survives_crash() {
7393 let executor = deterministic::Runner::default();
7394 let (_, checkpoint) = executor.start_and_recover(|context| async move {
7395 let cfg = test_cfg(NZU64!(3));
7396 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg)
7397 .await
7398 .unwrap();
7399
7400 for i in 0..8u64 {
7404 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7405 }
7406 let (journal, pruned) = journal.prune(3).await.unwrap();
7407 assert!(pruned);
7408 drop(journal);
7409 });
7410
7411 deterministic::Runner::from(checkpoint).start(|context| async move {
7412 let cfg = test_cfg(NZU64!(3));
7413 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg)
7414 .await
7415 .unwrap();
7416 assert_eq!(
7417 journal.bounds(),
7418 3..8,
7419 "pruned journal lost acknowledged items"
7420 );
7421 for i in 3..8u64 {
7422 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
7423 }
7424 journal.destroy().await.unwrap();
7425 });
7426 }
7427
7428 #[test_traced]
7431 fn test_fixed_journal_commit_after_prune() {
7432 let executor = deterministic::Runner::default();
7433 executor.start(|context| async move {
7434 let cfg = test_cfg(NZU64!(5));
7435 let mut journal = Journal::<_, Digest>::init(context.child("journal"), cfg.clone())
7436 .await
7437 .unwrap();
7438
7439 for i in 0..12 {
7440 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7441 }
7442
7443 let (mut journal, _) = journal.prune(5).await.unwrap();
7444 journal = journal
7445 .commit()
7446 .await
7447 .expect("commit should not try to sync pruned blobs");
7448 assert_eq!(journal.bounds(), 5..12);
7449 journal.destroy().await.unwrap();
7450 });
7451 }
7452
7453 #[test_traced]
7454 fn test_fixed_journal_replay_after_init_at_size_spanning_blobs() {
7455 let executor = deterministic::Runner::default();
7458 executor.start(|context| async move {
7459 let cfg = test_cfg(NZU64!(5));
7460
7461 let mut journal =
7464 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 7)
7465 .await
7466 .unwrap();
7467
7468 for i in 0..13u64 {
7470 let pos;
7471 (journal, pos) = journal.append(&test_digest(100 + i)).await.unwrap();
7472 assert_eq!(pos, 7 + i);
7473 }
7474 assert_eq!(journal.size(), 20);
7475 journal = journal.sync().await.unwrap();
7476
7477 {
7479 let reader;
7480 (journal, reader) = journal.snapshot().await.unwrap();
7481 let stream = replay_items(
7482 &reader,
7483 7..reader.bounds().end,
7484 NZUsize!(1024),
7485 ReadOptions::default(),
7486 )
7487 .await
7488 .expect("failed to replay");
7489 pin_mut!(stream);
7490 let mut items: Vec<(u64, Digest)> = Vec::new();
7491 while let Some(result) = stream.next().await {
7492 items.push(result.expect("replay item failed"));
7493 }
7494
7495 assert_eq!(items.len(), 13);
7497 for (i, (pos, item)) in items.iter().enumerate() {
7498 assert_eq!(*pos, 7 + i as u64);
7499 assert_eq!(*item, test_digest(100 + i as u64));
7500 }
7501 }
7502
7503 {
7505 let reader;
7506 (journal, reader) = journal.snapshot().await.unwrap();
7507 let stream = replay_items(
7508 &reader,
7509 12..reader.bounds().end,
7510 NZUsize!(1024),
7511 ReadOptions::default(),
7512 )
7513 .await
7514 .expect("failed to replay from mid-stream");
7515 pin_mut!(stream);
7516 let mut items: Vec<(u64, Digest)> = Vec::new();
7517 while let Some(result) = stream.next().await {
7518 items.push(result.expect("replay item failed"));
7519 }
7520
7521 assert_eq!(items.len(), 8);
7523 for (i, (pos, item)) in items.iter().enumerate() {
7524 assert_eq!(*pos, 12 + i as u64);
7525 assert_eq!(*item, test_digest(100 + 5 + i as u64));
7526 }
7527 }
7528
7529 journal.destroy().await.unwrap();
7530 });
7531 }
7532
7533 #[test_traced]
7534 fn test_fixed_journal_init_at_most_before_retained_start() {
7535 let executor = deterministic::Runner::default();
7537 executor.start(|context| async move {
7538 let cfg = test_cfg(NZU64!(5));
7539
7540 let mut journal =
7541 Journal::<_, Digest>::init_at_size(context.child("storage"), cfg.clone(), 10)
7542 .await
7543 .unwrap();
7544
7545 for i in 0..3u64 {
7547 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7548 }
7549 assert_eq!(journal.size(), 13);
7550
7551 journal = {
7553 _ = journal.sync().await.unwrap();
7554 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 11).await
7555 }
7556 .unwrap();
7557 assert_eq!(journal.size(), 11);
7558
7559 journal = {
7561 _ = journal.sync().await.unwrap();
7562 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 10).await
7563 }
7564 .unwrap();
7565 assert_eq!(journal.size(), 10);
7566
7567 let result = {
7569 _ = journal.sync().await.unwrap();
7570 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 9).await
7571 };
7572 assert!(matches!(result, Err(Error::ItemPruned(9))));
7573 });
7574 }
7575
7576 #[test_traced]
7577 fn test_fixed_journal_init_at_size_crash_scenarios() {
7578 let executor = deterministic::Runner::default();
7579 executor.start(|context| async move {
7580 let cfg = test_cfg(NZU64!(5));
7581
7582 let mut journal =
7584 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 7)
7585 .await
7586 .unwrap();
7587 for i in 0..5u64 {
7588 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7589 }
7590 let journal = journal.sync().await.unwrap();
7591 drop(journal);
7592
7593 let blob_part = blob_partition(&cfg);
7596 let mut checkpoint = Checkpoint::open(context.child("intent_meta"), &cfg.partition)
7597 .await
7598 .unwrap();
7599 checkpoint.set_clear_target(12);
7600 let checkpoint = checkpoint.sync().await.unwrap();
7601 drop(checkpoint);
7602 context.remove(&blob_part, None).await.unwrap();
7603
7604 let journal = Journal::<_, Digest>::init(
7606 context.child("crash").with_attribute("index", 1),
7607 cfg.clone(),
7608 )
7609 .await
7610 .expect("init failed after clear crash");
7611 let bounds = journal.bounds();
7612 assert_eq!(bounds.end, 12);
7613 assert_eq!(bounds.start, 12);
7614 drop(journal);
7615
7616 let mut checkpoint = Checkpoint::open(context.child("restore_meta"), &cfg.partition)
7618 .await
7619 .unwrap();
7620 checkpoint.set_boundary_hint(7);
7621 checkpoint.set_clear_target(2);
7622 let checkpoint = checkpoint.sync().await.unwrap();
7623 drop(checkpoint);
7624
7625 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
7628 blob.sync().await.unwrap(); drop(blob);
7630
7631 let journal = Journal::<_, Digest>::init(
7633 context.child("crash").with_attribute("index", 2),
7634 cfg.clone(),
7635 )
7636 .await
7637 .expect("init failed after create crash");
7638
7639 let bounds = journal.bounds();
7640 assert_eq!(bounds.start, 2);
7641 assert_eq!(bounds.end, 2);
7642 journal.destroy().await.unwrap();
7643 });
7644 }
7645
7646 #[test_traced]
7647 fn test_fixed_journal_clear_to_size_crash_scenarios() {
7648 let executor = deterministic::Runner::default();
7649 executor.start(|context| async move {
7650 let cfg = test_cfg(NZU64!(5));
7651
7652 let journal =
7655 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 12)
7656 .await
7657 .unwrap();
7658 let journal = journal.sync().await.unwrap();
7659 drop(journal);
7660
7661 let blob_part = blob_partition(&cfg);
7665 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
7666 .await
7667 .unwrap();
7668 checkpoint.set_clear_target(2);
7669 let checkpoint = checkpoint.sync().await.unwrap();
7670 drop(checkpoint);
7671
7672 context.remove(&blob_part, None).await.unwrap();
7673
7674 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
7675 blob.sync().await.unwrap();
7676
7677 let journal = Journal::<_, Digest>::init(context.child("crash_clear"), cfg.clone())
7678 .await
7679 .expect("init failed after clear_to_size crash");
7680
7681 let bounds = journal.bounds();
7682 assert_eq!(bounds.start, 2);
7683 assert_eq!(bounds.end, 2);
7684 journal.destroy().await.unwrap();
7685 });
7686 }
7687
7688 #[test_traced]
7689 fn test_fixed_journal_clear_to_size_crash_after_intent_before_blobs() {
7690 let executor = deterministic::Runner::default();
7691 executor.start(|context| async move {
7692 let cfg = test_cfg(NZU64!(5));
7693 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
7694 .await
7695 .unwrap();
7696 for i in 0..12u64 {
7697 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7698 }
7699 journal = journal.sync().await.unwrap();
7700
7701 drop(journal);
7702 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
7703 .await
7704 .unwrap();
7705 checkpoint.set_clear_target(100);
7706 let checkpoint = checkpoint.sync().await.unwrap();
7707 drop(checkpoint);
7708
7709 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7710 .await
7711 .expect("init failed after clear intent crash");
7712 assert_eq!(journal.bounds(), 100..100);
7713 let pos;
7714 (journal, pos) = journal.append(&test_digest(100)).await.unwrap();
7715 assert_eq!(pos, 100);
7716 journal.destroy().await.unwrap();
7717 });
7718 }
7719
7720 #[test_traced]
7721 fn test_fixed_journal_clear_intent_skips_corrupt_stale_blobs() {
7722 let executor = deterministic::Runner::default();
7723 executor.start(|context| async move {
7724 let cfg = test_cfg(NZU64!(5));
7725 let blob_part = blob_partition(&cfg);
7726 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
7727 .await
7728 .unwrap();
7729 checkpoint.set_clear_target(12);
7730 let checkpoint = checkpoint.sync().await.unwrap();
7731 drop(checkpoint);
7732
7733 let (blob, _) = context.open(&blob_part, b"not-u64").await.unwrap();
7736 blob.write_at(0, vec![1, 2, 3], WriteOptions::SYNC)
7737 .await
7738 .unwrap();
7739 drop(blob);
7740
7741 let journal = Journal::<_, Digest>::init(context.child("recover"), cfg.clone())
7742 .await
7743 .expect("clear intent should discard stale corrupt blobs before blob parsing");
7744 assert_eq!(journal.bounds(), 12..12);
7745 assert_eq!(journal.0.recovery_watermark(), 12);
7746 journal.destroy().await.unwrap();
7747 });
7748 }
7749
7750 #[test_traced]
7751 fn test_fixed_journal_clear_to_size_crash_after_mid_blob_intent_with_old_blobs_present() {
7752 let executor = deterministic::Runner::default();
7753 executor.start(|context| async move {
7754 let cfg = test_cfg(NZU64!(10));
7755 let mut journal =
7756 Journal::<_, Digest>::init_at_size(context.child("first"), cfg.clone(), 10)
7757 .await
7758 .unwrap();
7759
7760 for i in 0..6u64 {
7761 let pos;
7762 (journal, pos) = journal.append(&test_digest(i)).await.unwrap();
7763 assert_eq!(pos, 10 + i);
7764 }
7765 journal = journal.sync().await.unwrap();
7766
7767 drop(journal);
7768 let mut checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
7769 .await
7770 .unwrap();
7771 checkpoint.set_clear_target(15);
7772 let checkpoint = checkpoint.sync().await.unwrap();
7773 drop(checkpoint);
7774
7775 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
7776 .await
7777 .expect("init failed after mid-blob clear intent crash");
7778 assert_eq!(journal.bounds(), 15..15);
7779 drop(journal);
7780
7781 let mut journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
7782 .await
7783 .expect("init failed after completing mid-blob clear intent");
7784 assert_eq!(journal.bounds(), 15..15);
7785 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(14))));
7786 let pos;
7787 (journal, pos) = journal.append(&test_digest(100)).await.unwrap();
7788 assert_eq!(pos, 15);
7789 assert_eq!(journal.read(15).await.unwrap(), test_digest(100));
7790 journal.destroy().await.unwrap();
7791 });
7792 }
7793
7794 #[rstest::rstest]
7795 #[case::lower(7)]
7796 #[case::same(50)]
7797 #[case::higher(60)]
7798 #[test_traced]
7799 fn test_fixed_journal_init_sync_after_interrupted_clear(#[case] start: u64) {
7800 let executor = deterministic::Runner::default();
7803 executor.start(|context| async move {
7804 let cfg = test_cfg(NZU64!(10));
7805
7806 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
7808 .await
7809 .unwrap();
7810 for i in 0..30u64 {
7811 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7812 }
7813 journal = journal.sync().await.unwrap();
7814 drop(journal);
7815
7816 Journal::<_, Digest>::test_stage_clear(context.child("intent"), &cfg.partition, 50)
7819 .await
7820 .unwrap();
7821
7822 let mut journal = authenticated::init_sync::<_, Journal<_, Digest>>(
7825 context.child("sync"),
7826 cfg.clone(),
7827 start..start + 20,
7828 )
7829 .await
7830 .expect("init_sync must complete or replace the staged clear");
7831 assert_eq!(journal.bounds(), start..start);
7832
7833 for i in 0..3u64 {
7836 let pos;
7837 (journal, pos) = journal.append(&test_digest(100 + i)).await.unwrap();
7838 assert_eq!(pos, start + i);
7839 }
7840 journal = journal.sync().await.unwrap();
7841 drop(journal);
7842
7843 let journal = Journal::<_, Digest>::init(context.child("reopen"), cfg.clone())
7846 .await
7847 .unwrap();
7848 assert_eq!(journal.bounds(), start..start + 3);
7849
7850 assert!(matches!(
7852 journal.read(start - 1).await,
7853 Err(Error::ItemPruned(pos)) if pos == start - 1
7854 ));
7855 for i in 0..3u64 {
7856 assert_eq!(journal.read(start + i).await.unwrap(), test_digest(100 + i));
7857 }
7858 journal.destroy().await.unwrap();
7859 });
7860 }
7861
7862 #[test_traced]
7863 fn test_fixed_journal_rejects_watermark_with_aligned_empty_tail() {
7864 let executor = deterministic::Runner::default();
7866 executor.start(|context| async move {
7867 let cfg = test_cfg(NZU64!(5));
7868
7869 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
7870 .await
7871 .unwrap();
7872 for i in 0..10u64 {
7873 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7874 }
7875 let journal = journal.sync().await.unwrap();
7876 drop(journal);
7877
7878 let blob_part = blob_partition(&cfg);
7881 context.remove(&blob_part, None).await.unwrap();
7882 let (blob, _) = context.open(&blob_part, &1u64.to_be_bytes()).await.unwrap();
7883 blob.sync().await.unwrap();
7884
7885 drop(blob);
7886 let result = Journal::<_, Digest>::init(context.child("crash"), cfg.clone()).await;
7887 assert!(matches!(result, Err(Error::Corruption(_))));
7888 });
7889 }
7890
7891 #[test_traced]
7892 fn test_fixed_journal_rejects_far_watermark_with_aligned_empty_tail() {
7893 let executor = deterministic::Runner::default();
7895 executor.start(|context| async move {
7896 let cfg = test_cfg(NZU64!(5));
7897
7898 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
7899 .await
7900 .unwrap();
7901 for i in 0..10u64 {
7902 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7903 }
7904 let journal = journal.sync().await.unwrap();
7905 drop(journal);
7906
7907 let blob_part = blob_partition(&cfg);
7910 context.remove(&blob_part, None).await.unwrap();
7911 let (blob, _) = context.open(&blob_part, &0u64.to_be_bytes()).await.unwrap();
7912 blob.sync().await.unwrap();
7913
7914 drop(blob);
7915 let result = Journal::<_, Digest>::init(context.child("crash"), cfg.clone()).await;
7916 assert!(matches!(result, Err(Error::Corruption(_))));
7917 });
7918 }
7919
7920 #[test_traced]
7921 fn test_read_many_empty() {
7922 let executor = deterministic::Runner::default();
7923 executor.start(|context| async move {
7924 let cfg = test_cfg(NZU64!(10));
7925 let journal = Journal::<_, Digest>::init(context.child("j"), cfg)
7926 .await
7927 .unwrap();
7928
7929 let (journal, reader) = journal.snapshot().await.unwrap();
7930 let items = reader.read_many(&[]).await.unwrap();
7931 assert!(items.is_empty());
7932
7933 journal.destroy().await.unwrap();
7934 });
7935 }
7936
7937 #[test_traced]
7938 fn test_read_many_single_blob() {
7939 let executor = deterministic::Runner::default();
7941 executor.start(|context| async move {
7942 let cfg = test_cfg(NZU64!(10));
7943 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
7944
7945 for i in 0..5u64 {
7946 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7947 }
7948 assert_eq!(journal.size(), 5);
7949
7950 let (journal, reader) = journal.snapshot().await.unwrap();
7951 let items = reader.read_many(&[0, 2, 4]).await.unwrap();
7952 assert_eq!(items, vec![test_digest(0), test_digest(2), test_digest(4)]);
7953
7954 journal.destroy().await.unwrap();
7955 });
7956 }
7957
7958 #[test_traced]
7959 #[should_panic(expected = "positions must be strictly increasing")]
7960 fn test_read_many_rejects_unsorted_positions() {
7961 let executor = deterministic::Runner::default();
7962 executor.start(|context| async move {
7963 let cfg = test_cfg(NZU64!(10));
7964 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
7965 for i in 0..5u64 {
7966 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7967 }
7968
7969 let (_journal, reader) = journal.snapshot().await.unwrap();
7970 let _ = reader.read_many(&[2, 1]).await;
7971 });
7972 }
7973
7974 #[test_traced]
7975 #[should_panic(expected = "positions must be strictly increasing")]
7976 fn test_read_many_rejects_duplicate_positions() {
7977 let executor = deterministic::Runner::default();
7979 executor.start(|context| async move {
7980 let cfg = test_cfg(NZU64!(10));
7981 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
7982 for i in 0..5u64 {
7983 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
7984 }
7985
7986 let (_journal, reader) = journal.snapshot().await.unwrap();
7987 let _ = reader.read_many(&[1, 1]).await;
7988 });
7989 }
7990
7991 #[test_traced]
7992 fn test_read_many_across_blobs() {
7993 let executor = deterministic::Runner::default();
7995 executor.start(|context| async move {
7996 let cfg = test_cfg(NZU64!(3));
7997 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
7998
7999 for i in 0..9u64 {
8000 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8001 }
8002 assert_eq!(journal.size(), 9);
8003 let (journal, reader) = journal.snapshot().await.unwrap();
8006 let items = reader.read_many(&[1, 4, 7]).await.unwrap();
8007 assert_eq!(items, vec![test_digest(1), test_digest(4), test_digest(7)]);
8008
8009 journal.destroy().await.unwrap();
8010 });
8011 }
8012
8013 #[test_traced]
8014 fn test_read_many_after_prune() {
8015 let executor = deterministic::Runner::default();
8017 executor.start(|context| async move {
8018 let cfg = test_cfg(NZU64!(3));
8019 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
8020
8021 for i in 0..9u64 {
8022 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8023 }
8024 assert_eq!(journal.size(), 9);
8025 journal = journal.sync().await.unwrap();
8026
8027 (journal, _) = journal.prune(3).await.unwrap();
8029 assert_eq!(journal.bounds(), 3..9);
8030
8031 let (journal, reader) = journal.snapshot().await.unwrap();
8032 let items = reader.read_many(&[3, 5, 8]).await.unwrap();
8033 assert_eq!(items, vec![test_digest(3), test_digest(5), test_digest(8)]);
8034
8035 let (journal, reader) = journal.snapshot().await.unwrap();
8037 let err = reader.read_many(&[1]).await.unwrap_err();
8038 assert!(matches!(err, Error::ItemPruned(1)));
8039
8040 journal.destroy().await.unwrap();
8041 });
8042 }
8043
8044 #[test_traced]
8045 fn test_read_many_out_of_range() {
8046 let executor = deterministic::Runner::default();
8047 executor.start(|context| async move {
8048 let cfg = test_cfg(NZU64!(10));
8049 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
8050
8051 for i in 0..3u64 {
8052 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8053 }
8054 assert_eq!(journal.size(), 3);
8055
8056 let (journal, reader) = journal.snapshot().await.unwrap();
8057 let err = reader.read_many(&[0, 5]).await.unwrap_err();
8058 assert!(matches!(err, Error::ItemOutOfRange(5)));
8059
8060 journal.destroy().await.unwrap();
8061 });
8062 }
8063
8064 #[test_traced]
8065 fn test_read_many_matches_read() {
8066 let executor = deterministic::Runner::default();
8068 executor.start(|context| async move {
8069 let cfg = test_cfg(NZU64!(4));
8070 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
8071
8072 for i in 0..20u64 {
8073 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8074 }
8075 assert_eq!(journal.size(), 20);
8076 journal = journal.sync().await.unwrap();
8077
8078 let positions: Vec<u64> = (0..20).collect();
8079 let reader;
8080 (journal, reader) = journal.snapshot().await.unwrap();
8081 let batch = reader.read_many(&positions).await.unwrap();
8082
8083 for &pos in &positions {
8084 let single = reader.read(pos).await.unwrap();
8085 assert_eq!(batch[pos as usize], single);
8086 }
8087 drop(reader);
8088
8089 journal.destroy().await.unwrap();
8090 });
8091 }
8092
8093 #[test_traced]
8094 fn test_bounded_initialization_cached_reads() {
8095 deterministic::Runner::default().start(|context| async move {
8096 let cfg = test_cfg(NZU64!(4));
8097 let mut journal = Journal::<_, Digest>::init(context.child("seed"), cfg.clone())
8098 .await
8099 .unwrap();
8100 for i in 0..20 {
8101 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8102 }
8103 _ = journal.sync().await.unwrap();
8104 let journal = Journal::<_, Digest>::init_at_most(context.child("bounded"), cfg, 18)
8105 .await
8106 .unwrap();
8107 let (journal, reader) = journal.snapshot().await.unwrap();
8108 assert_eq!(reader.read(17).await.unwrap(), test_digest(17));
8109 let served = reader.try_read_many_sync(&[17, 18]);
8110 assert_eq!(served[0], Some(test_digest(17)));
8111 assert!(served[1].is_none());
8112 drop(reader);
8113 journal.destroy().await.unwrap();
8114 });
8115 }
8116
8117 #[test_traced]
8118 fn test_try_read_many_sync_matches_read_many() {
8119 let executor = deterministic::Runner::default();
8122 executor.start(|context| async move {
8123 let cfg = test_cfg(NZU64!(4));
8124 let page_cache = cfg.page_cache.clone();
8125 let mut journal = Journal::init(context.child("j"), cfg.clone())
8126 .await
8127 .unwrap();
8128
8129 for i in 0..20u64 {
8130 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8131 }
8132 journal = journal.sync().await.unwrap();
8133
8134 let positions: Vec<u64> = (0..20).collect();
8135 let reader;
8136 (journal, reader) = journal.snapshot().await.unwrap();
8137 let expected = reader.read_many(&positions).await.unwrap();
8138
8139 let served = reader.try_read_many_sync(&positions);
8142 assert_eq!(served.len(), positions.len());
8143 for (item, expected) in served.iter().zip(&expected) {
8144 if let Some(item) = item {
8145 assert_eq!(item, expected);
8146 }
8147 }
8148
8149 page_cache.clear();
8152 reader
8153 .read_many(&(16..20).collect::<Vec<_>>())
8154 .await
8155 .unwrap();
8156 page_cache.clear();
8157 for pos in [0, 4, 8] {
8158 reader.read(pos).await.unwrap();
8159 }
8160
8161 let tail: Vec<u64> = (18..20).collect();
8164 reader.read_many(&tail).await.unwrap();
8165 let served = reader.try_read_many_sync(&tail);
8166 for (item, pos) in served.iter().zip(&tail) {
8167 assert_eq!(
8168 item.as_ref().expect("warmed position is served"),
8169 &expected[*pos as usize]
8170 );
8171 }
8172
8173 assert!(reader.try_read_many_sync(&[0])[0].is_none());
8175
8176 let served = reader.try_read_many_sync(&[19, 20]);
8179 assert!(served[0].is_some());
8180 assert!(served[1].is_none());
8181 drop(served);
8182 drop(reader);
8183
8184 journal = {
8188 _ = journal.sync().await.unwrap();
8189 Journal::<_, Digest>::init_at_most(context.child("cap"), cfg.clone(), 18).await
8190 }
8191 .unwrap();
8192 let reader;
8193 (journal, reader) = journal.snapshot().await.unwrap();
8194 reader.read_many(&[17]).await.unwrap();
8195 let served = reader.try_read_many_sync(&[17, 18]);
8196 assert!(served[0].is_some());
8197 assert!(served[1].is_none());
8198 drop(served);
8199 drop(reader);
8200
8201 (journal, _) = journal.prune(8).await.unwrap();
8204 let reader;
8205 (journal, reader) = journal.snapshot().await.unwrap();
8206 reader.read_many(&[8]).await.unwrap();
8207 let served = reader.try_read_many_sync(&[3, 8]);
8208 assert!(served[0].is_none());
8209 assert!(served[1].is_some());
8210 drop(served);
8211 drop(reader);
8212
8213 journal.destroy().await.unwrap();
8214 });
8215 }
8216
8217 #[test_traced]
8218 fn test_probe_then_read_many_matches_read_many() {
8219 let executor = deterministic::Runner::default();
8222 executor.start(|context| async move {
8223 let cfg = test_cfg(NZU64!(4));
8224 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
8225
8226 for i in 0..20u64 {
8227 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8228 }
8229 journal = journal.sync().await.unwrap();
8230
8231 let positions: Vec<u64> = (0..20).collect();
8232 let reader;
8233 (journal, reader) = journal.snapshot().await.unwrap();
8234 let expected: Vec<_> = (0..20).map(test_digest).collect();
8235 for _ in 0..2 {
8236 let mut served = reader.try_read_many_sync(&positions);
8237 let misses: Vec<u64> = positions
8238 .iter()
8239 .zip(&served)
8240 .filter_map(|(&pos, item)| item.is_none().then_some(pos))
8241 .collect();
8242 let mut fetched = reader.read_many(&misses).await.unwrap().into_iter();
8243 for item in served.iter_mut().filter(|item| item.is_none()) {
8244 *item = fetched.next();
8245 }
8246 let completed: Vec<_> = served.into_iter().map(Option::unwrap).collect();
8247 assert_eq!(completed, expected);
8248 }
8249 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
8250 drop(reader);
8251
8252 journal.destroy().await.unwrap();
8253 });
8254 }
8255
8256 #[test_traced]
8257 fn test_fixed_journal_metrics() {
8258 let executor = deterministic::Runner::default();
8259 executor.start(|context| async move {
8260 let cfg = test_cfg(NZU64!(2));
8261 let mut journal =
8262 Journal::<_, Digest>::init(context.child("fixed_metrics"), cfg.clone())
8263 .await
8264 .unwrap();
8265
8266 let items: Vec<_> = (0..5).map(test_digest).collect();
8267 (journal, _) = journal.append_many(Many::Flat(&items)).await.unwrap();
8268 (journal, _) = journal.append(&test_digest(5)).await.unwrap();
8269 journal = journal.commit().await.unwrap();
8270 journal = journal.sync().await.unwrap();
8271 let handle;
8272 (journal, handle) = journal.start_sync().await.unwrap();
8273 handle.await.unwrap();
8274 let (journal, reader) = journal.snapshot().await.unwrap();
8275 reader.read(0).await.unwrap();
8276 let (journal, reader) = journal.snapshot().await.unwrap();
8277 reader.try_read_sync(0).unwrap();
8278 let (journal, reader) = journal.snapshot().await.unwrap();
8279 reader.read_many(&[1, 2, 4]).await.unwrap();
8280 let (journal, _) = journal.prune(2).await.unwrap();
8281
8282 let buffer = context.encode();
8283 for expected in [
8284 "fixed_metrics_size 6",
8285 "fixed_metrics_pruning_boundary 2",
8286 "fixed_metrics_retained 4",
8287 "fixed_metrics_tail_items 2",
8288 "fixed_metrics_append_calls_total 1",
8289 "fixed_metrics_append_many_calls_total 1",
8290 "fixed_metrics_read_calls_total 1",
8291 "fixed_metrics_read_many_calls_total 1",
8292 "fixed_metrics_items_read_total 5",
8293 "fixed_metrics_start_sync_calls_total 1",
8294 "fixed_metrics_commit_calls_total 1",
8295 "fixed_metrics_sync_calls_total 1",
8296 "fixed_metrics_append_duration_count 1",
8297 "fixed_metrics_append_many_duration_count 1",
8298 "fixed_metrics_read_duration_count 0",
8299 "fixed_metrics_read_many_duration_count 1",
8300 "fixed_metrics_commit_duration_count 1",
8301 "fixed_metrics_sync_duration_count 1",
8302 "fixed_metrics_cache_hits_total",
8303 "fixed_metrics_cache_misses_total",
8304 "fixed_metrics_blobs_tracked",
8305 ] {
8306 assert!(buffer.contains(expected), "{expected}\n{buffer}");
8307 }
8308
8309 journal.destroy().await.unwrap();
8310 });
8311 }
8312 #[test_traced]
8314 fn test_snapshot_frozen_across_roll() {
8315 let executor = deterministic::Runner::default();
8316 executor.start(|context| async move {
8317 let cfg = test_cfg(NZU64!(5));
8318 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
8319 .await
8320 .unwrap();
8321 for i in 0..7u64 {
8322 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8323 }
8324
8325 let snapshot;
8326 (journal, snapshot) = journal.snapshot().await.unwrap();
8327 assert_eq!(snapshot.bounds(), 0..7);
8328
8329 for i in 7..23u64 {
8332 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8333 }
8334 assert_eq!(snapshot.bounds(), 0..7);
8335 for i in 0..7u64 {
8336 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
8337 }
8338 assert!(matches!(
8339 snapshot.read(7).await,
8340 Err(Error::ItemOutOfRange(7))
8341 ));
8342
8343 let fresh;
8344 (journal, fresh) = journal.snapshot().await.unwrap();
8345 assert_eq!(fresh.bounds(), 0..23);
8346 assert_eq!(fresh.read(22).await.unwrap(), test_digest(22));
8347
8348 drop(snapshot);
8349 drop(fresh);
8350 journal.destroy().await.unwrap();
8351 });
8352 }
8353
8354 #[test_traced]
8357 fn test_prune_under_snapshot() {
8358 let executor = deterministic::Runner::default();
8359 executor.start(|context| async move {
8360 let cfg = test_cfg(NZU64!(5));
8361 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
8362 .await
8363 .unwrap();
8364 for i in 0..17u64 {
8365 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8366 }
8367 journal = journal.sync().await.unwrap();
8368
8369 let snapshot;
8370 (journal, snapshot) = journal.snapshot().await.unwrap();
8371 let pruned;
8372 (journal, pruned) = journal.prune(12).await.unwrap();
8373 assert!(pruned);
8374
8375 assert_eq!(snapshot.bounds(), 0..17);
8377 for i in 0..17u64 {
8378 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
8379 }
8380
8381 let fresh;
8382 (journal, fresh) = journal.snapshot().await.unwrap();
8383 assert_eq!(fresh.bounds(), 10..17);
8384 assert!(matches!(fresh.read(3).await, Err(Error::ItemPruned(3))));
8385
8386 drop(snapshot);
8387 drop(fresh);
8388 journal.destroy().await.unwrap();
8389 });
8390 }
8391
8392 #[test_traced]
8395 fn test_snapshots_readable_during_concurrent_appends() {
8396 let executor = deterministic::Runner::default();
8397 executor.start(|context| async move {
8398 let cfg = test_cfg(NZU64!(5));
8399 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
8400 .await
8401 .unwrap();
8402
8403 let (mut tx, mut rx) =
8404 futures::channel::mpsc::channel::<Reader<'static, Context, Digest>>(8);
8405 let validator = context.child("validator").spawn(|_| async move {
8406 let mut validated = 0usize;
8407 while let Some(snapshot) = rx.next().await {
8408 let bounds = snapshot.bounds();
8409 for i in bounds.clone() {
8410 assert_eq!(snapshot.read(i).await.unwrap(), test_digest(i));
8411 }
8412 validated += (bounds.end - bounds.start) as usize;
8413 }
8414 validated
8415 });
8416
8417 for i in 0..40u64 {
8418 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8419 if i % 7 == 0 {
8420 let snapshot;
8421 (journal, snapshot) = journal.snapshot().await.unwrap();
8422 if tx.try_send(snapshot).is_err() {
8423 break;
8424 }
8425 }
8426 }
8427 drop(tx);
8428 assert!(validator.await.unwrap() > 0);
8429
8430 journal.destroy().await.unwrap();
8431 });
8432 }
8433
8434 #[test_traced]
8437 fn test_replay_from_stale_snapshot() {
8438 let executor = deterministic::Runner::default();
8439 executor.start(|context| async move {
8440 let cfg = test_cfg(NZU64!(5));
8441 let mut journal = Journal::<_, Digest>::init(context.child("j"), cfg)
8442 .await
8443 .unwrap();
8444 for i in 0..7u64 {
8445 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8446 }
8447
8448 let snapshot;
8450 (journal, snapshot) = journal.snapshot().await.unwrap();
8451 assert_eq!(snapshot.bounds(), 0..7);
8452
8453 for i in 7..23u64 {
8455 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8456 }
8457 let pruned;
8458 (journal, pruned) = journal.prune(12).await.unwrap();
8459 assert!(pruned);
8460
8461 {
8462 let stream = replay_items(
8463 &snapshot,
8464 0..snapshot.bounds().end,
8465 NZUsize!(1024),
8466 ReadOptions::default(),
8467 )
8468 .await
8469 .unwrap();
8470 pin_mut!(stream);
8471 let mut expected = 0u64;
8472 while let Some(result) = stream.next().await {
8473 let (pos, item) = result.unwrap();
8474 assert_eq!(pos, expected);
8475 assert_eq!(item, test_digest(pos));
8476 expected += 1;
8477 }
8478 assert_eq!(expected, 7);
8479 }
8480
8481 drop(snapshot);
8482 journal.destroy().await.unwrap();
8483 });
8484 }
8485
8486 #[test_traced]
8487 fn test_read_many_sparse_sections_and_hit_accounting() {
8488 let executor = deterministic::Runner::default();
8492 executor.start(|context| async move {
8493 let mut cfg = test_cfg(NZU64!(8));
8494 cfg.page_cache = CacheRef::new(PAGE_SIZE, NZUsize!(16));
8497 let mut journal = Journal::init(context.child("j"), cfg).await.unwrap();
8498
8499 for i in 0..50u64 {
8500 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8501 }
8502 journal = journal.sync().await.unwrap();
8503 (journal, _) = journal.prune(11).await.unwrap();
8505
8506 let reader;
8507 (journal, reader) = journal.snapshot().await.unwrap();
8508
8509 let positions: Vec<u64> = vec![11, 12, 19, 20, 23, 31, 40, 47, 49];
8514 let expected_hits = positions
8515 .iter()
8516 .filter(|&&pos| reader.try_read_sync(pos).is_some())
8517 .count() as u64;
8518 let before = context.encode();
8519 let batch = reader.read_many(&positions).await.unwrap();
8520 let after = context.encode();
8521 assert_eq!(batch.len(), positions.len());
8522 assert_eq!(
8523 counter(&after, "cache_hits") - counter(&before, "cache_hits"),
8524 expected_hits,
8525 "batch read hit count should match the cached subset"
8526 );
8527 assert_eq!(
8528 counter(&after, "cache_misses") - counter(&before, "cache_misses"),
8529 positions.len() as u64 - expected_hits,
8530 "batch read miss count should cover the rest"
8531 );
8532 for (i, &pos) in positions.iter().enumerate() {
8533 let single = reader.read(pos).await.unwrap();
8534 assert_eq!(batch[i], single);
8535 assert_eq!(batch[i], test_digest(pos));
8536 }
8537
8538 let all: Vec<u64> = (11..50).collect();
8540 let batch = reader.read_many(&all).await.unwrap();
8541 for (i, &pos) in all.iter().enumerate() {
8542 assert_eq!(batch[i], reader.read(pos).await.unwrap());
8543 }
8544 drop(reader);
8545
8546 journal.destroy().await.unwrap();
8547 });
8548 }
8549
8550 #[test_traced]
8551 fn test_read_many_cold_blob_groups() {
8552 let executor = deterministic::Runner::default();
8557 executor.start(|context| async move {
8558 let mut cfg = test_cfg(NZU64!(8));
8559 cfg.page_cache = CacheRef::new(PAGE_SIZE, NZUsize!(32));
8560 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
8561 .await
8562 .unwrap();
8563 for i in 0..40u64 {
8564 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8565 }
8566 let journal = journal.sync().await.unwrap();
8567 drop(journal);
8568
8569 cfg.page_cache = CacheRef::new(PAGE_SIZE, NZUsize!(32));
8573 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg)
8574 .await
8575 .unwrap();
8576 let reader;
8577 (journal, reader) = journal.snapshot().await.unwrap();
8578 let positions = [1, 5, 10, 14, 17, 22, 25, 30];
8579 for pos in positions {
8580 assert!(reader.try_read_sync(pos).is_none(), "position {pos}");
8581 }
8582
8583 let before = context.encode();
8585 let batch = reader.read_many(&positions).await.unwrap();
8586 let after = context.encode();
8587 assert_eq!(
8588 counter(&after, "second_cache_misses") - counter(&before, "second_cache_misses"),
8589 positions.len() as u64
8590 );
8591 assert_eq!(
8592 counter(&after, "second_cache_hits"),
8593 counter(&before, "second_cache_hits")
8594 );
8595 for (i, &pos) in positions.iter().enumerate() {
8596 assert_eq!(batch[i], test_digest(pos), "position {pos}");
8597 }
8598
8599 let before = context.encode();
8602 let batch = reader.read_many(&positions).await.unwrap();
8603 let after = context.encode();
8604 assert_eq!(
8605 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
8606 positions.len() as u64
8607 );
8608 for (i, &pos) in positions.iter().enumerate() {
8609 assert_eq!(batch[i], test_digest(pos), "position {pos}");
8610 }
8611 drop(reader);
8612
8613 journal.destroy().await.unwrap();
8614 });
8615 }
8616
8617 #[test_traced]
8618 fn test_fixed_journal_read_miss_timed() {
8619 let executor = deterministic::Runner::default();
8621 executor.start(|context| async move {
8622 let mut journal =
8623 Journal::<_, Digest>::init(context.child("miss"), test_cfg(NZU64!(2)))
8624 .await
8625 .unwrap();
8626 for i in 0..20 {
8627 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8628 }
8629 journal = journal.sync().await.unwrap();
8630
8631 let reader;
8633 (journal, reader) = journal.snapshot().await.unwrap();
8634 let pos = (0..20)
8635 .find(|&pos| reader.try_read_sync(pos).is_none())
8636 .expect("some position should be cold");
8637 assert_eq!(reader.read(pos).await.unwrap(), test_digest(pos));
8638 drop(reader);
8639
8640 let buffer = context.encode();
8641 assert!(buffer.contains("miss_read_duration_count 1"), "{buffer}");
8642
8643 journal.destroy().await.unwrap();
8644 });
8645 }
8646
8647 #[test_traced]
8650 fn test_fixed_destroy_observes_failed_watermark_sync_before_removal() {
8651 let executor = deterministic::Runner::default();
8652 executor.start(|context| async move {
8653 let pending = PendingSyncs::default();
8654 let cfg = test_cfg(NZU64!(100));
8655 let data_partition = blob_partition(&cfg);
8656 let mut journal = Box::new(
8657 Inner::<_, u64>::init(
8658 DelayedSyncContext {
8659 inner: context.child("journal"),
8660 pending: pending.clone(),
8661 },
8662 cfg.clone(),
8663 )
8664 .await
8665 .unwrap(),
8666 );
8667 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
8668 journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
8669
8670 (journal, _) = journal.append(&4).await.unwrap();
8673 let (journal, handle) = journal.start_sync().await.unwrap();
8674 release_pending_syncs(&pending);
8675 handle.await.unwrap();
8676 let (journal, handle) = journal.start_sync().await.unwrap();
8677 drop(handle);
8678 assert_eq!(pending.lock().len(), 1);
8679 fail_pending_syncs(&pending);
8680
8681 assert!(journal.destroy().await.is_err());
8682 assert_eq!(
8683 scan_partition(&context, &data_partition).await,
8684 vec![0u64.to_be_bytes().to_vec()]
8685 );
8686 let journal = Inner::<_, u64>::init(context.child("second"), cfg)
8687 .await
8688 .unwrap();
8689 assert_eq!(journal.size(), 4);
8690 journal.destroy().await.unwrap();
8691 });
8692 }
8693
8694 #[test_traced]
8698 fn test_fixed_bounded_recovery_below_watermark_keeps_acknowledged_items() {
8699 let executor = deterministic::Runner::default();
8700 executor.start(|context| async move {
8701 const PAGE: u64 = 100;
8702 let cfg = Config {
8703 partition: "torn-rewrite-below-watermark".into(),
8704 items_per_blob: NZU64!(64),
8705 page_cache: CacheRef::new(NZU16!(PAGE as u16), NZUsize!(3)),
8706 write_buffer: NZUsize!(2048),
8707 replay_buffer: NZUsize!(2048),
8708 };
8709 let mut journal = Journal::<_, Digest>::init(context.child("first"), cfg.clone())
8710 .await
8711 .unwrap();
8712 for i in 0..33u64 {
8713 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8714 }
8715 journal = journal.sync().await.unwrap();
8716 for i in 33..38u64 {
8717 (journal, _) = journal.append(&test_digest(i)).await.unwrap();
8718 }
8719 journal.commit().await.unwrap();
8720 tear_page_rewrite(
8721 &context,
8722 &blob_partition(&cfg),
8723 &0u64.to_be_bytes(),
8724 10,
8725 60,
8726 PAGE,
8727 )
8728 .await;
8729
8730 let checkpoint = Checkpoint::open(context.child("meta"), &cfg.partition)
8732 .await
8733 .unwrap();
8734 let recovery = Recovery::<_, Digest>::open(
8735 context.child("capped"),
8736 cfg.clone(),
8737 checkpoint,
8738 Some(32),
8739 )
8740 .await
8741 .unwrap();
8742 assert_eq!(recovery.bounds, 0..32);
8743 assert_eq!(recovery.item(31).await.unwrap(), test_digest(31));
8744 drop(recovery);
8745 assert_eq!(
8746 Journal::<_, Digest>::persisted_watermark(
8747 context.child("watermark"),
8748 &cfg.partition
8749 )
8750 .await
8751 .unwrap(),
8752 Some(33)
8753 );
8754
8755 let checkpoint = Checkpoint::open(context.child("wider_meta"), &cfg.partition)
8757 .await
8758 .unwrap();
8759 let recovery = Recovery::<_, Digest>::open(
8760 context.child("wider"),
8761 cfg.clone(),
8762 checkpoint,
8763 Some(36),
8764 )
8765 .await
8766 .unwrap();
8767 assert_eq!(recovery.bounds, 0..33);
8768 assert_eq!(recovery.item(32).await.unwrap(), test_digest(32));
8769 drop(recovery);
8770 let journal = Journal::<_, Digest>::init(context.child("unbounded"), cfg)
8771 .await
8772 .unwrap();
8773 assert_eq!(journal.bounds(), 0..33);
8774 for i in 0..33u64 {
8775 assert_eq!(journal.read(i).await.unwrap(), test_digest(i));
8776 }
8777 journal.destroy().await.unwrap();
8778 });
8779 }
8780}