1use super::manager::{
24 AppendFactory, Config as ManagerConfig, Manager, section_from_name, stored_names,
25};
26use crate::journal::Error;
27use commonware_codec::{CodecFixed, CodecFixedShared, Copying, DecodeExt as _, ReadExt as _};
28use commonware_runtime::{
29 Blob, Error as RError, Handle, Metrics, ReadOptions, Storage,
30 buffer::paged::{CacheRef, Recovery as PagedRecovery, Replay as BlobReplay, Writer},
31};
32use commonware_utils::{Cached, NZUsize};
33use std::{
34 collections::{BTreeMap, BTreeSet, VecDeque},
35 marker::PhantomData,
36 num::{NonZeroU16, NonZeroUsize},
37};
38use tracing::{trace, warn};
39
40commonware_utils::thread_local_cache!(static READ_SCRATCH: Vec<u8>);
42
43struct SectionReplay<B: Blob> {
45 section: u64,
46 reader: BlobReplay<B>,
47 position: u64,
48}
49
50#[derive(Clone)]
52pub struct Config {
53 pub partition: String,
55
56 pub page_cache: CacheRef,
58
59 pub write_buffer: NonZeroUsize,
61}
62
63struct Inner<E: Storage + Metrics, A: CodecFixed> {
65 manager: Manager<E, AppendFactory>,
66
67 unrecovered: BTreeSet<u64>,
70
71 floors: BTreeMap<u64, u64>,
73
74 _array: PhantomData<A>,
75}
76
77enum PreflightMode {
80 Floors {
82 floors: BTreeMap<u64, u64>,
84
85 ceiling: u64,
88 },
89
90 Restore {
92 section: u64,
94
95 floors: BTreeMap<u64, u64>,
97 },
98}
99
100pub(crate) struct RecoveryPreflight<E: Storage + Metrics, A: CodecFixed> {
102 context: E,
103 cfg: Config,
104
105 boundaries: BTreeMap<u64, Option<A>>,
107
108 mode: PreflightMode,
110}
111
112impl<E: Storage + Metrics, A: CodecFixedShared> RecoveryPreflight<E, A> {
113 pub(crate) const fn boundaries(&self) -> &BTreeMap<u64, Option<A>> {
115 &self.boundaries
116 }
117
118 pub(crate) async fn finish(self) -> Result<Journal<E, A>, Error> {
120 Ok(Journal(Box::new(
121 Inner::init(self.context, self.cfg, self.mode).await?,
122 )))
123 }
124}
125
126impl<E: Storage + Metrics, A: CodecFixed> Inner<E, A> {
127 fn writer(&self, section: u64) -> Result<&PagedRecovery<E::Blob>, Error> {
130 Ok(self
131 .manager
132 .get(section)?
133 .expect("replayed section is present"))
134 }
135}
136
137impl<E: Storage + Metrics, A: CodecFixedShared> Inner<E, A> {
138 const CHUNK_SIZE: usize = A::SIZE;
140 const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE as u64;
141
142 async fn stored(context: &E, cfg: &Config) -> Result<BTreeMap<u64, Vec<u8>>, Error> {
144 let mut stored = BTreeMap::new();
146 for name in stored_names(context, &cfg.partition).await? {
147 let section = section_from_name(&name)?;
148 stored.insert(section, name);
149 }
150 Ok(stored)
151 }
152
153 async fn preflight_floors(
155 context: E,
156 cfg: Config,
157 minimum_items: &BTreeMap<u64, u64>,
158 ceiling: u64,
159 ) -> Result<RecoveryPreflight<E, A>, Error> {
160 assert!(
161 minimum_items.keys().all(|section| *section <= ceiling),
162 "validation floors must not exceed the recovery ceiling"
163 );
164 let stored = Self::stored(&context, &cfg).await?;
165 let page_size = cfg.page_cache.page_size();
166 let mut floors = BTreeMap::new();
167 let mut boundaries = BTreeMap::new();
168
169 for (§ion, &items) in minimum_items {
170 let Some(name) = stored.get(§ion) else {
172 return Err(Error::Corruption(format!(
173 "section {section} has a validation floor but no blob"
174 )));
175 };
176 let required = items.checked_mul(Self::CHUNK_SIZE_U64).ok_or_else(|| {
177 Error::Corruption(format!(
178 "section {section} validation floor {items} overflows its byte size"
179 ))
180 })?;
181 if required == 0 {
182 boundaries.insert(section, None);
183 continue;
184 }
185
186 let (blob, _) = context.open(&cfg.partition, name).await?;
190 let entry = Self::boundary(&blob, page_size, section, required).await?;
191 floors.insert(section, required);
192 boundaries.insert(section, Some(entry));
193 }
194
195 Ok(RecoveryPreflight {
196 context,
197 cfg,
198 boundaries,
199 mode: PreflightMode::Floors { floors, ceiling },
200 })
201 }
202
203 async fn preflight_restore(
205 context: E,
206 cfg: Config,
207 section: u64,
208 size: u64,
209 ) -> Result<RecoveryPreflight<E, A>, Error> {
210 if !size.is_multiple_of(Self::CHUNK_SIZE_U64) {
212 return Err(Error::Corruption(format!(
213 "section {section} checkpoint size {size} is not item-aligned"
214 )));
215 }
216
217 let stored = Self::stored(&context, &cfg).await?;
219 if size > 0 && !stored.contains_key(§ion) {
220 return Err(Error::Corruption(format!(
221 "section {section} has a checkpoint but no blob"
222 )));
223 }
224
225 let page_size = cfg.page_cache.page_size();
226 let mut boundaries = BTreeMap::new();
227 let mut floors = BTreeMap::new();
228
229 for (&candidate, name) in stored.range(..=section) {
230 let (blob, physical_size) = context.open(&cfg.partition, name).await?;
233 let entry = if candidate == section {
234 if size == 0 {
235 None
236 } else {
237 let entry = Self::boundary(&blob, page_size, section, size).await?;
238 floors.insert(section, size);
239 Some(entry)
240 }
241 } else if physical_size == 0 {
242 None
243 } else {
244 let (logical_size, entry) = Writer::<E::Blob>::read_tail(
245 &blob,
246 physical_size,
247 page_size,
248 Self::CHUNK_SIZE,
249 ReadOptions::default(),
250 )
251 .await
252 .map_err(|err| Self::boundary_error(candidate, physical_size, err))?;
253 if !logical_size.is_multiple_of(Self::CHUNK_SIZE_U64) {
254 return Err(Error::Corruption(format!(
255 "section {candidate} is not a complete checkpoint-covered index"
256 )));
257 }
258 floors.insert(candidate, logical_size);
259 Some(A::decode(entry.coalesce()).map_err(Error::Codec)?)
260 };
261 boundaries.insert(candidate, entry);
262 }
263 boundaries.entry(section).or_insert(None);
264 Ok(RecoveryPreflight {
265 context,
266 cfg,
267 boundaries,
268 mode: PreflightMode::Restore { section, floors },
269 })
270 }
271
272 async fn boundary(
274 blob: &E::Blob,
275 page_size: NonZeroU16,
276 section: u64,
277 size: u64,
278 ) -> Result<A, Error> {
279 let entry = Writer::<E::Blob>::read_range(
280 blob,
281 page_size,
282 size - Self::CHUNK_SIZE_U64,
283 Self::CHUNK_SIZE,
284 ReadOptions::default(),
285 )
286 .await
287 .map_err(|err| Self::boundary_error(section, size, err))?;
288 A::decode(entry.coalesce()).map_err(Error::Codec)
289 }
290
291 fn boundary_error(section: u64, searched: u64, err: RError) -> Error {
296 match err {
297 RError::InvalidChecksum | RError::BlobInsufficientLength => Error::Corruption(format!(
298 "section {section} does not retain a valid durable boundary within {searched} bytes"
299 )),
300 err => err.into(),
301 }
302 }
303
304 async fn init(context: E, cfg: Config, mode: PreflightMode) -> Result<Self, Error> {
306 let (floors, restore, ceiling) = match mode {
309 PreflightMode::Floors { floors, ceiling } => (floors, None, ceiling),
310 PreflightMode::Restore { section, floors } => {
311 let size = floors.get(§ion).copied().unwrap_or(0);
312 (floors, Some((section, size)), section)
313 }
314 };
315
316 let manager_cfg = ManagerConfig {
317 partition: cfg.partition,
318 factory: AppendFactory {
319 write_buffer: cfg.write_buffer,
320 page_cache_ref: cfg.page_cache,
321 },
322 };
323 let manager = Manager::init_bounded(context, manager_cfg, ceiling).await?;
324 if let Some((section, size)) = restore {
325 let manager = manager
328 .truncate_pending(section, size)
329 .await?
330 .sync(section)
331 .await?;
332 return Ok(Self {
333 manager,
334 unrecovered: BTreeSet::new(),
335 floors,
336 _array: PhantomData,
337 });
338 }
339
340 let mut unrecovered = BTreeSet::new();
341 for section in manager.sections() {
342 let size = manager.size(section)?;
343 let floor = floors.get(§ion).copied().unwrap_or(0);
344 if size < floor {
345 return Err(Error::Corruption(format!(
346 "section {section} retains {size} of its {floor}-byte validation floor"
347 )));
348 }
349 if size > floor {
350 unrecovered.insert(section);
351 }
352 }
353
354 Ok(Self {
355 manager,
356 unrecovered,
357 floors,
358 _array: PhantomData,
359 })
360 }
361
362 async fn append(&mut self, section: u64, item: &A) -> Result<u64, Error> {
364 assert!(
365 !self.unrecovered.contains(§ion),
366 "section {section} must be replayed before append"
367 );
368
369 let blob = self.manager.get_or_create(section).await?;
371 let offset = match blob.try_append_value(item) {
372 Some(offset) => offset,
373 None => {
374 let blob = self.manager.take(section).await?;
376 let (blob, offset) = blob.append_owned(item.encode_mut().into()).await?;
377 self.manager.put(section, blob);
378 offset
379 }
380 };
381 if !offset.is_multiple_of(Self::CHUNK_SIZE_U64) {
382 return Err(Error::InvalidBlobSize(section, offset));
383 }
384 let position = offset / Self::CHUNK_SIZE_U64;
385 trace!(section, position, "appended item");
386
387 Ok(position)
388 }
389
390 async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
392 let blob = self
393 .manager
394 .get(section)?
395 .ok_or(Error::SectionOutOfRange(section))?;
396
397 let offset = position
398 .checked_mul(Self::CHUNK_SIZE_U64)
399 .ok_or(Error::ItemOutOfRange(position))?;
400
401 let buf = blob
403 .read_at(offset, Self::CHUNK_SIZE)
404 .await
405 .map_err(|err| match err {
406 commonware_runtime::Error::BlobInsufficientLength
407 | commonware_runtime::Error::OffsetOverflow => Error::ItemOutOfRange(position),
408 err => Error::Runtime(err),
409 })?;
410 A::decode(buf.coalesce()).map_err(Error::Codec)
411 }
412
413 async fn get_many(
415 &self,
416 section: u64,
417 positions: &[u64],
418 buf: &mut [u8],
419 ) -> Result<(Vec<A>, usize), Error> {
420 assert!(
421 positions.is_sorted_by(|a, b| a < b),
422 "positions must be strictly increasing"
423 );
424 if positions.is_empty() {
425 return Ok((Vec::new(), 0));
426 }
427 assert!(
428 buf.len() >= positions.len() * Self::CHUNK_SIZE,
429 "get_many requires buf.len() >= positions.len() * CHUNK_SIZE"
430 );
431 let buf = &mut buf[..positions.len() * Self::CHUNK_SIZE];
432 let blob = self
433 .manager
434 .get(section)?
435 .ok_or(Error::SectionOutOfRange(section))?;
436
437 let mut offsets = positions.to_vec();
438 for offset in &mut offsets {
439 *offset = offset
440 .checked_mul(Self::CHUNK_SIZE_U64)
441 .ok_or(Error::ItemOutOfRange(*offset))?;
442 }
443
444 let hits = blob
445 .read_many_into(buf, &offsets, NZUsize!(Self::CHUNK_SIZE))
446 .await?;
447
448 let mut items = Vec::with_capacity(positions.len());
449 for i in 0..positions.len() {
450 let slice = &buf[i * Self::CHUNK_SIZE..(i + 1) * Self::CHUNK_SIZE];
451 items.push(A::decode(Copying(slice)).map_err(Error::Codec)?);
452 }
453 Ok((items, hits))
454 }
455
456 fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
458 let blob = self.manager.get(section).ok()??;
459 let offset = position.checked_mul(Self::CHUNK_SIZE_U64)?;
460 let remaining = blob.size().checked_sub(offset)?;
461 if remaining < Self::CHUNK_SIZE_U64 {
462 return None;
463 }
464 let mut scratch =
465 Cached::take(&READ_SCRATCH, || Ok::<_, ()>(Vec::new()), |_| Ok(())).unwrap();
466 if scratch.len() < Self::CHUNK_SIZE {
467 scratch.resize(Self::CHUNK_SIZE, 0);
468 }
469 let buf = &mut scratch[..Self::CHUNK_SIZE];
470 if !blob.try_read_sync_into(buf, offset) {
471 return None;
472 }
473 A::decode(Copying(buf)).ok()
474 }
475
476 async fn last(&self, section: u64) -> Result<Option<A>, Error> {
478 let blob = self
479 .manager
480 .get(section)?
481 .ok_or(Error::SectionOutOfRange(section))?;
482
483 let size = blob.size();
484 if size < Self::CHUNK_SIZE_U64 {
485 return Ok(None);
486 }
487
488 let last_position = (size / Self::CHUNK_SIZE_U64) - 1;
489 let offset = last_position * Self::CHUNK_SIZE_U64;
490 let buf = blob.read_at(offset, Self::CHUNK_SIZE).await?;
491 A::decode(buf.coalesce()).map_err(Error::Codec).map(Some)
492 }
493
494 async fn sync(mut self: Box<Self>, sections: impl crate::Sections) -> Result<Box<Self>, Error> {
496 self.manager = self.manager.sync(sections).await?;
497 Ok(self)
498 }
499
500 async fn start_sync(
502 mut self: Box<Self>,
503 sections: impl crate::Sections,
504 ) -> Result<(Box<Self>, Handle<()>), Error> {
505 let (manager, handle) = self.manager.start_sync(sections).await?;
506 self.manager = manager;
507 Ok((self, handle))
508 }
509
510 async fn sync_all(mut self: Box<Self>) -> Result<Box<Self>, Error> {
512 self.manager = self.manager.sync_all().await?;
513 Ok(self)
514 }
515
516 async fn prune(mut self: Box<Self>, min: u64) -> Result<(Box<Self>, bool), Error> {
518 let (manager, pruned) = self.manager.prune(min).await?;
519 self.manager = manager;
520 if pruned {
521 self.unrecovered.retain(|section| *section >= min);
522 self.floors.retain(|section, _| *section >= min);
523 }
524 Ok((self, pruned))
525 }
526
527 const fn pruned(&self, section: u64) -> bool {
529 self.manager.pruned(section)
530 }
531
532 fn oldest_section(&self) -> Option<u64> {
534 self.manager.oldest_section()
535 }
536
537 fn newest_section(&self) -> Option<u64> {
539 self.manager.newest_section()
540 }
541
542 fn sections(&self) -> impl Iterator<Item = u64> + '_ {
544 self.manager.sections()
545 }
546
547 fn section_len(&self, section: u64) -> Result<u64, Error> {
549 let size = self.manager.size(section)?;
550 Ok(size / Self::CHUNK_SIZE_U64)
551 }
552
553 fn size(&self, section: u64) -> Result<u64, Error> {
555 self.manager.size(section)
556 }
557
558 async fn truncate_pending_section(
560 mut self: Box<Self>,
561 section: u64,
562 size: u64,
563 ) -> Result<Box<Self>, Error> {
564 self.manager = self.manager.truncate_pending_section(section, size).await?;
565 if size == 0 {
566 self.unrecovered.remove(§ion);
567 }
568 if let Some(floor) = self.floors.get_mut(§ion) {
569 *floor = (*floor).min(size);
570 }
571 Ok(self)
572 }
573
574 async fn destroy(self) -> Result<(), Error> {
576 self.manager.destroy().await
577 }
578
579 async fn clear(mut self: Box<Self>) -> Result<Box<Self>, Error> {
581 self.manager = self.manager.clear().await?;
582 self.unrecovered.clear();
583 self.floors.clear();
584 Ok(self)
585 }
586}
587
588pub struct Journal<E: Storage + Metrics, A: CodecFixed>(Box<Inner<E, A>>);
609
610impl<E: Storage + Metrics, A: CodecFixedShared> std::fmt::Debug for Journal<E, A> {
611 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612 f.debug_struct("Journal")
613 .field("oldest_section", &self.oldest_section())
614 .field("newest_section", &self.newest_section())
615 .finish_non_exhaustive()
616 }
617}
618
619impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
620 pub const CHUNK_SIZE: usize = Inner::<E, A>::CHUNK_SIZE;
622
623 pub async fn init_at_most(
628 context: E,
629 cfg: Config,
630 section: u64,
631 end: u64,
632 ) -> Result<Self, Error> {
633 let replay_buffer = cfg.write_buffer;
634 let mut end = end - end % Self::CHUNK_SIZE as u64;
635 super::manager::truncate_paged_tail(
636 &context,
637 &cfg.partition,
638 cfg.page_cache.page_size(),
639 section,
640 end,
641 )
642 .await?;
643 let mut journal = Self::init(context, cfg).await?;
644
645 if end > 0
647 && journal
648 .0
649 .manager
650 .get(section)?
651 .is_some_and(|blob| end < blob.size())
652 {
653 end = journal
654 .recoverable_prefix_len_at_most(section, end, replay_buffer)
655 .await?;
656 }
657 journal.0.manager = journal
658 .0
659 .manager
660 .truncate_pending(section, end - end % Self::CHUNK_SIZE as u64)
661 .await?;
662 let mut replay = journal
663 .replay(0, 0, replay_buffer, ReadOptions::default())
664 .await?;
665 while let Some(item) = replay.next().await {
666 item?;
667 }
668 replay.finish()
669 }
670
671 pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
676 Self::init_bounded(context, cfg, u64::MAX).await
677 }
678
679 pub(crate) async fn init_bounded(context: E, cfg: Config, ceiling: u64) -> Result<Self, Error> {
682 let mode = PreflightMode::Floors {
683 floors: BTreeMap::new(),
684 ceiling,
685 };
686 Ok(Self(Box::new(Inner::init(context, cfg, mode).await?)))
687 }
688
689 pub(crate) async fn preflight_floors(
694 context: E,
695 cfg: Config,
696 minimum_items: &BTreeMap<u64, u64>,
697 ceiling: u64,
698 ) -> Result<RecoveryPreflight<E, A>, Error> {
699 Inner::preflight_floors(context, cfg, minimum_items, ceiling).await
700 }
701
702 pub(crate) async fn recoverable_prefix_len_at_most(
706 &self,
707 section: u64,
708 end: u64,
709 buffer: NonZeroUsize,
710 ) -> Result<u64, Error> {
711 let Some(blob) = self.0.manager.get(section)? else {
712 return Ok(end);
713 };
714 let recoverable = blob
715 .recoverable_prefix_len_at_most(0, end, buffer, ReadOptions::default())
716 .await?;
717 Ok(recoverable - recoverable % Self::CHUNK_SIZE as u64)
718 }
719
720 pub(crate) async fn truncate_pending_tail(
722 mut self,
723 section: u64,
724 end: u64,
725 ) -> Result<Self, Error> {
726 self.0.manager = self.0.manager.truncate_pending(section, end).await?;
727 self.0.unrecovered.retain(|candidate| *candidate <= section);
728 self.0.floors.retain(|candidate, _| *candidate <= section);
729 Ok(self)
730 }
731
732 pub(crate) async fn preflight_restore(
734 context: E,
735 cfg: Config,
736 section: u64,
737 size: u64,
738 ) -> Result<RecoveryPreflight<E, A>, Error> {
739 Inner::preflight_restore(context, cfg, section, size).await
740 }
741
742 pub async fn append(mut self, section: u64, item: &A) -> Result<(Self, u64), Error> {
751 let position = self.0.append(section, item).await?;
752 Ok((self, position))
753 }
754
755 pub async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
763 self.0.get(section, position).await
764 }
765
766 pub async fn get_many(
774 &self,
775 section: u64,
776 positions: &[u64],
777 buf: &mut [u8],
778 ) -> Result<(Vec<A>, usize), Error> {
779 self.0.get_many(section, positions, buf).await
780 }
781
782 pub fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
784 self.0.try_get_sync(section, position)
785 }
786
787 pub async fn last(&self, section: u64) -> Result<Option<A>, Error> {
796 self.0.last(section).await
797 }
798
799 pub async fn replay(
811 mut self,
812 start_section: u64,
813 start_position: u64,
814 buffer: NonZeroUsize,
815 read_options: ReadOptions,
816 ) -> Result<Replay<E, A>, Error> {
817 let mut sections = VecDeque::new();
819 let replayed: Vec<_> = self.0.manager.sections_from(start_section).collect();
820 for section in replayed {
821 let blob = self.0.manager.take(section).await?;
822 let blob_size = blob.size();
823 let (blob, mut reader) = blob.replay(buffer, read_options).await?;
824 self.0.manager.put(section, blob);
825
826 let position = if section == start_section {
828 let start = start_position
829 .checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
830 .ok_or(Error::ItemOutOfRange(start_position))?;
831 if start > blob_size {
832 return Err(Error::ItemOutOfRange(start_position));
833 }
834 reader.seek_to(start)?;
835 start_position
836 } else {
837 0
838 };
839 sections.push_back(SectionReplay {
840 section,
841 reader,
842 position,
843 });
844 }
845 let finished = sections.is_empty();
846 Ok(Replay {
847 journal: self,
848 sections,
849 recovered_from: if start_position == 0 {
850 Some(start_section)
851 } else {
852 start_section.checked_add(1)
853 },
854 buffer,
855 read_options,
856 finished,
857 errored: false,
858 repairing: false,
859 })
860 }
861
862 pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
864 self.0 = self.0.sync(sections).await?;
865 Ok(self)
866 }
867
868 pub async fn start_sync(
873 mut self,
874 sections: impl crate::Sections,
875 ) -> Result<(Self, Handle<()>), Error> {
876 let (inner, handle) = self.0.start_sync(sections).await?;
877 self.0 = inner;
878 Ok((self, handle))
879 }
880
881 pub async fn sync_all(mut self) -> Result<Self, Error> {
883 self.0 = self.0.sync_all().await?;
884 Ok(self)
885 }
886
887 pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
889 let (inner, pruned) = self.0.prune(min).await?;
890 self.0 = inner;
891 Ok((self, pruned))
892 }
893
894 pub fn pruned(&self, section: u64) -> bool {
899 self.0.pruned(section)
900 }
901
902 pub fn oldest_section(&self) -> Option<u64> {
904 self.0.oldest_section()
905 }
906
907 pub fn newest_section(&self) -> Option<u64> {
909 self.0.newest_section()
910 }
911
912 pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
914 self.0.sections()
915 }
916
917 pub fn section_len(&self, section: u64) -> Result<u64, Error> {
919 self.0.section_len(section)
920 }
921
922 pub fn size(&self, section: u64) -> Result<u64, Error> {
924 self.0.size(section)
925 }
926
927 pub(crate) async fn truncate_pending_section(
931 mut self,
932 section: u64,
933 size: u64,
934 ) -> Result<Self, Error> {
935 self.0 = self.0.truncate_pending_section(section, size).await?;
936 Ok(self)
937 }
938
939 pub async fn destroy(self) -> Result<(), Error> {
941 self.0.destroy().await
942 }
943
944 pub async fn clear(mut self) -> Result<Self, Error> {
948 self.0 = self.0.clear().await?;
949 Ok(self)
950 }
951}
952
953pub struct Replay<E: Storage + Metrics, A: CodecFixed> {
959 journal: Journal<E, A>,
960 sections: VecDeque<SectionReplay<E::Blob>>,
961 recovered_from: Option<u64>,
964 buffer: NonZeroUsize,
965 read_options: ReadOptions,
966 finished: bool,
967 errored: bool,
968 repairing: bool,
969}
970
971impl<E: Storage + Metrics, A: CodecFixedShared> Replay<E, A> {
972 async fn plan_repair(&mut self, source: RError) -> Result<(u64, u64), Error> {
975 if !matches!(source, RError::InvalidChecksum) {
978 return Err(source.into());
979 }
980
981 let current = self.sections.front().expect("replayed section is present");
983 let section = current.section;
984 if !self.journal.0.unrecovered.contains(§ion) {
985 return Err(source.into());
986 }
987 let position = current.position;
988 let size = current.reader.blob_size();
989 let valid_size = position
990 .checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
991 .ok_or(Error::OffsetOverflow)?;
992
993 let recoverable = self
995 .journal
996 .0
997 .writer(section)?
998 .recoverable_prefix_len(valid_size, self.buffer, self.read_options)
999 .await?;
1000
1001 if recoverable >= size {
1004 return Err(source.into());
1005 }
1006
1007 let target = recoverable - recoverable % Inner::<E, A>::CHUNK_SIZE_U64;
1010 if target < valid_size {
1011 return Err(Error::ItemOutOfRange(position));
1012 }
1013 self.ensure_above_floor(section, target)?;
1014
1015 Ok((valid_size, target))
1016 }
1017
1018 async fn repair(&mut self, source: RError) -> Result<(), Error> {
1020 let (valid_size, target) = match self.plan_repair(source).await {
1022 Ok(plan) => plan,
1023 Err(err) => {
1024 self.sections.pop_front();
1025 return Err(err);
1026 }
1027 };
1028
1029 let current = self.sections.front().expect("replayed section is present");
1030 let (section, position) = (current.section, current.position);
1031 warn!(
1032 section,
1033 invalid_size = current.reader.blob_size(),
1034 new_size = target,
1035 "torn page detected: truncating"
1036 );
1037
1038 self.repairing = true;
1040 let current = self
1041 .sections
1042 .pop_front()
1043 .expect("repaired section is present");
1044 drop(current.reader);
1045 let writer = self.journal.0.manager.take(section).await?;
1046 let writer = writer.truncate(target).await?;
1047 let (writer, mut reader) = writer.replay(self.buffer, self.read_options).await?;
1048 self.journal.0.manager.put(section, writer);
1049 reader.seek_to(valid_size)?;
1050 self.sections.push_front(SectionReplay {
1051 section,
1052 reader,
1053 position,
1054 });
1055 self.repairing = false;
1056 Ok(())
1057 }
1058
1059 async fn repair_tail(&mut self, section: u64, size: u64) -> Result<(), Error> {
1061 self.repairing = true;
1063 let writer = self.journal.0.manager.take(section).await?;
1064 let writer = writer.truncate(size).await?;
1065 self.journal.0.manager.put(section, writer);
1066 self.repairing = false;
1067 Ok(())
1068 }
1069
1070 fn ensure_above_floor(&self, section: u64, target: u64) -> Result<(), Error> {
1072 let floor = self.journal.0.floors.get(§ion).copied().unwrap_or(0);
1073 if target < floor {
1074 return Err(Error::Corruption(format!(
1075 "section {section} recovery target {target} is below its {floor}-byte validation floor"
1076 )));
1077 }
1078 Ok(())
1079 }
1080
1081 pub async fn next(&mut self) -> Option<Result<(u64, u64, A), Error>> {
1088 if self.repairing {
1091 self.repairing = false;
1092 self.sections.clear();
1093 if !self.errored {
1094 return self.fail(Error::ReplayInterrupted);
1095 }
1096 }
1097 while let Some(current) = self.sections.front_mut() {
1098 match current.reader.ensure(Inner::<E, A>::CHUNK_SIZE).await {
1100 Ok(true) => {}
1101 Ok(false) => {
1102 let valid_size =
1103 match current.position.checked_mul(Inner::<E, A>::CHUNK_SIZE_U64) {
1104 Some(size) => size,
1105 None => return self.fail(Error::OffsetOverflow),
1106 };
1107 let blob_size = current.reader.blob_size();
1108 if valid_size < blob_size {
1109 let section = current.section;
1110 if !self.journal.0.unrecovered.contains(§ion) {
1111 let position = current.position;
1112 self.sections.pop_front();
1113 return self.fail(Error::ItemOutOfRange(position));
1114 }
1115 if let Err(err) = self.ensure_above_floor(section, valid_size) {
1116 self.sections.pop_front();
1117 return self.fail(err);
1118 }
1119 warn!(
1120 section,
1121 invalid_size = blob_size,
1122 new_size = valid_size,
1123 "incomplete item detected: truncating"
1124 );
1125 if let Err(err) = self.repair_tail(section, valid_size).await {
1126 self.sections.pop_front();
1127 return self.fail(err);
1128 }
1129 }
1130 self.sections.pop_front();
1131 continue;
1132 }
1133 Err(err) => {
1134 if let Err(err) = self.repair(err).await {
1135 return self.fail(err);
1136 }
1137 continue;
1138 }
1139 }
1140
1141 match A::read(&mut current.reader) {
1143 Ok(item) => {
1144 let yielded = (current.section, current.position, item);
1145 current.position += 1;
1146 return Some(Ok(yielded));
1147 }
1148 Err(err) => {
1149 self.sections.pop_front();
1150 return self.fail(Error::Codec(err));
1151 }
1152 }
1153 }
1154 self.finished = true;
1155 None
1156 }
1157
1158 const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, A), Error>> {
1160 self.errored = true;
1161 Some(Err(err))
1162 }
1163
1164 pub fn finish(mut self) -> Result<Journal<E, A>, Error> {
1169 if self.errored || !self.finished {
1170 return Err(Error::ReplayFailed);
1171 }
1172 if let Some(start) = self.recovered_from {
1173 self.journal
1174 .0
1175 .unrecovered
1176 .retain(|section| *section < start);
1177 }
1178 Ok(self.journal)
1179 }
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184 use super::*;
1185 use crate::utils::codec::View;
1186 use commonware_codec::FixedSize;
1187 use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
1188 use commonware_macros::test_traced;
1189 use commonware_runtime::{
1190 Error as RError, Runner, Spawner as _, Supervisor as _,
1191 buffer::paged::{CacheRef, corrupt_page},
1192 deterministic,
1193 mocks::{
1194 DelayedSyncContext, PendingSyncs, RecordingContext, fail_pending_syncs,
1195 release_pending_syncs,
1196 },
1197 };
1198 use commonware_utils::{NZU16, NZUsize};
1199 use core::num::NonZeroU16;
1200 use std::{
1201 ops::RangeInclusive,
1202 sync::{
1203 Arc,
1204 atomic::{AtomicUsize, Ordering},
1205 },
1206 };
1207
1208 impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
1209 pub(in super::super) async fn test_reopen_at_most(
1210 self,
1211 section: u64,
1212 end: u64,
1213 ) -> Result<Self, Error> {
1214 let (context, partition, factory) = self.0.manager.test_configuration();
1215 let cfg = Config {
1216 partition,
1217 write_buffer: factory.write_buffer,
1218 page_cache: factory.page_cache_ref,
1219 };
1220 _ = self.sync_all().await?;
1221 Self::init_at_most(context, cfg, section, end).await
1222 }
1223
1224 async fn test_reopen_section(self, section: u64, end: u64) -> Result<Self, Error> {
1225 let (context, partition, factory) = self.0.manager.test_configuration();
1226 let replay_buffer = factory.write_buffer;
1227 let cfg = Config {
1228 partition,
1229 write_buffer: replay_buffer,
1230 page_cache: factory.page_cache_ref,
1231 };
1232 _ = self.sync_all().await?;
1233 let mut journal = Self::init(context, cfg).await?;
1234 journal.0.manager = journal
1235 .0
1236 .manager
1237 .truncate_pending_section(section, end)
1238 .await?;
1239 let mut replay = journal
1240 .replay(0, 0, replay_buffer, ReadOptions::default())
1241 .await?;
1242 while let Some(item) = replay.next().await {
1243 item?;
1244 }
1245 replay.finish()
1246 }
1247
1248 pub(in super::super) fn test_configuration(&self) -> (E, String, AppendFactory) {
1249 self.0.manager.test_configuration()
1250 }
1251 }
1252
1253 const PAGE_SIZE: NonZeroU16 = NZU16!(44);
1254 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
1255
1256 fn test_digest(value: u64) -> Digest {
1257 Sha256::hash(&[&value.to_be_bytes()])
1258 }
1259
1260 fn test_cfg() -> Config {
1261 Config {
1262 partition: "test-partition".into(),
1263 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1264 write_buffer: NZUsize!(2048),
1265 }
1266 }
1267
1268 fn aligned_cfg() -> Config {
1269 Config {
1270 partition: "segmented-fixed-aligned".into(),
1271 page_cache: CacheRef::new(NZU16!(16), NZUsize!(4)),
1272 write_buffer: NZUsize!(128),
1273 }
1274 }
1275
1276 #[test_traced]
1277 fn test_fixed_cached_get_preserves_owned_byte_fields() {
1278 deterministic::Runner::default().start(|context| async move {
1279 let cfg = test_cfg();
1280 let mut journal = Journal::init(context, cfg).await.unwrap();
1281 (journal, _) = journal.append(1, &View::new(7)).await.unwrap();
1282 let decoded = journal.try_get_sync(1, 0).unwrap();
1283 journal.destroy().await.unwrap();
1284 assert_eq!(decoded.bytes.as_ref(), &7u64.to_be_bytes());
1285 });
1286 }
1287
1288 fn lazy_recovery_cfg(partition: &str) -> Config {
1289 Config {
1290 partition: partition.into(),
1291 page_cache: CacheRef::new(NZU16!(16), NZUsize!(4)),
1292 write_buffer: NZUsize!(1),
1293 }
1294 }
1295
1296 async fn replay_all<E, A>(journal: Journal<E, A>) -> Journal<E, A>
1297 where
1298 E: Storage + Metrics,
1299 A: CodecFixedShared,
1300 {
1301 let mut replay = journal
1302 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1303 .await
1304 .expect("failed to start recovery replay");
1305 while let Some(item) = replay.next().await {
1306 item.expect("failed to recover journal");
1307 }
1308 replay.finish().expect("failed to finish recovery replay")
1309 }
1310
1311 async fn seed<E: Storage + Metrics>(context: &E, cfg: &Config, sections: RangeInclusive<u64>) {
1313 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1314 .await
1315 .expect("failed to init");
1316 for section in sections {
1317 for value in 0..16u64 {
1318 (journal, _) = journal
1319 .append(section, &value)
1320 .await
1321 .expect("failed to append");
1322 }
1323 }
1324 journal.sync_all().await.expect("failed to sync");
1325 }
1326
1327 #[test]
1328 fn test_segmented_capped_init_bounds_torn_suffix_reads() {
1329 deterministic::Runner::default().start(|context| async move {
1330 for source_section in [0, 1] {
1331 for cap in [0, 8] {
1332 let mut counts = Vec::new();
1334 for pages in [16, 4096] {
1335 let partition = format!("torn-cap-{source_section}-{cap}-{pages}");
1336 let page: Vec<u8> = (0..9u64).flat_map(u64::to_be_bytes).collect();
1337 assert_eq!(page.len(), 72);
1338 super::super::manager::tests::seed_torn_suffix(
1339 &context,
1340 &partition,
1341 source_section,
1342 &page,
1343 pages,
1344 )
1345 .await;
1346
1347 let cfg = Config {
1349 partition,
1350 page_cache: CacheRef::new(NZU16!(72), NZUsize!(4)),
1351 write_buffer: NZUsize!(144),
1352 };
1353 let (recorded, recordings) = RecordingContext::new(context.child("cap"));
1354 let mut journal =
1355 Journal::<_, u64>::init_at_most(recorded, cfg.clone(), 0, cap)
1356 .await
1357 .unwrap();
1358 let retained = if source_section == 0 { cap } else { 0 };
1359 assert_eq!(journal.size(0).unwrap(), retained);
1360 counts.push(recordings.snapshot().reads.len());
1361
1362 (journal, _) = journal.append(0, &999).await.unwrap();
1364 drop(journal.sync_all().await.unwrap());
1365 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1366 .await
1367 .unwrap();
1368 let mut replay = journal
1369 .replay(0, 0, NZUsize!(144), ReadOptions::default())
1370 .await
1371 .unwrap();
1372 let mut actual = Vec::new();
1373 while let Some(item) = replay.next().await {
1374 actual.push(item.unwrap().2);
1375 }
1376 replay.finish().unwrap();
1377 let expected = if retained == 0 {
1378 vec![999]
1379 } else {
1380 vec![0, 999]
1381 };
1382 assert_eq!(actual, expected);
1383 }
1384
1385 assert_eq!(
1387 counts[0], counts[1],
1388 "discarded suffix reads grew: {counts:?}"
1389 );
1390 }
1391 }
1392 });
1393 }
1394
1395 #[test_traced]
1396 fn test_bounded_initialization_bounds_scan() {
1397 deterministic::Runner::default().start(|context| async move {
1398 let mut counts = Vec::new();
1400 for count in [128, 65536] {
1401 let cfg = Config {
1402 partition: format!("fixed-cap-scan-{count}"),
1403 page_cache: CacheRef::new(NZU16!(64), NZUsize!(4)),
1404 write_buffer: NZUsize!(65536),
1405 };
1406 let mut journal = Journal::<_, u64>::init(
1407 context.child(if count == 128 {
1408 "seed_small"
1409 } else {
1410 "seed_large"
1411 }),
1412 cfg.clone(),
1413 )
1414 .await
1415 .unwrap();
1416 for value in 0..count {
1417 (journal, _) = journal.append(0, &value).await.unwrap();
1418 }
1419 _ = journal.sync_all().await.unwrap();
1420
1421 let (recorded, recordings) =
1423 RecordingContext::new(context.child(if count == 128 {
1424 "cap_small"
1425 } else {
1426 "cap_large"
1427 }));
1428 let journal = Journal::<_, u64>::init_at_most(recorded, cfg, 0, 8)
1429 .await
1430 .unwrap();
1431 assert_eq!(journal.section_len(0).unwrap(), 1);
1432 counts.push(recordings.snapshot().reads.len());
1433 }
1434
1435 assert_eq!(
1437 counts[0], counts[1],
1438 "same one-item cap: small/large suffix read counts {counts:?}"
1439 );
1440 });
1441 }
1442
1443 #[rstest::rstest]
1444 #[case(30)]
1445 #[case(31)]
1446 #[case(32)]
1447 fn test_bounded_initialization_recovers_torn_page(#[case] tail: u64) {
1448 deterministic::Runner::default().start(|context| async move {
1449 for cap in [0, 8, 16, 20, 24, 28, 29, 30, 31, 32, u64::MAX] {
1450 let cfg = Config {
1452 partition: format!("capped-torn-{cap}"),
1453 page_cache: CacheRef::new(NZU16!(5), NZUsize!(4)),
1454 write_buffer: NZUsize!(256),
1455 };
1456 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1457 .await
1458 .unwrap();
1459 for value in 0..4 {
1460 (journal, _) = journal.append(0, &value).await.unwrap();
1461 }
1462 (journal, _) = journal.append(1, &100).await.unwrap();
1463 _ = journal.sync_all().await.unwrap();
1464
1465 let (blob, size) = context
1467 .open(&cfg.partition, &0u64.to_be_bytes())
1468 .await
1469 .unwrap();
1470 let partial = commonware_runtime::buffer::paged::Recovery::open(
1471 blob,
1472 size,
1473 cfg.write_buffer.get(),
1474 cfg.page_cache.clone(),
1475 )
1476 .await
1477 .unwrap();
1478 partial.truncate(tail).await.unwrap();
1479
1480 corrupt_page(&context, &cfg.partition, &0u64.to_be_bytes(), 4, 5).await;
1482 let journal =
1483 Journal::<_, u64>::init_at_most(context.child("cap"), cfg.clone(), 0, cap)
1484 .await
1485 .unwrap();
1486 let expected_bytes = cap.min(16) / 8 * 8;
1487 assert_eq!(journal.size(0).unwrap(), expected_bytes, "cap {cap}");
1488 assert_eq!(journal.newest_section(), Some(0));
1489 drop(journal);
1490
1491 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1493 .await
1494 .unwrap();
1495 let mut replay = journal
1496 .replay(0, 0, NZUsize!(256), ReadOptions::default())
1497 .await
1498 .unwrap();
1499 let mut count = 0;
1500 while let Some(item) = replay.next().await {
1501 let item = item.unwrap();
1502 assert_eq!(item.0, 0);
1503 assert_eq!(item.2, count);
1504 count += 1;
1505 }
1506 assert_eq!(count, expected_bytes / 8);
1507 let mut journal = replay.finish().unwrap();
1508 let position;
1509 (journal, position) = journal.append(0, &99).await.unwrap();
1510 assert_eq!(position, expected_bytes / 8);
1511 journal.destroy().await.unwrap();
1512 }
1513 });
1514 }
1515
1516 #[test_traced]
1517 fn test_segmented_fixed_append_and_get() {
1518 let executor = deterministic::Runner::default();
1519 executor.start(|context| async move {
1520 let cfg = test_cfg();
1521 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1522 .await
1523 .expect("failed to init");
1524
1525 let pos0;
1526 (journal, pos0) = journal
1527 .append(1, &test_digest(0))
1528 .await
1529 .expect("failed to append");
1530 assert_eq!(pos0, 0);
1531
1532 let pos1;
1533 (journal, pos1) = journal
1534 .append(1, &test_digest(1))
1535 .await
1536 .expect("failed to append");
1537 assert_eq!(pos1, 1);
1538
1539 let pos2;
1540 (journal, pos2) = journal
1541 .append(2, &test_digest(2))
1542 .await
1543 .expect("failed to append");
1544 assert_eq!(pos2, 0);
1545
1546 let item0 = journal.get(1, 0).await.expect("failed to get");
1547 assert_eq!(item0, test_digest(0));
1548
1549 let item1 = journal.get(1, 1).await.expect("failed to get");
1550 assert_eq!(item1, test_digest(1));
1551
1552 let item2 = journal.get(2, 0).await.expect("failed to get");
1553 assert_eq!(item2, test_digest(2));
1554
1555 let err = journal.get(1, 2).await;
1556 assert!(matches!(err, Err(Error::ItemOutOfRange(2))));
1557
1558 let err = journal.get(3, 0).await;
1559 assert!(matches!(err, Err(Error::SectionOutOfRange(3))));
1560
1561 journal.destroy().await.expect("failed to destroy");
1562 });
1563 }
1564
1565 #[rstest::rstest]
1568 #[case(100)]
1569 #[case(8193)]
1570 #[case(32769)]
1571 fn test_segmented_fixed_synced_sections_partial_pages(#[case] items: u64) {
1572 const PAGE_SIZE: usize = 4096;
1573 const WRITE_BUFFER: usize = 131072;
1574 const SECTIONS: u64 = 8;
1575 const CACHE_PAGES: usize = 4;
1576
1577 deterministic::Runner::default().start(move |context| async move {
1578 let cfg = Config {
1579 partition: "segmented-fixed-buffers".into(),
1580 page_cache: CacheRef::new(NZU16!(PAGE_SIZE as u16), NZUsize!(CACHE_PAGES)),
1581 write_buffer: NZUsize!(WRITE_BUFFER),
1582 };
1583 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg)
1584 .await
1585 .unwrap();
1586 for section in 0..SECTIONS {
1587 for value in 0..items {
1588 (journal, _) = journal.append(section, &value).await.unwrap();
1589 }
1590 journal = journal.sync_all().await.unwrap();
1591 }
1592
1593 for section in 0..SECTIONS {
1594 assert_eq!(journal.get(section, 0).await.unwrap(), 0);
1595 assert_eq!(journal.get(section, items - 1).await.unwrap(), items - 1);
1596 (journal, _) = journal.append(section, &items).await.unwrap();
1597 }
1598 journal = journal.sync_all().await.unwrap();
1599 for section in 0..SECTIONS {
1600 assert_eq!(journal.get(section, items).await.unwrap(), items);
1601 }
1602 });
1603 }
1604
1605 #[test_traced]
1606 fn test_segmented_fixed_append_buffer_boundaries() {
1607 for page_size in [NZU16!(8), NZU16!(16), PAGE_SIZE] {
1610 deterministic::Runner::default().start(|context| async move {
1611 let cfg = Config {
1612 partition: "buffer-boundaries".into(),
1613 page_cache: CacheRef::new(page_size, PAGE_CACHE_SIZE),
1614 write_buffer: NZUsize!(1),
1615 };
1616 let mut journal = Journal::init(context.child("initial"), cfg.clone())
1617 .await
1618 .unwrap();
1619 for section in [1, 3] {
1620 for position in 0..10 {
1621 let item = test_digest(section * 100 + position);
1622 let appended;
1623 (journal, appended) = journal.append(section, &item).await.unwrap();
1624 assert_eq!(appended, position);
1625 assert_eq!(journal.section_len(section).unwrap(), position + 1);
1626 assert_eq!(journal.get(section, position).await.unwrap(), item);
1627 }
1628 }
1629 journal = journal.sync_all().await.unwrap();
1630 drop(journal);
1631
1632 let journal = Journal::<_, Digest>::init(context, cfg).await.unwrap();
1633 let mut replay = journal
1634 .replay(0, 0, NZUsize!(128), ReadOptions::default())
1635 .await
1636 .unwrap();
1637 for section in [1, 3] {
1638 for position in 0..10 {
1639 assert_eq!(
1640 replay.next().await.unwrap().unwrap(),
1641 (section, position, test_digest(section * 100 + position))
1642 );
1643 }
1644 }
1645 assert!(replay.next().await.is_none());
1646 let mut journal = replay.finish().unwrap();
1647 let position;
1648 (journal, position) = journal.append(3, &test_digest(310)).await.unwrap();
1649 assert_eq!(position, 10);
1650 assert_eq!(journal.get(3, 10).await.unwrap(), test_digest(310));
1651 journal.destroy().await.unwrap();
1652 });
1653 }
1654 }
1655
1656 #[test_traced]
1657 fn test_segmented_fixed_replay_empty_finishes_immediately() {
1658 let executor = deterministic::Runner::default();
1659 executor.start(|context| async move {
1660 let cfg = test_cfg();
1661 let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
1662 .await
1663 .expect("failed to init");
1664
1665 let replay = journal
1667 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1668 .await
1669 .expect("failed to replay");
1670 let journal = replay.finish().expect("failed to finish replay");
1671 journal.destroy().await.expect("failed to destroy");
1672 });
1673 }
1674
1675 #[test_traced]
1676 fn test_segmented_fixed_replay_propagates_read_options() {
1677 let executor = deterministic::Runner::default();
1678 executor.start(|context| async move {
1679 let (context, recordings) = RecordingContext::new(context);
1680 let cfg = aligned_cfg();
1681 let mut journal = Journal::init(context.child("storage"), cfg)
1682 .await
1683 .expect("failed to init");
1684
1685 for section in 1..=2 {
1686 (journal, _) = journal
1687 .append(section, &test_digest(section))
1688 .await
1689 .expect("failed to append");
1690 }
1691
1692 let mut replay = journal
1693 .replay(1, 0, NZUsize!(56), ReadOptions::DONT_CACHE)
1694 .await
1695 .expect("failed to replay");
1696 recordings.clear();
1697
1698 let (section, position, item) = replay
1700 .next()
1701 .await
1702 .expect("missing first replay item")
1703 .expect("failed to read first replay item");
1704 assert_eq!((section, position, item), (1, 0, test_digest(1)));
1705 let reads = recordings.snapshot().reads;
1706 assert!(!reads.is_empty());
1707 assert!(
1708 reads
1709 .iter()
1710 .all(|options| *options == ReadOptions::DONT_CACHE)
1711 );
1712
1713 recordings.clear();
1715 let (section, position, item) = replay
1716 .next()
1717 .await
1718 .expect("missing second replay item")
1719 .expect("failed to read second replay item");
1720 assert_eq!((section, position, item), (2, 0, test_digest(2)));
1721 let reads = recordings.snapshot().reads;
1722 assert!(!reads.is_empty());
1723 assert!(
1724 reads
1725 .iter()
1726 .all(|options| *options == ReadOptions::DONT_CACHE)
1727 );
1728 assert!(replay.next().await.is_none());
1729
1730 let journal = replay.finish().expect("failed to finish replay");
1731 journal.destroy().await.expect("failed to destroy");
1732 });
1733 }
1734
1735 #[test_traced]
1736 fn test_segmented_fixed_replay_leaves_page_cache_cold() {
1737 deterministic::Runner::default().start(|context| async move {
1738 let (context, recordings) = RecordingContext::new(context);
1739 let cfg = aligned_cfg();
1740 let page_cache = cfg.page_cache.clone();
1741 let mut journal = Journal::<_, [u8; 16]>::init(context.child("storage"), cfg)
1742 .await
1743 .unwrap();
1744
1745 for value in 0..2 {
1747 (journal, _) = journal.append(1, &[value; 16]).await.unwrap();
1748 }
1749 journal = journal.sync(1).await.unwrap();
1750 page_cache.clear();
1751
1752 let mut replay = journal
1753 .replay(0, 0, NZUsize!(1024), ReadOptions::DONT_CACHE)
1754 .await
1755 .unwrap();
1756 for value in 0..2 {
1757 assert_eq!(
1758 replay.next().await.unwrap().unwrap(),
1759 (1, u64::from(value), [value; 16])
1760 );
1761 }
1762 assert!(replay.next().await.is_none());
1763 let journal = replay.finish().unwrap();
1764 for position in 0..2 {
1765 assert!(journal.try_get_sync(1, position).is_none());
1766 }
1767
1768 recordings.clear();
1769 assert_eq!(journal.get(1, 0).await.unwrap(), [0; 16]);
1770 assert!(!recordings.snapshot().reads.is_empty());
1771 assert_eq!(journal.try_get_sync(1, 0), Some([0; 16]));
1772 recordings.clear();
1773 assert_eq!(journal.get(1, 0).await.unwrap(), [0; 16]);
1774 assert!(recordings.snapshot().reads.is_empty());
1775 journal.destroy().await.unwrap();
1776 });
1777 }
1778
1779 #[test_traced]
1780 fn test_segmented_fixed_clean_recovery_uses_replay_buffer() {
1781 let executor = deterministic::Runner::default();
1782 executor.start(|context| async move {
1783 let (context, recordings) = RecordingContext::new(context);
1784 let cfg = lazy_recovery_cfg("segmented-fixed-lazy-recovery");
1785
1786 seed(&context, &cfg, 1..=1).await;
1789
1790 recordings.clear();
1794 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1795 .await
1796 .expect("failed to reopen");
1797 assert_eq!(recordings.snapshot().reads.len(), 1);
1798
1799 let mut replay = journal
1800 .replay(0, 0, NZUsize!(448), ReadOptions::default())
1801 .await
1802 .expect("failed to replay");
1803 while let Some(item) = replay.next().await {
1804 item.expect("failed to read replay item");
1805 }
1806 assert_eq!(recordings.snapshot().reads.len(), 3);
1807 replay
1808 .finish()
1809 .expect("failed to finish replay")
1810 .destroy()
1811 .await
1812 .expect("failed to destroy");
1813 });
1814 }
1815
1816 #[test_traced]
1817 #[should_panic(expected = "must be replayed before append")]
1818 fn test_segmented_fixed_append_requires_replay_after_reopen() {
1819 let executor = deterministic::Runner::default();
1820 executor.start(|context| async move {
1821 let cfg = test_cfg();
1822 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1823 .await
1824 .expect("failed to init");
1825 (journal, _) = journal
1826 .append(1, &test_digest(0))
1827 .await
1828 .expect("failed to append");
1829 journal = journal.sync_all().await.expect("failed to sync");
1830 drop(journal);
1831
1832 let journal = Journal::init(context.child("reopen"), cfg)
1835 .await
1836 .expect("failed to reopen");
1837 journal.append(1, &test_digest(1)).await.unwrap();
1838 });
1839 }
1840
1841 #[test_traced]
1842 #[should_panic(expected = "must be replayed before append")]
1843 fn test_segmented_fixed_gates_older_section_after_reopen() {
1844 let executor = deterministic::Runner::default();
1845 executor.start(|context| async move {
1846 let cfg = test_cfg();
1847 seed(&context, &cfg, 1..=3).await;
1848
1849 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1852 .await
1853 .expect("failed to reopen");
1854 journal.append(2, &2).await.unwrap();
1855 });
1856 }
1857
1858 #[test_traced]
1859 fn test_segmented_fixed_floor_preflight_reads_boundary_only() {
1860 let executor = deterministic::Runner::default();
1861 executor.start(|context| async move {
1862 let (context, recordings) = RecordingContext::new(context);
1863 let cfg = lazy_recovery_cfg("segmented-fixed-floor-boundary");
1864 seed(&context, &cfg, 1..=1).await;
1865
1866 recordings.clear();
1869 let floors = BTreeMap::from([(1, 16)]);
1870 let preflight = Journal::<_, u64>::preflight_floors(
1871 context.child("preflight"),
1872 cfg,
1873 &floors,
1874 u64::MAX,
1875 )
1876 .await
1877 .expect("failed to preflight");
1878 assert_eq!(recordings.snapshot().reads.len(), 1);
1879 preflight
1880 .finish()
1881 .await
1882 .expect("failed to finish preflight")
1883 .destroy()
1884 .await
1885 .expect("failed to destroy");
1886 });
1887 }
1888
1889 #[test_traced]
1890 fn test_segmented_fixed_restore_reads_boundaries_only() {
1891 let executor = deterministic::Runner::default();
1892 executor.start(|context| async move {
1893 let (context, recordings) = RecordingContext::new(context);
1894 let cfg = lazy_recovery_cfg("segmented-fixed-restore-boundaries");
1895 seed(&context, &cfg, 0..=1).await;
1896
1897 recordings.clear();
1900 let preflight = Journal::<_, u64>::preflight_restore(
1901 context.child("preflight"),
1902 cfg,
1903 1,
1904 16 * u64::SIZE as u64,
1905 )
1906 .await
1907 .expect("failed to preflight");
1908 assert_eq!(recordings.snapshot().reads.len(), 2);
1909 preflight
1910 .finish()
1911 .await
1912 .expect("failed to finish preflight")
1913 .destroy()
1914 .await
1915 .expect("failed to destroy");
1916 });
1917 }
1918
1919 #[test_traced]
1920 fn test_segmented_fixed_restore_retains_empty_checkpoint_section() {
1921 let executor = deterministic::Runner::default();
1922 executor.start(|context| async move {
1923 let cfg = lazy_recovery_cfg("segmented-fixed-empty-restore");
1924 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1925 .await
1926 .expect("failed to init");
1927 for section in 0..=2 {
1928 (journal, _) = journal
1929 .append(section, §ion)
1930 .await
1931 .expect("failed to append");
1932 }
1933 journal = journal.sync_all().await.expect("failed to sync");
1934 journal = journal
1935 .test_reopen_section(1, 0)
1936 .await
1937 .expect("failed to empty checkpoint section");
1938 journal = journal.sync(1).await.expect("failed to sync empty section");
1939 drop(journal);
1940
1941 let journal = Journal::<_, u64>::preflight_restore(context.child("restore"), cfg, 1, 0)
1942 .await
1943 .expect("failed to preflight")
1944 .finish()
1945 .await
1946 .expect("failed to finish preflight");
1947
1948 assert_eq!(journal.sections().collect::<Vec<_>>(), vec![0, 1]);
1949 assert_eq!(journal.size(1).expect("missing checkpoint section"), 0);
1950 journal.destroy().await.expect("failed to destroy");
1951 });
1952 }
1953
1954 #[test_traced]
1955 fn test_segmented_fixed_restore_floors_block_below_checkpoint_repair() {
1956 let executor = deterministic::Runner::default();
1957 executor.start(|context| async move {
1958 let cfg = lazy_recovery_cfg("segmented-fixed-restore-floors");
1959 seed(&context, &cfg, 0..=1).await;
1960
1961 corrupt_page(&context, &cfg.partition, &1u64.to_be_bytes(), 2, 16).await;
1964 let size = 16 * u64::SIZE as u64;
1965 let journal = Journal::<_, u64>::preflight_restore(
1966 context.child("restore"),
1967 cfg.clone(),
1968 1,
1969 size,
1970 )
1971 .await
1972 .expect("failed to preflight")
1973 .finish()
1974 .await
1975 .expect("failed to finish preflight");
1976
1977 let mut replay = journal
1979 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1980 .await
1981 .expect("failed to start replay");
1982 let mut outcome = None;
1983 while let Some(item) = replay.next().await {
1984 if let Err(err) = item {
1985 outcome = Some(err);
1986 break;
1987 }
1988 }
1989 assert!(matches!(
1990 outcome,
1991 Some(Error::Runtime(RError::InvalidChecksum))
1992 ));
1993 drop(replay);
1994
1995 let (_, blob_size) = context
1997 .open(&cfg.partition, &1u64.to_be_bytes())
1998 .await
1999 .expect("failed to open");
2000 assert_eq!(blob_size, 8 * 28);
2001 });
2002 }
2003
2004 #[test_traced]
2005 fn test_segmented_fixed_floor_preflight_reads_terminal_entry_only() {
2006 let executor = deterministic::Runner::default();
2007 executor.start(|context| async move {
2008 let (context, recordings) = RecordingContext::new(context);
2009 let cfg = aligned_cfg();
2010 let mut journal = Journal::init(context.child("seed"), cfg.clone())
2011 .await
2012 .expect("failed to init");
2013 for value in 0..2 {
2014 (journal, _) = journal
2015 .append(1, &test_digest(value))
2016 .await
2017 .expect("failed to append");
2018 }
2019 journal = journal.sync(1).await.expect("failed to sync");
2020 drop(journal);
2021
2022 recordings.clear();
2023 let floors = BTreeMap::from([(1, 2)]);
2024 let journal = Journal::<_, Digest>::preflight_floors(
2025 context.child("reopen"),
2026 cfg,
2027 &floors,
2028 u64::MAX,
2029 )
2030 .await
2031 .expect("failed to preflight")
2032 .finish()
2033 .await
2034 .expect("failed to reopen");
2035
2036 assert_eq!(recordings.snapshot().reads.len(), 3);
2039 journal.destroy().await.expect("failed to destroy");
2040 });
2041 }
2042
2043 #[test_traced]
2044 fn test_segmented_fixed_replay_finish_before_drain_fails() {
2045 let executor = deterministic::Runner::default();
2046 executor.start(|context| async move {
2047 let cfg = test_cfg();
2048 let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
2049 .await
2050 .expect("failed to init");
2051 (journal, _) = journal
2052 .append(1, &test_digest(0))
2053 .await
2054 .expect("failed to append");
2055 journal = journal.sync_all().await.expect("failed to sync");
2056
2057 let replay = journal
2058 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2059 .await
2060 .expect("failed to replay");
2061 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2062 });
2063 }
2064
2065 #[test_traced]
2066 fn test_segmented_fixed_replay() {
2067 let executor = deterministic::Runner::default();
2068 executor.start(|context| async move {
2069 let cfg = test_cfg();
2070 let mut journal = Journal::init(context.child("first"), cfg.clone())
2071 .await
2072 .expect("failed to init");
2073
2074 for i in 0u64..10 {
2075 (journal, _) = journal
2076 .append(1, &test_digest(i))
2077 .await
2078 .expect("failed to append");
2079 }
2080 for i in 10u64..20 {
2081 (journal, _) = journal
2082 .append(2, &test_digest(i))
2083 .await
2084 .expect("failed to append");
2085 }
2086
2087 journal = journal.sync_all().await.expect("failed to sync");
2088 drop(journal);
2089
2090 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2091 .await
2092 .expect("failed to re-init");
2093
2094 let items = {
2095 let mut replay = journal
2096 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2097 .await
2098 .expect("failed to replay");
2099
2100 let mut items = Vec::new();
2101 while let Some(result) = replay.next().await {
2102 match result {
2103 Ok((section, pos, item)) => items.push((section, pos, item)),
2104 Err(err) => panic!("replay error: {err}"),
2105 }
2106 }
2107 journal = replay.finish().expect("failed to finish replay");
2108 items
2109 };
2110
2111 assert_eq!(items.len(), 20);
2112 for (i, item) in items.iter().enumerate().take(10) {
2113 assert_eq!(item.0, 1);
2114 assert_eq!(item.1, i as u64);
2115 assert_eq!(item.2, test_digest(i as u64));
2116 }
2117 for (i, item) in items.iter().enumerate().skip(10).take(10) {
2118 assert_eq!(item.0, 2);
2119 assert_eq!(item.1, (i - 10) as u64);
2120 assert_eq!(item.2, test_digest(i as u64));
2121 }
2122
2123 journal.destroy().await.expect("failed to destroy");
2124 });
2125 }
2126
2127 #[test_traced]
2128 fn test_segmented_fixed_replay_with_start_offset() {
2129 let executor = deterministic::Runner::default();
2131 executor.start(|context| async move {
2132 let cfg = test_cfg();
2133 let mut journal = Journal::init(context.child("first"), cfg.clone())
2134 .await
2135 .expect("failed to init");
2136
2137 for i in 0u64..10 {
2139 (journal, _) = journal
2140 .append(1, &test_digest(i))
2141 .await
2142 .expect("failed to append");
2143 }
2144 for i in 10u64..15 {
2146 (journal, _) = journal
2147 .append(2, &test_digest(i))
2148 .await
2149 .expect("failed to append");
2150 }
2151 journal = journal.sync_all().await.expect("failed to sync");
2152 drop(journal);
2153
2154 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2155 .await
2156 .expect("failed to re-init");
2157
2158 {
2160 let mut replay = journal
2161 .replay(1, 5, NZUsize!(1024), ReadOptions::default())
2162 .await
2163 .expect("failed to replay");
2164
2165 let mut items = Vec::new();
2166 while let Some(result) = replay.next().await {
2167 let (section, pos, item) = result.expect("replay error");
2168 items.push((section, pos, item));
2169 }
2170 journal = replay.finish().expect("failed to finish replay");
2171
2172 assert_eq!(
2173 items.len(),
2174 10,
2175 "Should have 5 items from section 1 + 5 from section 2"
2176 );
2177
2178 for (i, (section, pos, item)) in items.iter().enumerate().take(5) {
2180 assert_eq!(*section, 1);
2181 assert_eq!(*pos, (i + 5) as u64);
2182 assert_eq!(*item, test_digest((i + 5) as u64));
2183 }
2184
2185 for (i, (section, pos, item)) in items.iter().enumerate().skip(5) {
2187 assert_eq!(*section, 2);
2188 assert_eq!(*pos, (i - 5) as u64);
2189 assert_eq!(*item, test_digest((i + 5) as u64));
2190 }
2191 }
2192
2193 {
2195 let mut replay = journal
2196 .replay(1, 9, NZUsize!(1024), ReadOptions::default())
2197 .await
2198 .expect("failed to replay");
2199
2200 let mut items = Vec::new();
2201 while let Some(result) = replay.next().await {
2202 let (section, pos, item) = result.expect("replay error");
2203 items.push((section, pos, item));
2204 }
2205 journal = replay.finish().expect("failed to finish replay");
2206
2207 assert_eq!(
2208 items.len(),
2209 6,
2210 "Should have 1 item from section 1 + 5 from section 2"
2211 );
2212 assert_eq!(items[0], (1, 9, test_digest(9)));
2213 for (i, (section, pos, item)) in items.iter().enumerate().skip(1) {
2214 assert_eq!(*section, 2);
2215 assert_eq!(*pos, (i - 1) as u64);
2216 assert_eq!(*item, test_digest((i + 9) as u64));
2217 }
2218 }
2219
2220 {
2222 let mut replay = journal
2223 .replay(2, 3, NZUsize!(1024), ReadOptions::default())
2224 .await
2225 .expect("failed to replay");
2226
2227 let mut items = Vec::new();
2228 while let Some(result) = replay.next().await {
2229 let (section, pos, item) = result.expect("replay error");
2230 items.push((section, pos, item));
2231 }
2232 journal = replay.finish().expect("failed to finish replay");
2233
2234 assert_eq!(items.len(), 2, "Should have 2 items from section 2");
2235 assert_eq!(items[0], (2, 3, test_digest(13)));
2236 assert_eq!(items[1], (2, 4, test_digest(14)));
2237 }
2238
2239 let result = journal
2242 .replay(1, 100, NZUsize!(1024), ReadOptions::default())
2243 .await;
2244 assert!(matches!(result, Err(Error::ItemOutOfRange(100))));
2245
2246 let journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
2247 .await
2248 .expect("failed to re-init");
2249 let result = journal
2250 .replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
2251 .await;
2252 assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
2253
2254 let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
2255 .await
2256 .expect("failed to re-init");
2257 journal.destroy().await.expect("failed to destroy");
2258 });
2259 }
2260
2261 #[test_traced]
2262 fn test_segmented_fixed_prune() {
2263 let executor = deterministic::Runner::default();
2264 executor.start(|context| async move {
2265 let cfg = test_cfg();
2266 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2267 .await
2268 .expect("failed to init");
2269
2270 for section in 1u64..=5 {
2271 (journal, _) = journal
2272 .append(section, &test_digest(section))
2273 .await
2274 .expect("failed to append");
2275 }
2276 journal = journal.sync_all().await.expect("failed to sync");
2277
2278 (journal, _) = journal.prune(3).await.expect("failed to prune");
2279
2280 let err = journal.get(1, 0).await;
2281 assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
2282
2283 let err = journal.get(2, 0).await;
2284 assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
2285
2286 let item = journal.get(3, 0).await.expect("should exist");
2287 assert_eq!(item, test_digest(3));
2288
2289 journal.destroy().await.expect("failed to destroy");
2290 });
2291 }
2292
2293 #[test_traced]
2294 fn test_segmented_fixed_pruned_after_full_prune() {
2295 let executor = deterministic::Runner::default();
2298 executor.start(|context| async move {
2299 let cfg = test_cfg();
2300 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2301 .await
2302 .expect("failed to init");
2303
2304 for section in 1u64..=3 {
2305 (journal, _) = journal
2306 .append(section, &test_digest(section))
2307 .await
2308 .expect("failed to append");
2309 }
2310 journal = journal.sync_all().await.expect("failed to sync");
2311
2312 (journal, _) = journal.prune(10).await.expect("failed to prune");
2313 assert_eq!(journal.oldest_section(), None);
2314 assert!(journal.pruned(3));
2315 assert!(journal.pruned(9));
2316 assert!(!journal.pruned(10));
2317
2318 journal.destroy().await.expect("failed to destroy");
2319 });
2320 }
2321
2322 #[test_traced]
2323 fn test_segmented_fixed_truncate() {
2324 let executor = deterministic::Runner::default();
2325 executor.start(|context| async move {
2326 let cfg = test_cfg();
2327 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2328 .await
2329 .expect("failed to init");
2330
2331 for section in 1u64..=3 {
2333 (journal, _) = journal
2334 .append(section, &test_digest(section))
2335 .await
2336 .expect("failed to append");
2337 }
2338 journal = journal.sync_all().await.expect("failed to sync");
2339
2340 for section in 1u64..=3 {
2342 let size = journal.size(section).expect("failed to get size");
2343 assert!(size > 0, "section {section} should have data");
2344 }
2345
2346 let size = journal.size(1).expect("failed to get size");
2348 journal = journal
2349 .test_reopen_at_most(1, size)
2350 .await
2351 .expect("failed to truncate");
2352
2353 let size = journal.size(1).expect("failed to get size");
2355 assert!(size > 0, "section 1 should still have data");
2356
2357 for section in 2u64..=3 {
2359 let size = journal.size(section).expect("failed to get size");
2360 assert_eq!(size, 0, "section {section} should be removed");
2361 }
2362
2363 let item = journal.get(1, 0).await.expect("failed to get");
2365 assert_eq!(item, test_digest(1));
2366
2367 journal.destroy().await.expect("failed to destroy");
2368 });
2369 }
2370
2371 #[test_traced]
2372 fn test_segmented_fixed_truncate_max_section() {
2373 let executor = deterministic::Runner::default();
2374 executor.start(|context| async move {
2375 let cfg = test_cfg();
2376 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2377 .await
2378 .expect("failed to init");
2379
2380 (journal, _) = journal
2382 .append(u64::MAX, &test_digest(0))
2383 .await
2384 .expect("failed to append");
2385 journal = journal.sync_all().await.expect("failed to sync");
2386
2387 let size = journal.size(u64::MAX).expect("failed to get size");
2389 journal = journal
2390 .test_reopen_at_most(u64::MAX, size)
2391 .await
2392 .expect("failed to truncate");
2393
2394 assert_eq!(journal.size(u64::MAX).expect("failed to get size"), size);
2396 assert_eq!(journal.get(u64::MAX, 0).await.unwrap(), test_digest(0));
2397
2398 journal.destroy().await.expect("failed to destroy");
2399 });
2400 }
2401
2402 #[test_traced]
2403 fn test_segmented_fixed_truncate_many_sections() {
2404 let executor = deterministic::Runner::default();
2405 executor.start(|context| async move {
2406 let cfg = test_cfg();
2407 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2408 .await
2409 .expect("failed to init");
2410
2411 for section in 1u64..=10 {
2413 (journal, _) = journal
2414 .append(section, &test_digest(section))
2415 .await
2416 .expect("failed to append");
2417 }
2418 journal = journal.sync_all().await.expect("failed to sync");
2419
2420 let size = journal.size(5).expect("failed to get size");
2422 journal = journal
2423 .test_reopen_at_most(5, size)
2424 .await
2425 .expect("failed to truncate");
2426
2427 for section in 1u64..=5 {
2429 let size = journal.size(section).expect("failed to get size");
2430 assert!(size > 0, "section {section} should still have data");
2431 }
2432
2433 for section in 6u64..=10 {
2435 let size = journal.size(section).expect("failed to get size");
2436 assert_eq!(size, 0, "section {section} should be removed");
2437 }
2438
2439 {
2441 let mut replay = journal
2442 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2443 .await
2444 .expect("failed to replay");
2445 let mut items = Vec::new();
2446 while let Some(result) = replay.next().await {
2447 let (section, _, item) = result.expect("failed to read");
2448 items.push((section, item));
2449 }
2450 journal = replay.finish().expect("failed to finish replay");
2451 assert_eq!(items.len(), 5);
2452 for (i, (section, item)) in items.iter().enumerate() {
2453 assert_eq!(*section, (i + 1) as u64);
2454 assert_eq!(*item, test_digest((i + 1) as u64));
2455 }
2456 }
2457
2458 journal.destroy().await.expect("failed to destroy");
2459 });
2460 }
2461
2462 #[test_traced]
2463 fn test_segmented_fixed_truncate_persistence() {
2464 let executor = deterministic::Runner::default();
2465 executor.start(|context| async move {
2466 let cfg = test_cfg();
2467
2468 let mut journal = Journal::init(context.child("first"), cfg.clone())
2470 .await
2471 .expect("failed to init");
2472 for section in 1u64..=5 {
2473 (journal, _) = journal
2474 .append(section, &test_digest(section))
2475 .await
2476 .expect("failed to append");
2477 }
2478 journal = journal.sync_all().await.expect("failed to sync");
2479
2480 let size = journal.size(2).expect("failed to get size");
2482 journal = journal
2483 .test_reopen_at_most(2, size)
2484 .await
2485 .expect("failed to truncate");
2486 journal = journal.sync_all().await.expect("failed to sync");
2487 drop(journal);
2488
2489 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2491 .await
2492 .expect("failed to re-init");
2493
2494 for section in 1u64..=2 {
2496 let size = journal.size(section).expect("failed to get size");
2497 assert!(size > 0, "section {section} should have data after restart");
2498 }
2499
2500 for section in 3u64..=5 {
2502 let size = journal.size(section).expect("failed to get size");
2503 assert_eq!(size, 0, "section {section} should be gone after restart");
2504 }
2505
2506 let item1 = journal.get(1, 0).await.expect("failed to get");
2508 assert_eq!(item1, test_digest(1));
2509 let item2 = journal.get(2, 0).await.expect("failed to get");
2510 assert_eq!(item2, test_digest(2));
2511
2512 journal.destroy().await.expect("failed to destroy");
2513 });
2514 }
2515
2516 #[test_traced]
2517 fn test_segmented_fixed_corruption_recovery() {
2518 let executor = deterministic::Runner::default();
2519 executor.start(|context| async move {
2520 let cfg = test_cfg();
2521 let mut journal = Journal::init(context.child("first"), cfg.clone())
2522 .await
2523 .expect("failed to init");
2524
2525 for i in 0u64..5 {
2526 (journal, _) = journal
2527 .append(1, &test_digest(i))
2528 .await
2529 .expect("failed to append");
2530 }
2531 journal = journal.sync_all().await.expect("failed to sync");
2532 drop(journal);
2533
2534 let (blob, size) = context
2535 .open(&cfg.partition, &1u64.to_be_bytes())
2536 .await
2537 .expect("failed to open blob");
2538 blob.resize(size - 1).await.expect("failed to truncate");
2539 blob.sync().await.expect("failed to sync");
2540
2541 drop(blob);
2542 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2543 .await
2544 .expect("failed to re-init");
2545
2546 let count = {
2547 let mut replay = journal
2548 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2549 .await
2550 .expect("failed to replay");
2551
2552 let mut count = 0;
2553 while let Some(result) = replay.next().await {
2554 result.expect("should be ok");
2555 count += 1;
2556 }
2557 journal = replay.finish().expect("failed to finish replay");
2558 count
2559 };
2560 assert_eq!(count, 4);
2561
2562 journal.destroy().await.expect("failed to destroy");
2563 });
2564 }
2565
2566 #[test_traced]
2567 fn test_segmented_fixed_persistence() {
2568 let executor = deterministic::Runner::default();
2569 executor.start(|context| async move {
2570 let cfg = test_cfg();
2571
2572 let mut journal = Journal::init(context.child("first"), cfg.clone())
2574 .await
2575 .expect("failed to init");
2576
2577 for i in 0u64..5 {
2578 (journal, _) = journal
2579 .append(1, &test_digest(i))
2580 .await
2581 .expect("failed to append");
2582 }
2583 journal = journal.sync_all().await.expect("failed to sync");
2584 drop(journal);
2585
2586 let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
2588 .await
2589 .expect("failed to re-init");
2590
2591 for i in 0u64..5 {
2592 let item = journal.get(1, i).await.expect("failed to get");
2593 assert_eq!(item, test_digest(i));
2594 }
2595
2596 journal.destroy().await.expect("failed to destroy");
2597 });
2598 }
2599
2600 #[test_traced]
2601 fn test_segmented_fixed_sync() {
2602 let executor = deterministic::Runner::default();
2603 executor.start(|context| async move {
2604 let cfg = test_cfg();
2605 let mut journal = Journal::init(context.child("first"), cfg.clone())
2606 .await
2607 .expect("failed to init");
2608
2609 for section in 1u64..=3 {
2611 (journal, _) = journal
2612 .append(section, &test_digest(section))
2613 .await
2614 .expect("failed to append");
2615 }
2616
2617 journal
2619 .sync(&[1, 3, 99])
2620 .await
2621 .expect("failed to sync sections");
2622
2623 let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
2625 .await
2626 .expect("failed to re-init");
2627 assert_eq!(
2628 journal.get(1, 0).await.expect("section 1 durable"),
2629 test_digest(1)
2630 );
2631 assert_eq!(
2632 journal.get(3, 0).await.expect("section 3 durable"),
2633 test_digest(3)
2634 );
2635 assert!(matches!(
2636 journal.get(2, 0).await,
2637 Err(Error::ItemOutOfRange(0)) | Err(Error::SectionOutOfRange(2))
2638 ));
2639
2640 journal.destroy().await.expect("failed to destroy");
2641 });
2642 }
2643
2644 #[test_traced]
2645 fn test_segmented_fixed_section_len() {
2646 let executor = deterministic::Runner::default();
2647 executor.start(|context| async move {
2648 let cfg = test_cfg();
2649 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2650 .await
2651 .expect("failed to init");
2652
2653 assert_eq!(journal.section_len(1).unwrap(), 0);
2654
2655 for i in 0u64..5 {
2656 (journal, _) = journal
2657 .append(1, &test_digest(i))
2658 .await
2659 .expect("failed to append");
2660 }
2661
2662 assert_eq!(journal.section_len(1).unwrap(), 5);
2663 assert_eq!(journal.section_len(2).unwrap(), 0);
2664
2665 journal.destroy().await.expect("failed to destroy");
2666 });
2667 }
2668
2669 #[test_traced]
2670 fn test_segmented_fixed_non_contiguous_sections() {
2671 let executor = deterministic::Runner::default();
2674 executor.start(|context| async move {
2675 let cfg = test_cfg();
2676 let mut journal = Journal::init(context.child("first"), cfg.clone())
2677 .await
2678 .expect("failed to init");
2679
2680 (journal, _) = journal
2682 .append(1, &test_digest(100))
2683 .await
2684 .expect("failed to append");
2685 (journal, _) = journal
2686 .append(5, &test_digest(500))
2687 .await
2688 .expect("failed to append");
2689 (journal, _) = journal
2690 .append(10, &test_digest(1000))
2691 .await
2692 .expect("failed to append");
2693 journal = journal.sync_all().await.expect("failed to sync");
2694
2695 assert_eq!(journal.get(1, 0).await.unwrap(), test_digest(100));
2697 assert_eq!(journal.get(5, 0).await.unwrap(), test_digest(500));
2698 assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(1000));
2699
2700 for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
2702 let result = journal.get(missing_section, 0).await;
2703 assert!(
2704 matches!(result, Err(Error::SectionOutOfRange(_))),
2705 "Expected SectionOutOfRange for section {}, got {:?}",
2706 missing_section,
2707 result
2708 );
2709 }
2710
2711 drop(journal);
2713 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2714 .await
2715 .expect("failed to re-init");
2716
2717 {
2719 let mut replay = journal
2720 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2721 .await
2722 .expect("failed to replay");
2723
2724 let mut items = Vec::new();
2725 while let Some(result) = replay.next().await {
2726 let (section, _, item) = result.expect("replay error");
2727 items.push((section, item));
2728 }
2729 journal = replay.finish().expect("failed to finish replay");
2730
2731 assert_eq!(items.len(), 3, "Should have 3 items");
2732 assert_eq!(items[0], (1, test_digest(100)));
2733 assert_eq!(items[1], (5, test_digest(500)));
2734 assert_eq!(items[2], (10, test_digest(1000)));
2735 }
2736
2737 {
2739 let mut replay = journal
2740 .replay(5, 0, NZUsize!(1024), ReadOptions::default())
2741 .await
2742 .expect("failed to replay from section 5");
2743
2744 let mut items = Vec::new();
2745 while let Some(result) = replay.next().await {
2746 let (section, _, item) = result.expect("replay error");
2747 items.push((section, item));
2748 }
2749 journal = replay.finish().expect("failed to finish replay");
2750
2751 assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
2752 assert_eq!(items[0], (5, test_digest(500)));
2753 assert_eq!(items[1], (10, test_digest(1000)));
2754 }
2755
2756 journal.destroy().await.expect("failed to destroy");
2757 });
2758 }
2759
2760 #[test_traced]
2761 fn test_segmented_fixed_empty_section_in_middle() {
2762 let executor = deterministic::Runner::default();
2765 executor.start(|context| async move {
2766 let cfg = test_cfg();
2767 let mut journal = Journal::init(context.child("first"), cfg.clone())
2768 .await
2769 .expect("failed to init");
2770
2771 (journal, _) = journal
2773 .append(1, &test_digest(100))
2774 .await
2775 .expect("failed to append");
2776
2777 (journal, _) = journal
2779 .append(2, &test_digest(200))
2780 .await
2781 .expect("failed to append");
2782 journal = journal.sync(2).await.expect("failed to sync");
2783 journal = journal
2784 .test_reopen_section(2, 0)
2785 .await
2786 .expect("failed to truncate");
2787
2788 (journal, _) = journal
2790 .append(3, &test_digest(300))
2791 .await
2792 .expect("failed to append");
2793
2794 journal = journal.sync_all().await.expect("failed to sync");
2795
2796 assert_eq!(journal.section_len(1).unwrap(), 1);
2798 assert_eq!(journal.section_len(2).unwrap(), 0);
2799 assert_eq!(journal.section_len(3).unwrap(), 1);
2800
2801 drop(journal);
2803 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2804 .await
2805 .expect("failed to re-init");
2806
2807 {
2809 let mut replay = journal
2810 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2811 .await
2812 .expect("failed to replay");
2813
2814 let mut items = Vec::new();
2815 while let Some(result) = replay.next().await {
2816 let (section, _, item) = result.expect("replay error");
2817 items.push((section, item));
2818 }
2819 journal = replay.finish().expect("failed to finish replay");
2820
2821 assert_eq!(
2822 items.len(),
2823 2,
2824 "Should have 2 items (skipping empty section)"
2825 );
2826 assert_eq!(items[0], (1, test_digest(100)));
2827 assert_eq!(items[1], (3, test_digest(300)));
2828 }
2829
2830 {
2832 let mut replay = journal
2833 .replay(2, 0, NZUsize!(1024), ReadOptions::default())
2834 .await
2835 .expect("failed to replay from section 2");
2836
2837 let mut items = Vec::new();
2838 while let Some(result) = replay.next().await {
2839 let (section, _, item) = result.expect("replay error");
2840 items.push((section, item));
2841 }
2842 journal = replay.finish().expect("failed to finish replay");
2843
2844 assert_eq!(items.len(), 1, "Should have 1 item from section 3");
2845 assert_eq!(items[0], (3, test_digest(300)));
2846 }
2847
2848 journal.destroy().await.expect("failed to destroy");
2849 });
2850 }
2851
2852 #[test_traced]
2853 fn test_segmented_fixed_validates_pages_before_trailing_bytes() {
2854 let executor = deterministic::Runner::default();
2855 executor.start(|context| async move {
2856 const LOGICAL_PAGE_SIZE: u64 = 5;
2857 const SECTION: u64 = 0;
2858
2859 let cfg = Config {
2860 partition: "segmented-fixed-validate-before-tail-trim".into(),
2861 page_cache: CacheRef::new(NZU16!(LOGICAL_PAGE_SIZE as u16), NZUsize!(4)),
2862 write_buffer: NZUsize!(128),
2863 };
2864 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2865 .await
2866 .unwrap();
2867 for value in [11u64, 22, 33, 44] {
2868 (journal, _) = journal.append(SECTION, &value).await.unwrap();
2869 }
2870 journal = journal.sync_all().await.unwrap();
2871 drop(journal);
2872
2873 let (blob, size) = context
2874 .open(&cfg.partition, &SECTION.to_be_bytes())
2875 .await
2876 .unwrap();
2877 let writer = commonware_runtime::buffer::paged::Recovery::open(
2878 blob,
2879 size,
2880 128,
2881 cfg.page_cache.clone(),
2882 )
2883 .await
2884 .unwrap();
2885 let writer = writer.truncate(30).await.unwrap();
2886 writer.sync().await.unwrap();
2887
2888 corrupt_page(
2898 &context,
2899 &cfg.partition,
2900 &SECTION.to_be_bytes(),
2901 4,
2902 LOGICAL_PAGE_SIZE,
2903 )
2904 .await;
2905
2906 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
2907 .await
2908 .unwrap();
2909 let journal = replay_all(journal).await;
2910 assert_eq!(journal.section_len(SECTION).unwrap(), 2);
2911 assert_eq!(journal.get(SECTION, 0).await.unwrap(), 11);
2912 assert_eq!(journal.get(SECTION, 1).await.unwrap(), 22);
2913 journal.destroy().await.unwrap();
2914 });
2915 }
2916
2917 #[test_traced]
2918 fn test_segmented_fixed_repairs_torn_interior_page() {
2919 const SECTION: u64 = 0;
2920
2921 let executor = deterministic::Runner::default();
2922 executor.start(|context| async move {
2923 let cfg = aligned_cfg();
2924 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2925 .await
2926 .unwrap();
2927 for value in 0..6 {
2928 (journal, _) = journal.append(SECTION, &value).await.unwrap();
2929 }
2930 journal = journal.sync_all().await.unwrap();
2931 drop(journal);
2932
2933 corrupt_page(&context, &cfg.partition, &SECTION.to_be_bytes(), 1, 16).await;
2943
2944 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
2945 .await
2946 .unwrap();
2947 journal = replay_all(journal).await;
2948 assert_eq!(journal.section_len(SECTION).unwrap(), 2);
2949 assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
2950 assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
2951 assert!(matches!(
2952 journal.get(SECTION, 2).await,
2953 Err(Error::ItemOutOfRange(2))
2954 ));
2955
2956 let position;
2959 (journal, position) = journal.append(SECTION, &99).await.unwrap();
2960 assert_eq!(position, 2);
2961 journal.sync_all().await.unwrap();
2962
2963 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
2964 .await
2965 .unwrap();
2966 assert_eq!(journal.section_len(SECTION).unwrap(), 3);
2967 assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
2968 assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
2969 assert_eq!(journal.get(SECTION, 2).await.unwrap(), 99);
2970 journal.destroy().await.unwrap();
2971 });
2972 }
2973
2974 #[rstest::rstest]
2977 #[case::tail(false)]
2978 #[case::torn(true)]
2979 fn test_segmented_fixed_replay_dropped_during_repair_fails_replay(#[case] torn: bool) {
2980 const SECTION: u64 = 0;
2981
2982 let executor = deterministic::Runner::default();
2983 executor.start(|context| async move {
2984 let cfg = aligned_cfg();
2986 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2987 .await
2988 .unwrap();
2989 for value in 0..6 {
2990 (journal, _) = journal.append(SECTION, &value).await.unwrap();
2991 }
2992 journal = journal.sync_all().await.unwrap();
2993 drop(journal);
2994
2995 let (drained, retained) = if torn {
2998 corrupt_page(&context, &cfg.partition, &SECTION.to_be_bytes(), 1, 16).await;
2999 (0, 2)
3000 } else {
3001 let (blob, size) = context
3002 .open(&cfg.partition, &SECTION.to_be_bytes())
3003 .await
3004 .unwrap();
3005 let writer = commonware_runtime::buffer::paged::Recovery::open(
3006 blob,
3007 size,
3008 cfg.write_buffer.get(),
3009 cfg.page_cache.clone(),
3010 )
3011 .await
3012 .unwrap();
3013 writer.truncate(44).await.unwrap();
3014 (5, 5)
3015 };
3016
3017 let pending = PendingSyncs::default();
3020 let gated = DelayedSyncContext {
3021 inner: context.child("second"),
3022 pending: pending.clone(),
3023 };
3024 let journal = Journal::<_, u64>::init(gated, cfg.clone()).await.unwrap();
3025 let mut replay = journal
3026 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3027 .await
3028 .unwrap();
3029 for value in 0..drained {
3030 assert_eq!(
3031 replay.next().await.unwrap().unwrap(),
3032 (SECTION, value, value)
3033 );
3034 }
3035 pending.arm();
3036 {
3037 let next = replay.next();
3038 futures::pin_mut!(next);
3039 assert!(futures::poll!(next.as_mut()).is_pending());
3040 }
3041 release_pending_syncs(&pending);
3042
3043 assert!(matches!(
3045 replay.next().await,
3046 Some(Err(Error::ReplayInterrupted))
3047 ));
3048 assert!(replay.next().await.is_none());
3049 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
3050
3051 let journal = Journal::<_, u64>::init(context.child("third"), cfg)
3053 .await
3054 .unwrap();
3055 let journal = replay_all(journal).await;
3056 assert_eq!(journal.section_len(SECTION).unwrap(), retained);
3057 for value in 0..retained {
3058 assert_eq!(journal.get(SECTION, value).await.unwrap(), value);
3059 }
3060 journal.destroy().await.unwrap();
3061 });
3062 }
3063
3064 #[test_traced]
3065 fn test_segmented_fixed_truncation_recovery_across_page_boundary() {
3066 let executor = deterministic::Runner::default();
3078 executor.start(|context| async move {
3079 let cfg = test_cfg();
3080 let mut journal = Journal::init(context.child("first"), cfg.clone())
3081 .await
3082 .expect("failed to init");
3083
3084 for i in 0u64..3 {
3086 (journal, _) = journal
3087 .append(1, &test_digest(i))
3088 .await
3089 .expect("failed to append");
3090 }
3091 journal = journal.sync_all().await.expect("failed to sync");
3092
3093 for i in 0u64..3 {
3095 let item = journal.get(1, i).await.expect("failed to get");
3096 assert_eq!(item, test_digest(i));
3097 }
3098 drop(journal);
3099
3100 let (blob, size) = context
3102 .open(&cfg.partition, &1u64.to_be_bytes())
3103 .await
3104 .expect("failed to open blob");
3105 blob.resize(size - 1).await.expect("failed to truncate");
3106 blob.sync().await.expect("failed to sync");
3107 drop(blob);
3108
3109 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
3112 .await
3113 .expect("failed to re-init");
3114 let journal = replay_all(journal).await;
3115
3116 assert_eq!(journal.section_len(1).unwrap(), 2);
3118
3119 assert_eq!(journal.size(1).unwrap(), 64);
3122
3123 let item0 = journal.get(1, 0).await.expect("failed to get item 0");
3125 assert_eq!(item0, test_digest(0));
3126 let item1 = journal.get(1, 1).await.expect("failed to get item 1");
3127 assert_eq!(item1, test_digest(1));
3128
3129 let err = journal.get(1, 2).await;
3131 assert!(
3132 matches!(err, Err(Error::ItemOutOfRange(2))),
3133 "expected ItemOutOfRange(2), got {:?}",
3134 err
3135 );
3136
3137 journal.destroy().await.expect("failed to destroy");
3138 });
3139 }
3140
3141 #[test_traced]
3142 fn test_journal_clear() {
3143 let executor = deterministic::Runner::default();
3144 executor.start(|context| async move {
3145 let cfg = Config {
3146 partition: "clear-test".into(),
3147 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3148 write_buffer: NZUsize!(1024),
3149 };
3150
3151 let mut journal: Journal<_, Digest> =
3152 Journal::init(context.child("journal"), cfg.clone())
3153 .await
3154 .expect("Failed to initialize journal");
3155
3156 for section in 0..5u64 {
3158 for i in 0..10u64 {
3159 (journal, _) = journal
3160 .append(section, &test_digest(section * 1000 + i))
3161 .await
3162 .expect("Failed to append");
3163 }
3164 journal = journal.sync(section).await.expect("Failed to sync");
3165 }
3166
3167 assert_eq!(journal.get(0, 0).await.unwrap(), test_digest(0));
3169 assert_eq!(journal.get(4, 0).await.unwrap(), test_digest(4000));
3170
3171 journal = journal.clear().await.expect("Failed to clear");
3173
3174 for section in 0..5u64 {
3176 assert!(matches!(
3177 journal.get(section, 0).await,
3178 Err(Error::SectionOutOfRange(s)) if s == section
3179 ));
3180 }
3181
3182 for i in 0..5u64 {
3184 (journal, _) = journal
3185 .append(10, &test_digest(i * 100))
3186 .await
3187 .expect("Failed to append after clear");
3188 }
3189 journal = journal.sync(10).await.expect("Failed to sync after clear");
3190
3191 assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(0));
3193
3194 assert!(matches!(
3196 journal.get(0, 0).await,
3197 Err(Error::SectionOutOfRange(0))
3198 ));
3199
3200 journal.destroy().await.unwrap();
3201 });
3202 }
3203
3204 #[test_traced]
3205 fn test_last_missing_section_returns_error() {
3206 let executor = deterministic::Runner::default();
3207 executor.start(|context| async move {
3208 let cfg = test_cfg();
3209 let journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
3210 .await
3211 .expect("failed to init");
3212
3213 assert!(matches!(
3214 journal.last(0).await,
3215 Err(Error::SectionOutOfRange(0))
3216 ));
3217 assert!(matches!(
3218 journal.last(99).await,
3219 Err(Error::SectionOutOfRange(99))
3220 ));
3221
3222 journal.destroy().await.unwrap();
3223 });
3224 }
3225
3226 #[test_traced]
3227 fn test_last_after_truncate_to_zero() {
3228 let executor = deterministic::Runner::default();
3229 executor.start(|context| async move {
3230 let cfg = test_cfg();
3231 let mut journal = Journal::init(context.child("storage"), cfg.clone())
3232 .await
3233 .expect("failed to init");
3234
3235 (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
3236 (journal, _) = journal.append(0, &test_digest(1)).await.unwrap();
3237 journal = journal.sync(0).await.unwrap();
3238
3239 assert!(journal.last(0).await.unwrap().is_some());
3240
3241 journal = journal.test_reopen_at_most(0, 0).await.unwrap();
3242 assert_eq!(journal.last(0).await.unwrap(), None);
3243
3244 journal.destroy().await.unwrap();
3245 });
3246 }
3247
3248 #[test_traced]
3249 fn test_last_pruned_section_returns_error() {
3250 let executor = deterministic::Runner::default();
3251 executor.start(|context| async move {
3252 let cfg = test_cfg();
3253 let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
3254 .await
3255 .expect("failed to init");
3256
3257 (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
3258 (journal, _) = journal.append(1, &test_digest(1)).await.unwrap();
3259 journal = journal.sync_all().await.unwrap();
3260
3261 (journal, _) = journal.prune(1).await.unwrap();
3262
3263 assert!(matches!(
3264 journal.last(0).await,
3265 Err(Error::AlreadyPrunedToSection(1))
3266 ));
3267 assert!(journal.last(1).await.unwrap().is_some());
3268
3269 journal.destroy().await.unwrap();
3270 });
3271 }
3272
3273 #[test_traced]
3274 fn test_get_many_empty() {
3275 let executor = deterministic::Runner::default();
3276 executor.start(|context| async move {
3277 let cfg = test_cfg();
3278 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3279 (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
3280 assert_eq!(journal.section_len(0).unwrap(), 1);
3281
3282 let mut buf = [];
3283 let (items, hits) = journal.get_many(0, &[], &mut buf).await.unwrap();
3284 assert!(items.is_empty());
3285 assert_eq!(hits, 0);
3286
3287 journal.destroy().await.unwrap();
3288 });
3289 }
3290
3291 #[test_traced]
3292 fn test_get_many_single_section() {
3293 let executor = deterministic::Runner::default();
3294 executor.start(|context| async move {
3295 let cfg = test_cfg();
3296 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3297
3298 for i in 0..5 {
3299 (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
3300 }
3301 assert_eq!(journal.section_len(0).unwrap(), 5);
3302
3303 let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
3306 let mut buf = vec![0u8; 6 * chunk];
3307 let (items, _) = journal
3308 .get_many(0, &[0, 1, 2, 3, 4], &mut buf)
3309 .await
3310 .unwrap();
3311
3312 for (i, item) in items.iter().enumerate() {
3313 assert_eq!(*item, test_digest(i as u64));
3314 }
3315
3316 journal.destroy().await.unwrap();
3317 });
3318 }
3319
3320 #[test_traced]
3321 fn test_get_many_subset() {
3322 let executor = deterministic::Runner::default();
3324 executor.start(|context| async move {
3325 let cfg = test_cfg();
3326 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3327
3328 for i in 0..10 {
3329 (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
3330 }
3331 assert_eq!(journal.section_len(0).unwrap(), 10);
3332
3333 let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
3334 let positions = [1, 4, 7, 9];
3335 let mut buf = vec![0u8; positions.len() * chunk];
3336 let (items, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
3337
3338 for (i, &pos) in positions.iter().enumerate() {
3339 assert_eq!(items[i], test_digest(pos));
3340 }
3341
3342 journal.destroy().await.unwrap();
3343 });
3344 }
3345
3346 #[test_traced]
3347 fn test_get_many_bad_section() {
3348 let executor = deterministic::Runner::default();
3349 executor.start(|context| async move {
3350 let cfg = test_cfg();
3351 let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
3352 .await
3353 .unwrap();
3354
3355 let mut buf = vec![0u8; 64];
3356 let err = journal.get_many(99, &[0], &mut buf).await.unwrap_err();
3357 assert!(matches!(err, Error::SectionOutOfRange(99)));
3358
3359 journal.destroy().await.unwrap();
3360 });
3361 }
3362
3363 #[test_traced]
3364 fn test_get_many_matches_get() {
3365 let executor = deterministic::Runner::default();
3367 executor.start(|context| async move {
3368 let cfg = test_cfg();
3369 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3370
3371 for i in 0..8 {
3372 (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
3373 }
3374 assert_eq!(journal.section_len(0).unwrap(), 8);
3375 journal = journal.sync_all().await.unwrap();
3376
3377 let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
3378 let positions: Vec<u64> = (0..8).collect();
3379 let mut buf = vec![0u8; positions.len() * chunk];
3380 let (batch, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
3381
3382 for pos in &positions {
3383 let single = journal.get(0, *pos).await.unwrap();
3384 assert_eq!(batch[*pos as usize], single);
3385 }
3386
3387 journal.destroy().await.unwrap();
3388 });
3389 }
3390
3391 #[test_traced]
3392 fn test_segmented_fixed_prune_waits_for_in_flight_start_sync() {
3393 let executor = deterministic::Runner::default();
3394 executor.start(|context| async move {
3395 let pending = PendingSyncs::default();
3396 let context = DelayedSyncContext {
3397 inner: context,
3398 pending: pending.clone(),
3399 };
3400 let cfg = test_cfg();
3401 let mut journal = Journal::init(context.child("storage"), cfg)
3402 .await
3403 .expect("failed to init");
3404
3405 (journal, _) = journal
3406 .append(1, &test_digest(0))
3407 .await
3408 .expect("failed to append");
3409 let handle;
3410 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
3411 assert!(!pending.lock().is_empty());
3412
3413 let started = Arc::new(AtomicUsize::new(0));
3414 let completed = Arc::new(AtomicUsize::new(0));
3415 let started_clone = started.clone();
3416 let completed_clone = completed.clone();
3417 let waiter = context.inner.child("prune").spawn(|_| async move {
3418 started_clone.fetch_add(1, Ordering::Relaxed);
3419 let (journal, pruned) = journal.prune(2).await.expect("failed to prune");
3420 assert!(pruned);
3421 completed_clone.fetch_add(1, Ordering::Relaxed);
3422 journal
3423 });
3424
3425 while started.load(Ordering::Relaxed) == 0 {
3426 commonware_runtime::reschedule().await;
3427 }
3428 commonware_runtime::reschedule().await;
3429 assert_eq!(
3430 completed.load(Ordering::Relaxed),
3431 0,
3432 "prune must wait for in-flight syncs on pruned sections"
3433 );
3434
3435 release_pending_syncs(&pending);
3436 handle
3437 .await
3438 .expect("sync handle should complete despite pruning");
3439 while completed.load(Ordering::Relaxed) == 0 {
3440 commonware_runtime::reschedule().await;
3441 }
3442 let journal = waiter.await.expect("prune task failed");
3443 assert_eq!(journal.oldest_section(), None);
3444 });
3445 }
3446
3447 #[test_traced]
3448 fn test_segmented_fixed_destroy_waits_for_in_flight_start_sync() {
3449 let executor = deterministic::Runner::default();
3450 executor.start(|context| async move {
3451 let pending = PendingSyncs::default();
3452 let context = DelayedSyncContext {
3453 inner: context,
3454 pending: pending.clone(),
3455 };
3456 let cfg = test_cfg();
3457 let mut journal = Journal::init(context.child("storage"), cfg)
3458 .await
3459 .expect("failed to init");
3460
3461 (journal, _) = journal
3462 .append(1, &test_digest(0))
3463 .await
3464 .expect("failed to append");
3465 let handle;
3466 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
3467 assert!(!pending.lock().is_empty());
3468
3469 let started = Arc::new(AtomicUsize::new(0));
3470 let completed = Arc::new(AtomicUsize::new(0));
3471 let started_clone = started.clone();
3472 let completed_clone = completed.clone();
3473 let waiter = context.inner.child("destroy").spawn(|_| async move {
3474 started_clone.fetch_add(1, Ordering::Relaxed);
3475 journal.destroy().await.expect("failed to destroy");
3476 completed_clone.fetch_add(1, Ordering::Relaxed);
3477 });
3478
3479 while started.load(Ordering::Relaxed) == 0 {
3480 commonware_runtime::reschedule().await;
3481 }
3482 commonware_runtime::reschedule().await;
3483 assert_eq!(
3484 completed.load(Ordering::Relaxed),
3485 0,
3486 "destroy must wait for in-flight syncs"
3487 );
3488
3489 release_pending_syncs(&pending);
3490 handle
3491 .await
3492 .expect("sync handle should complete despite destruction");
3493 while completed.load(Ordering::Relaxed) == 0 {
3494 commonware_runtime::reschedule().await;
3495 }
3496 waiter.await.expect("destroy task failed");
3497 });
3498 }
3499
3500 #[test_traced]
3501 fn test_segmented_fixed_clear_waits_for_in_flight_start_sync() {
3502 let executor = deterministic::Runner::default();
3503 executor.start(|context| async move {
3504 let pending = PendingSyncs::default();
3505 let context = DelayedSyncContext {
3506 inner: context,
3507 pending: pending.clone(),
3508 };
3509 let cfg = test_cfg();
3510 let mut journal = Journal::init(context.child("storage"), cfg)
3511 .await
3512 .expect("failed to init");
3513
3514 (journal, _) = journal
3515 .append(1, &test_digest(0))
3516 .await
3517 .expect("failed to append");
3518 let handle;
3519 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
3520 assert!(!pending.lock().is_empty());
3521
3522 let started = Arc::new(AtomicUsize::new(0));
3523 let completed = Arc::new(AtomicUsize::new(0));
3524 let started_clone = started.clone();
3525 let completed_clone = completed.clone();
3526 let waiter = context.inner.child("clear").spawn(|_| async move {
3527 started_clone.fetch_add(1, Ordering::Relaxed);
3528 journal = journal.clear().await.expect("failed to clear");
3529 completed_clone.fetch_add(1, Ordering::Relaxed);
3530 journal
3531 });
3532
3533 while started.load(Ordering::Relaxed) == 0 {
3534 commonware_runtime::reschedule().await;
3535 }
3536 commonware_runtime::reschedule().await;
3537 assert_eq!(
3538 completed.load(Ordering::Relaxed),
3539 0,
3540 "clear must wait for in-flight syncs"
3541 );
3542
3543 release_pending_syncs(&pending);
3544 handle
3545 .await
3546 .expect("sync handle should complete despite clearing");
3547 while completed.load(Ordering::Relaxed) == 0 {
3548 commonware_runtime::reschedule().await;
3549 }
3550 let mut journal = waiter.await.expect("clear task failed");
3551
3552 assert_eq!(journal.oldest_section(), None);
3554 let position;
3555 (journal, position) = journal
3556 .append(1, &test_digest(1))
3557 .await
3558 .expect("failed to append after clear");
3559 assert_eq!(position, 0);
3560 journal.destroy().await.expect("failed to destroy");
3561 });
3562 }
3563
3564 #[test_traced]
3565 fn test_segmented_fixed_prune_surfaces_failed_in_flight_start_sync() {
3566 let executor = deterministic::Runner::default();
3567 executor.start(|context| async move {
3568 let pending = PendingSyncs::default();
3569 let context = DelayedSyncContext {
3570 inner: context,
3571 pending: pending.clone(),
3572 };
3573 let cfg = test_cfg();
3574 let mut journal = Journal::init(context.child("storage"), cfg)
3575 .await
3576 .expect("failed to init");
3577
3578 (journal, _) = journal
3579 .append(1, &test_digest(0))
3580 .await
3581 .expect("failed to append");
3582 let handle;
3583 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
3584 fail_pending_syncs(&pending);
3585
3586 let err = journal
3587 .prune(2)
3588 .await
3589 .expect_err("prune must surface a failed in-flight sync");
3590 assert!(matches!(err, Error::Runtime(RError::Io(_))));
3591
3592 let err = handle.await.expect_err("sync handle should fail");
3593 assert!(matches!(err, RError::Io(_)));
3594 });
3595 }
3596}