1use super::manager::{
24 AppendFactory, Config as ManagerConfig, Manager, section_from_name, stored_names,
25};
26use crate::journal::Error;
27use commonware_codec::{CodecFixed, CodecFixedShared, DecodeExt as _, ReadExt as _};
28use commonware_runtime::{
29 Blob, Error as RError, Handle, Metrics, ReadOptions, Storage,
30 buffer::paged::{CacheRef, Replay as BlobReplay, Writer},
31};
32use commonware_utils::NZUsize;
33use std::{
34 collections::{BTreeMap, BTreeSet, VecDeque},
35 marker::PhantomData,
36 num::{NonZeroU16, NonZeroUsize},
37};
38use tracing::{trace, warn};
39
40struct SectionReplay<B: Blob> {
42 section: u64,
43 reader: BlobReplay<B>,
44 position: u64,
45}
46
47#[derive(Clone)]
49pub struct Config {
50 pub partition: String,
52
53 pub page_cache: CacheRef,
55
56 pub write_buffer: NonZeroUsize,
58}
59
60struct Inner<E: Storage + Metrics, A: CodecFixed> {
62 manager: Manager<E, AppendFactory>,
63
64 unrecovered: BTreeSet<u64>,
66
67 floors: BTreeMap<u64, u64>,
69
70 _array: PhantomData<A>,
71}
72
73enum PreflightMode {
75 Floors(BTreeMap<u64, u64>),
77 Restore {
79 section: u64,
81 floors: BTreeMap<u64, u64>,
82 },
83}
84
85pub(crate) struct RecoveryPreflight<E: Storage + Metrics, A: CodecFixed> {
87 context: E,
88 cfg: Config,
89
90 boundaries: BTreeMap<u64, Option<A>>,
92
93 mode: PreflightMode,
95}
96
97impl<E: Storage + Metrics, A: CodecFixedShared> RecoveryPreflight<E, A> {
98 pub(crate) const fn boundaries(&self) -> &BTreeMap<u64, Option<A>> {
100 &self.boundaries
101 }
102
103 pub(crate) async fn finish(self) -> Result<Journal<E, A>, Error> {
105 Ok(Journal(Box::new(
106 Inner::init(self.context, self.cfg, Some(self.mode)).await?,
107 )))
108 }
109}
110
111impl<E: Storage + Metrics, A: CodecFixed> Inner<E, A> {
112 fn writer(&mut self, section: u64) -> &mut Writer<E::Blob> {
114 self.manager
115 .get_mut(section)
116 .expect("replayed section is present")
117 }
118}
119
120impl<E: Storage + Metrics, A: CodecFixedShared> Inner<E, A> {
121 const CHUNK_SIZE: usize = A::SIZE;
123 const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE as u64;
124
125 async fn stored(context: &E, cfg: &Config) -> Result<BTreeMap<u64, Vec<u8>>, Error> {
127 let mut stored = BTreeMap::new();
129 for name in stored_names(context, &cfg.partition).await? {
130 let section = section_from_name(&name)?;
131 stored.insert(section, name);
132 }
133 Ok(stored)
134 }
135
136 async fn preflight_floors(
138 context: E,
139 cfg: Config,
140 minimum_items: &BTreeMap<u64, u64>,
141 ) -> Result<RecoveryPreflight<E, A>, Error> {
142 let stored = Self::stored(&context, &cfg).await?;
143 let page_size = cfg.page_cache.page_size();
144 let mut floor_sizes = BTreeMap::new();
145 let mut boundaries = BTreeMap::new();
146
147 for (§ion, &items) in minimum_items {
148 let Some(name) = stored.get(§ion) else {
150 return Err(Error::Corruption(format!(
151 "section {section} has a validation floor but no blob"
152 )));
153 };
154 let required = items.checked_mul(Self::CHUNK_SIZE_U64).ok_or_else(|| {
155 Error::Corruption(format!(
156 "section {section} validation floor {items} overflows its byte size"
157 ))
158 })?;
159 if required == 0 {
160 boundaries.insert(section, None);
161 continue;
162 }
163
164 let (blob, _) = context.open(&cfg.partition, name).await?;
167 let entry = Self::boundary(&blob, page_size, section, required).await?;
168 floor_sizes.insert(section, required);
169 boundaries.insert(section, Some(entry));
170 }
171
172 Ok(RecoveryPreflight {
173 context,
174 cfg,
175 boundaries,
176 mode: PreflightMode::Floors(floor_sizes),
177 })
178 }
179
180 async fn preflight_restore(
182 context: E,
183 cfg: Config,
184 section: u64,
185 size: u64,
186 ) -> Result<RecoveryPreflight<E, A>, Error> {
187 if !size.is_multiple_of(Self::CHUNK_SIZE_U64) {
189 return Err(Error::Corruption(format!(
190 "section {section} checkpoint size {size} is not item-aligned"
191 )));
192 }
193
194 let stored = Self::stored(&context, &cfg).await?;
196 if size > 0 && !stored.contains_key(§ion) {
197 return Err(Error::Corruption(format!(
198 "section {section} has a checkpoint but no blob"
199 )));
200 }
201
202 let page_size = cfg.page_cache.page_size();
203 let mut boundaries = BTreeMap::new();
204 let mut floors = BTreeMap::new();
205
206 for (&candidate, name) in stored.range(..=section) {
207 let (blob, physical_size) = context.open(&cfg.partition, name).await?;
210 let entry = if candidate == section {
211 if size == 0 {
212 None
213 } else {
214 let entry = Self::boundary(&blob, page_size, section, size).await?;
215 floors.insert(section, size);
216 Some(entry)
217 }
218 } else if physical_size == 0 {
219 None
220 } else {
221 let (logical_size, entry) = Writer::<E::Blob>::read_tail(
222 &blob,
223 physical_size,
224 page_size,
225 Self::CHUNK_SIZE,
226 ReadOptions::default(),
227 )
228 .await
229 .map_err(|err| Self::boundary_error(candidate, physical_size, err))?;
230 if !logical_size.is_multiple_of(Self::CHUNK_SIZE_U64) {
231 return Err(Error::Corruption(format!(
232 "section {candidate} is not a complete checkpoint-covered index"
233 )));
234 }
235 floors.insert(candidate, logical_size);
236 Some(A::decode(entry.coalesce()).map_err(Error::Codec)?)
237 };
238 boundaries.insert(candidate, entry);
239 }
240 boundaries.entry(section).or_insert(None);
241 Ok(RecoveryPreflight {
242 context,
243 cfg,
244 boundaries,
245 mode: PreflightMode::Restore { section, floors },
246 })
247 }
248
249 async fn boundary(
251 blob: &E::Blob,
252 page_size: NonZeroU16,
253 section: u64,
254 size: u64,
255 ) -> Result<A, Error> {
256 let entry = Writer::<E::Blob>::read_range(
257 blob,
258 page_size,
259 size - Self::CHUNK_SIZE_U64,
260 Self::CHUNK_SIZE,
261 ReadOptions::default(),
262 )
263 .await
264 .map_err(|err| Self::boundary_error(section, size, err))?;
265 A::decode(entry.coalesce()).map_err(Error::Codec)
266 }
267
268 fn boundary_error(section: u64, searched: u64, err: RError) -> Error {
273 match err {
274 RError::InvalidChecksum | RError::BlobInsufficientLength => Error::Corruption(format!(
275 "section {section} does not retain a valid durable boundary within {searched} bytes"
276 )),
277 err => err.into(),
278 }
279 }
280
281 async fn init(context: E, cfg: Config, mode: Option<PreflightMode>) -> Result<Self, Error> {
283 let (floors, restore) = match mode {
284 None => (BTreeMap::new(), None),
285 Some(PreflightMode::Floors(floors)) => (floors, None),
286 Some(PreflightMode::Restore { section, floors }) => {
287 let size = floors.get(§ion).copied().unwrap_or(0);
288 (floors, Some((section, size)))
289 }
290 };
291
292 let manager_cfg = ManagerConfig {
293 partition: cfg.partition,
294 factory: AppendFactory {
295 write_buffer: cfg.write_buffer,
296 page_cache_ref: cfg.page_cache,
297 },
298 };
299 let mut manager = Manager::init(context, manager_cfg).await?;
300 if let Some((section, size)) = restore {
301 manager.rewind(section, size).await?;
304 manager.sync(section).await?;
305 return Ok(Self {
306 manager,
307 unrecovered: BTreeSet::new(),
308 floors,
309 _array: PhantomData,
310 });
311 }
312
313 let mut unrecovered = BTreeSet::new();
314 for section in manager.sections() {
315 let size = manager.size(section)?;
316 let floor = floors.get(§ion).copied().unwrap_or(0);
317 if size < floor {
318 return Err(Error::Corruption(format!(
319 "section {section} retains {size} of its {floor}-byte validation floor"
320 )));
321 }
322 if size > floor {
323 unrecovered.insert(section);
324 }
325 }
326
327 Ok(Self {
328 manager,
329 unrecovered,
330 floors,
331 _array: PhantomData,
332 })
333 }
334
335 async fn append(&mut self, section: u64, item: &A) -> Result<u64, Error> {
337 assert!(
338 !self.unrecovered.contains(§ion),
339 "section {section} must be replayed before append"
340 );
341 let blob = self.manager.get_or_create(section).await?;
342
343 let buf = item.encode_mut();
345 let offset = blob.append(&buf).await?;
346 if !offset.is_multiple_of(Self::CHUNK_SIZE_U64) {
347 return Err(Error::InvalidBlobSize(section, offset));
348 }
349 let position = offset / Self::CHUNK_SIZE_U64;
350 trace!(section, position, "appended item");
351
352 Ok(position)
353 }
354
355 async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
357 let blob = self
358 .manager
359 .get(section)?
360 .ok_or(Error::SectionOutOfRange(section))?;
361
362 let offset = position
363 .checked_mul(Self::CHUNK_SIZE_U64)
364 .ok_or(Error::ItemOutOfRange(position))?;
365
366 let buf = blob
368 .read_at(offset, Self::CHUNK_SIZE)
369 .await
370 .map_err(|err| match err {
371 commonware_runtime::Error::BlobInsufficientLength
372 | commonware_runtime::Error::OffsetOverflow => Error::ItemOutOfRange(position),
373 err => Error::Runtime(err),
374 })?;
375 A::decode(buf.coalesce()).map_err(Error::Codec)
376 }
377
378 async fn get_many(
380 &self,
381 section: u64,
382 positions: &[u64],
383 buf: &mut [u8],
384 ) -> Result<(Vec<A>, usize), Error> {
385 assert!(
386 positions.is_sorted_by(|a, b| a < b),
387 "positions must be strictly increasing"
388 );
389 if positions.is_empty() {
390 return Ok((Vec::new(), 0));
391 }
392 assert!(
393 buf.len() >= positions.len() * Self::CHUNK_SIZE,
394 "get_many requires buf.len() >= positions.len() * CHUNK_SIZE"
395 );
396 let buf = &mut buf[..positions.len() * Self::CHUNK_SIZE];
397 let blob = self
398 .manager
399 .get(section)?
400 .ok_or(Error::SectionOutOfRange(section))?;
401
402 let offsets: Vec<u64> = positions
403 .iter()
404 .map(|&p| {
405 p.checked_mul(Self::CHUNK_SIZE_U64)
406 .ok_or(Error::ItemOutOfRange(p))
407 })
408 .collect::<Result<_, _>>()?;
409
410 let hits = blob
411 .read_many_into(buf, &offsets, NZUsize!(Self::CHUNK_SIZE))
412 .await?;
413
414 let mut items = Vec::with_capacity(positions.len());
415 for i in 0..positions.len() {
416 let slice = &buf[i * Self::CHUNK_SIZE..(i + 1) * Self::CHUNK_SIZE];
417 items.push(A::decode(slice).map_err(Error::Codec)?);
418 }
419 Ok((items, hits))
420 }
421
422 fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
424 let blob = self.manager.get(section).ok()??;
425 let offset = position.checked_mul(Self::CHUNK_SIZE_U64)?;
426 let remaining = blob.size().checked_sub(offset)?;
427 if remaining < Self::CHUNK_SIZE_U64 {
428 return None;
429 }
430 let mut buf = vec![0u8; Self::CHUNK_SIZE];
431 if !blob.try_read_sync_into(&mut buf, offset) {
432 return None;
433 }
434 A::decode(&buf[..]).ok()
435 }
436
437 async fn last(&self, section: u64) -> Result<Option<A>, Error> {
439 let blob = self
440 .manager
441 .get(section)?
442 .ok_or(Error::SectionOutOfRange(section))?;
443
444 let size = blob.size();
445 if size < Self::CHUNK_SIZE_U64 {
446 return Ok(None);
447 }
448
449 let last_position = (size / Self::CHUNK_SIZE_U64) - 1;
450 let offset = last_position * Self::CHUNK_SIZE_U64;
451 let buf = blob.read_at(offset, Self::CHUNK_SIZE).await?;
452 A::decode(buf.coalesce()).map_err(Error::Codec).map(Some)
453 }
454
455 async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
457 self.manager.sync(sections).await
458 }
459
460 async fn start_sync(&mut self, sections: impl crate::Sections) -> Result<Handle<()>, Error> {
462 self.manager.start_sync(sections).await
463 }
464
465 async fn sync_all(&mut self) -> Result<(), Error> {
467 self.manager.sync_all().await
468 }
469
470 async fn prune(&mut self, min: u64) -> Result<bool, Error> {
472 let pruned = self.manager.prune(min).await?;
473 if pruned {
474 self.unrecovered.retain(|section| *section >= min);
475 self.floors.retain(|section, _| *section >= min);
476 }
477 Ok(pruned)
478 }
479
480 const fn pruned(&self, section: u64) -> bool {
482 self.manager.pruned(section)
483 }
484
485 fn oldest_section(&self) -> Option<u64> {
487 self.manager.oldest_section()
488 }
489
490 fn newest_section(&self) -> Option<u64> {
492 self.manager.newest_section()
493 }
494
495 fn sections(&self) -> impl Iterator<Item = u64> + '_ {
497 self.manager.sections()
498 }
499
500 fn section_len(&self, section: u64) -> Result<u64, Error> {
502 let size = self.manager.size(section)?;
503 Ok(size / Self::CHUNK_SIZE_U64)
504 }
505
506 fn size(&self, section: u64) -> Result<u64, Error> {
508 self.manager.size(section)
509 }
510
511 async fn rewind(&mut self, section: u64, offset: u64) -> Result<(), Error> {
513 self.manager.rewind(section, offset).await?;
514 self.unrecovered.retain(|candidate| *candidate <= section);
515 self.floors.retain(|candidate, _| *candidate <= section);
516 if offset == 0 {
517 self.unrecovered.remove(§ion);
518 }
519 if let Some(floor) = self.floors.get_mut(§ion) {
520 *floor = (*floor).min(offset);
521 }
522 Ok(())
523 }
524
525 async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
527 self.manager.rewind_section(section, size).await?;
528 if size == 0 {
529 self.unrecovered.remove(§ion);
530 }
531 if let Some(floor) = self.floors.get_mut(§ion) {
532 *floor = (*floor).min(size);
533 }
534 Ok(())
535 }
536
537 async fn destroy(self) -> Result<(), Error> {
539 self.manager.destroy().await
540 }
541
542 async fn clear(&mut self) -> Result<(), Error> {
544 self.manager.clear().await?;
545 self.unrecovered.clear();
546 self.floors.clear();
547 Ok(())
548 }
549}
550
551pub struct Journal<E: Storage + Metrics, A: CodecFixed>(Box<Inner<E, A>>);
572
573impl<E: Storage + Metrics, A: CodecFixedShared> std::fmt::Debug for Journal<E, A> {
574 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575 f.debug_struct("Journal")
576 .field("oldest_section", &self.oldest_section())
577 .field("newest_section", &self.newest_section())
578 .finish_non_exhaustive()
579 }
580}
581
582impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
583 pub const CHUNK_SIZE: usize = Inner::<E, A>::CHUNK_SIZE;
585
586 pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
591 Ok(Self(Box::new(Inner::init(context, cfg, None).await?)))
592 }
593
594 pub(crate) async fn preflight_floors(
596 context: E,
597 cfg: Config,
598 minimum_items: &BTreeMap<u64, u64>,
599 ) -> Result<RecoveryPreflight<E, A>, Error> {
600 Inner::preflight_floors(context, cfg, minimum_items).await
601 }
602
603 pub(crate) async fn preflight_restore(
605 context: E,
606 cfg: Config,
607 section: u64,
608 size: u64,
609 ) -> Result<RecoveryPreflight<E, A>, Error> {
610 Inner::preflight_restore(context, cfg, section, size).await
611 }
612
613 pub async fn append(mut self, section: u64, item: &A) -> Result<(Self, u64), Error> {
622 let position = self.0.append(section, item).await?;
623 Ok((self, position))
624 }
625
626 pub async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
634 self.0.get(section, position).await
635 }
636
637 pub async fn get_many(
645 &self,
646 section: u64,
647 positions: &[u64],
648 buf: &mut [u8],
649 ) -> Result<(Vec<A>, usize), Error> {
650 self.0.get_many(section, positions, buf).await
651 }
652
653 pub fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
655 self.0.try_get_sync(section, position)
656 }
657
658 pub async fn last(&self, section: u64) -> Result<Option<A>, Error> {
667 self.0.last(section).await
668 }
669
670 pub async fn replay(
682 mut self,
683 start_section: u64,
684 start_position: u64,
685 buffer: NonZeroUsize,
686 read_options: ReadOptions,
687 ) -> Result<Replay<E, A>, Error> {
688 let mut sections = VecDeque::new();
689 for (§ion, blob) in self.0.manager.sections_from(start_section) {
690 let blob_size = blob.size();
691 let mut reader = blob.replay(buffer, read_options).await?;
692 let position = if section == start_section {
694 let start = start_position
695 .checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
696 .ok_or(Error::ItemOutOfRange(start_position))?;
697 if start > blob_size {
698 return Err(Error::ItemOutOfRange(start_position));
699 }
700 reader.seek_to(start)?;
701 start_position
702 } else {
703 0
704 };
705 sections.push_back(SectionReplay {
706 section,
707 reader,
708 position,
709 });
710 }
711 let finished = sections.is_empty();
712 Ok(Replay {
713 journal: self,
714 sections,
715 recovered_from: if start_position == 0 {
716 Some(start_section)
717 } else {
718 start_section.checked_add(1)
719 },
720 buffer,
721 read_options,
722 finished,
723 errored: false,
724 repairing: false,
725 })
726 }
727
728 pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
730 self.0.sync(sections).await?;
731 Ok(self)
732 }
733
734 pub async fn start_sync(
739 mut self,
740 sections: impl crate::Sections,
741 ) -> Result<(Self, Handle<()>), Error> {
742 let handle = self.0.start_sync(sections).await?;
743 Ok((self, handle))
744 }
745
746 pub async fn sync_all(mut self) -> Result<Self, Error> {
748 self.0.sync_all().await?;
749 Ok(self)
750 }
751
752 pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
754 let pruned = self.0.prune(min).await?;
755 Ok((self, pruned))
756 }
757
758 pub fn pruned(&self, section: u64) -> bool {
763 self.0.pruned(section)
764 }
765
766 pub fn oldest_section(&self) -> Option<u64> {
768 self.0.oldest_section()
769 }
770
771 pub fn newest_section(&self) -> Option<u64> {
773 self.0.newest_section()
774 }
775
776 pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
778 self.0.sections()
779 }
780
781 pub fn section_len(&self, section: u64) -> Result<u64, Error> {
783 self.0.section_len(section)
784 }
785
786 pub fn size(&self, section: u64) -> Result<u64, Error> {
788 self.0.size(section)
789 }
790
791 pub async fn rewind(mut self, section: u64, size: u64) -> Result<Self, Error> {
796 self.0.rewind(section, size).await?;
797 Ok(self)
798 }
799
800 pub async fn rewind_section(mut self, section: u64, size: u64) -> Result<Self, Error> {
804 self.0.rewind_section(section, size).await?;
805 Ok(self)
806 }
807
808 pub async fn destroy(self) -> Result<(), Error> {
810 self.0.destroy().await
811 }
812
813 pub async fn clear(mut self) -> Result<Self, Error> {
817 self.0.clear().await?;
818 Ok(self)
819 }
820}
821
822pub struct Replay<E: Storage + Metrics, A: CodecFixed> {
828 journal: Journal<E, A>,
829 sections: VecDeque<SectionReplay<E::Blob>>,
830 recovered_from: Option<u64>,
833 buffer: NonZeroUsize,
834 read_options: ReadOptions,
835 finished: bool,
836 errored: bool,
837 repairing: bool,
838}
839
840impl<E: Storage + Metrics, A: CodecFixedShared> Replay<E, A> {
841 async fn plan_repair(&mut self, source: RError) -> Result<(u64, u64), Error> {
844 if !matches!(source, RError::InvalidChecksum) {
847 return Err(source.into());
848 }
849
850 let current = self.sections.front().expect("replayed section is present");
852 let section = current.section;
853 let position = current.position;
854 let size = current.reader.blob_size();
855 let valid_size = position
856 .checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
857 .ok_or(Error::OffsetOverflow)?;
858
859 let recoverable = self
861 .journal
862 .0
863 .writer(section)
864 .recoverable_prefix_len(valid_size, self.buffer, self.read_options)
865 .await?;
866
867 if recoverable >= size {
870 return Err(source.into());
871 }
872
873 let target = recoverable - recoverable % Inner::<E, A>::CHUNK_SIZE_U64;
876 if target < valid_size {
877 return Err(Error::ItemOutOfRange(position));
878 }
879 self.ensure_above_floor(section, target)?;
880
881 Ok((valid_size, target))
882 }
883
884 async fn repair(&mut self, source: RError) -> Result<(), Error> {
886 let (valid_size, target) = match self.plan_repair(source).await {
888 Ok(plan) => plan,
889 Err(err) => {
890 self.sections.pop_front();
891 return Err(err);
892 }
893 };
894
895 let current = self.sections.front().expect("replayed section is present");
896 let (section, position) = (current.section, current.position);
897 warn!(
898 section,
899 invalid_size = current.reader.blob_size(),
900 new_size = target,
901 "torn page detected: truncating"
902 );
903
904 self.repairing = true;
906 let current = self
907 .sections
908 .pop_front()
909 .expect("repaired section is present");
910 drop(current.reader);
911 repair_blob(&mut self.journal, section, target).await?;
912 let mut reader = self
913 .journal
914 .0
915 .writer(section)
916 .replay(self.buffer, self.read_options)
917 .await?;
918 reader.seek_to(valid_size)?;
919 self.sections.push_front(SectionReplay {
920 section,
921 reader,
922 position,
923 });
924 self.repairing = false;
925 Ok(())
926 }
927
928 fn ensure_above_floor(&self, section: u64, target: u64) -> Result<(), Error> {
930 let floor = self.journal.0.floors.get(§ion).copied().unwrap_or(0);
931 if target < floor {
932 return Err(Error::Corruption(format!(
933 "section {section} recovery target {target} is below its {floor}-byte validation floor"
934 )));
935 }
936 Ok(())
937 }
938
939 pub async fn next(&mut self) -> Option<Result<(u64, u64, A), Error>> {
946 if self.repairing {
948 self.repairing = false;
949 self.sections.clear();
950 if !self.errored {
951 return self.fail(Error::ReplayInterrupted);
952 }
953 }
954 while let Some(current) = self.sections.front_mut() {
955 match current.reader.ensure(Inner::<E, A>::CHUNK_SIZE).await {
957 Ok(true) => {}
958 Ok(false) => {
959 let valid_size =
960 match current.position.checked_mul(Inner::<E, A>::CHUNK_SIZE_U64) {
961 Some(size) => size,
962 None => return self.fail(Error::OffsetOverflow),
963 };
964 let blob_size = current.reader.blob_size();
965 if valid_size < blob_size {
966 let section = current.section;
967 if let Err(err) = self.ensure_above_floor(section, valid_size) {
968 self.sections.pop_front();
969 return self.fail(err);
970 }
971 warn!(
972 section,
973 invalid_size = blob_size,
974 new_size = valid_size,
975 "incomplete item detected: truncating"
976 );
977 self.repairing = true;
978 if let Err(err) = repair_blob(&mut self.journal, section, valid_size).await
979 {
980 self.sections.pop_front();
981 return self.fail(err);
982 }
983 self.repairing = false;
984 }
985 self.sections.pop_front();
986 continue;
987 }
988 Err(err) => {
989 if let Err(err) = self.repair(err).await {
990 return self.fail(err);
991 }
992 continue;
993 }
994 }
995
996 match A::read(&mut current.reader) {
998 Ok(item) => {
999 let yielded = (current.section, current.position, item);
1000 current.position += 1;
1001 return Some(Ok(yielded));
1002 }
1003 Err(err) => {
1004 self.sections.pop_front();
1005 return self.fail(Error::Codec(err));
1006 }
1007 }
1008 }
1009 self.finished = true;
1010 None
1011 }
1012
1013 const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, A), Error>> {
1015 self.errored = true;
1016 Some(Err(err))
1017 }
1018
1019 pub fn finish(mut self) -> Result<Journal<E, A>, Error> {
1024 if self.errored || !self.finished {
1025 return Err(Error::ReplayFailed);
1026 }
1027 if let Some(start) = self.recovered_from {
1028 self.journal
1029 .0
1030 .unrecovered
1031 .retain(|section| *section < start);
1032 }
1033 Ok(self.journal)
1034 }
1035}
1036
1037async fn repair_blob<E: Storage + Metrics, A: CodecFixed>(
1039 journal: &mut Journal<E, A>,
1040 section: u64,
1041 size: u64,
1042) -> Result<(), Error> {
1043 let blob = journal.0.writer(section);
1044 blob.resize(size).await?;
1045 blob.sync().await?;
1046 Ok(())
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051 use super::*;
1052 use commonware_codec::FixedSize;
1053 use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
1054 use commonware_macros::test_traced;
1055 use commonware_runtime::{
1056 BufferPooler, Error as RError, Runner, Spawner as _, Supervisor as _,
1057 buffer::paged::{CacheRef, Writer, corrupt_page},
1058 deterministic,
1059 mocks::{
1060 DelayedSyncContext, PendingSyncs, RecordingContext, fail_pending_syncs,
1061 release_pending_syncs,
1062 },
1063 };
1064 use commonware_utils::{NZU16, NZUsize};
1065 use core::num::NonZeroU16;
1066 use std::{
1067 ops::RangeInclusive,
1068 sync::{
1069 Arc,
1070 atomic::{AtomicUsize, Ordering},
1071 },
1072 };
1073
1074 const PAGE_SIZE: NonZeroU16 = NZU16!(44);
1075 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
1076
1077 fn test_digest(value: u64) -> Digest {
1078 Sha256::hash(&[&value.to_be_bytes()])
1079 }
1080
1081 fn test_cfg(pooler: &impl BufferPooler) -> Config {
1082 Config {
1083 partition: "test-partition".into(),
1084 page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
1085 write_buffer: NZUsize!(2048),
1086 }
1087 }
1088
1089 fn aligned_cfg(pooler: &impl BufferPooler) -> Config {
1090 Config {
1091 partition: "segmented-fixed-aligned".into(),
1092 page_cache: CacheRef::from_pooler(pooler, NZU16!(16), NZUsize!(4)),
1093 write_buffer: NZUsize!(128),
1094 }
1095 }
1096
1097 fn lazy_recovery_cfg(pooler: &impl BufferPooler, partition: &str) -> Config {
1098 Config {
1099 partition: partition.into(),
1100 page_cache: CacheRef::from_pooler(pooler, NZU16!(16), NZUsize!(4)),
1101 write_buffer: NZUsize!(1),
1102 }
1103 }
1104
1105 async fn replay_all<E, A>(journal: Journal<E, A>) -> Journal<E, A>
1106 where
1107 E: Storage + Metrics,
1108 A: CodecFixedShared,
1109 {
1110 let mut replay = journal
1111 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1112 .await
1113 .expect("failed to start recovery replay");
1114 while let Some(item) = replay.next().await {
1115 item.expect("failed to recover journal");
1116 }
1117 replay.finish().expect("failed to finish recovery replay")
1118 }
1119
1120 async fn seed<E: Storage + Metrics>(context: &E, cfg: &Config, sections: RangeInclusive<u64>) {
1122 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1123 .await
1124 .expect("failed to init");
1125 for section in sections {
1126 for value in 0..16u64 {
1127 (journal, _) = journal
1128 .append(section, &value)
1129 .await
1130 .expect("failed to append");
1131 }
1132 }
1133 journal.sync_all().await.expect("failed to sync");
1134 }
1135
1136 #[test_traced]
1137 fn test_segmented_fixed_append_and_get() {
1138 let executor = deterministic::Runner::default();
1139 executor.start(|context| async move {
1140 let cfg = test_cfg(&context);
1141 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1142 .await
1143 .expect("failed to init");
1144
1145 let pos0;
1146 (journal, pos0) = journal
1147 .append(1, &test_digest(0))
1148 .await
1149 .expect("failed to append");
1150 assert_eq!(pos0, 0);
1151
1152 let pos1;
1153 (journal, pos1) = journal
1154 .append(1, &test_digest(1))
1155 .await
1156 .expect("failed to append");
1157 assert_eq!(pos1, 1);
1158
1159 let pos2;
1160 (journal, pos2) = journal
1161 .append(2, &test_digest(2))
1162 .await
1163 .expect("failed to append");
1164 assert_eq!(pos2, 0);
1165
1166 let item0 = journal.get(1, 0).await.expect("failed to get");
1167 assert_eq!(item0, test_digest(0));
1168
1169 let item1 = journal.get(1, 1).await.expect("failed to get");
1170 assert_eq!(item1, test_digest(1));
1171
1172 let item2 = journal.get(2, 0).await.expect("failed to get");
1173 assert_eq!(item2, test_digest(2));
1174
1175 let err = journal.get(1, 2).await;
1176 assert!(matches!(err, Err(Error::ItemOutOfRange(2))));
1177
1178 let err = journal.get(3, 0).await;
1179 assert!(matches!(err, Err(Error::SectionOutOfRange(3))));
1180
1181 journal.destroy().await.expect("failed to destroy");
1182 });
1183 }
1184
1185 #[test_traced]
1186 fn test_segmented_fixed_replay_empty_finishes_immediately() {
1187 let executor = deterministic::Runner::default();
1188 executor.start(|context| async move {
1189 let cfg = test_cfg(&context);
1190 let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
1191 .await
1192 .expect("failed to init");
1193
1194 let replay = journal
1196 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1197 .await
1198 .expect("failed to replay");
1199 let journal = replay.finish().expect("failed to finish replay");
1200 journal.destroy().await.expect("failed to destroy");
1201 });
1202 }
1203
1204 #[test_traced]
1205 fn test_segmented_fixed_replay_propagates_read_options() {
1206 let executor = deterministic::Runner::default();
1207 executor.start(|context| async move {
1208 let (context, recordings) = RecordingContext::new(context);
1209 let cfg = test_cfg(&context);
1210 let mut journal = Journal::init(context.child("storage"), cfg)
1211 .await
1212 .expect("failed to init");
1213
1214 for section in 1..=2 {
1215 (journal, _) = journal
1216 .append(section, &test_digest(section))
1217 .await
1218 .expect("failed to append");
1219 }
1220
1221 let mut replay = journal
1222 .replay(1, 0, NZUsize!(56), ReadOptions::DONT_CACHE)
1223 .await
1224 .expect("failed to replay");
1225 recordings.clear();
1226
1227 let (section, position, item) = replay
1229 .next()
1230 .await
1231 .expect("missing first replay item")
1232 .expect("failed to read first replay item");
1233 assert_eq!((section, position, item), (1, 0, test_digest(1)));
1234 let reads = recordings.snapshot().reads;
1235 assert!(!reads.is_empty());
1236 assert!(
1237 reads
1238 .iter()
1239 .all(|options| *options == ReadOptions::DONT_CACHE)
1240 );
1241
1242 recordings.clear();
1244 let (section, position, item) = replay
1245 .next()
1246 .await
1247 .expect("missing second replay item")
1248 .expect("failed to read second replay item");
1249 assert_eq!((section, position, item), (2, 0, test_digest(2)));
1250 let reads = recordings.snapshot().reads;
1251 assert!(!reads.is_empty());
1252 assert!(
1253 reads
1254 .iter()
1255 .all(|options| *options == ReadOptions::DONT_CACHE)
1256 );
1257 assert!(replay.next().await.is_none());
1258
1259 let journal = replay.finish().expect("failed to finish replay");
1260 journal.destroy().await.expect("failed to destroy");
1261 });
1262 }
1263
1264 #[test_traced]
1265 fn test_segmented_fixed_clean_recovery_uses_replay_buffer() {
1266 let executor = deterministic::Runner::default();
1267 executor.start(|context| async move {
1268 let (context, recordings) = RecordingContext::new(context);
1269 let cfg = lazy_recovery_cfg(&context, "segmented-fixed-lazy-recovery");
1270
1271 seed(&context, &cfg, 1..=1).await;
1274
1275 recordings.clear();
1278 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1279 .await
1280 .expect("failed to reopen");
1281 assert_eq!(recordings.snapshot().reads.len(), 1);
1282
1283 let mut replay = journal
1284 .replay(0, 0, NZUsize!(112), ReadOptions::default())
1285 .await
1286 .expect("failed to replay");
1287 while let Some(item) = replay.next().await {
1288 item.expect("failed to read replay item");
1289 }
1290 assert_eq!(recordings.snapshot().reads.len(), 3);
1291 replay
1292 .finish()
1293 .expect("failed to finish replay")
1294 .destroy()
1295 .await
1296 .expect("failed to destroy");
1297 });
1298 }
1299
1300 #[test_traced]
1301 #[should_panic(expected = "must be replayed before append")]
1302 fn test_segmented_fixed_append_requires_replay_after_reopen() {
1303 let executor = deterministic::Runner::default();
1304 executor.start(|context| async move {
1305 let cfg = test_cfg(&context);
1306 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1307 .await
1308 .expect("failed to init");
1309 (journal, _) = journal
1310 .append(1, &test_digest(0))
1311 .await
1312 .expect("failed to append");
1313 journal = journal.sync_all().await.expect("failed to sync");
1314 drop(journal);
1315
1316 let journal = Journal::init(context.child("reopen"), cfg)
1319 .await
1320 .expect("failed to reopen");
1321 journal.append(1, &test_digest(1)).await.unwrap();
1322 });
1323 }
1324
1325 #[test_traced]
1326 #[should_panic(expected = "must be replayed before append")]
1327 fn test_segmented_fixed_gates_older_section_after_reopen() {
1328 let executor = deterministic::Runner::default();
1329 executor.start(|context| async move {
1330 let cfg = test_cfg(&context);
1331 seed(&context, &cfg, 1..=3).await;
1332
1333 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1336 .await
1337 .expect("failed to reopen");
1338 journal.append(2, &2).await.unwrap();
1339 });
1340 }
1341
1342 #[test_traced]
1343 fn test_segmented_fixed_floor_preflight_reads_boundary_only() {
1344 let executor = deterministic::Runner::default();
1345 executor.start(|context| async move {
1346 let (context, recordings) = RecordingContext::new(context);
1347 let cfg = lazy_recovery_cfg(&context, "segmented-fixed-floor-boundary");
1348 seed(&context, &cfg, 1..=1).await;
1349
1350 recordings.clear();
1353 let floors = BTreeMap::from([(1, 16)]);
1354 let preflight =
1355 Journal::<_, u64>::preflight_floors(context.child("preflight"), cfg, &floors)
1356 .await
1357 .expect("failed to preflight");
1358 assert_eq!(recordings.snapshot().reads.len(), 1);
1359 preflight
1360 .finish()
1361 .await
1362 .expect("failed to finish preflight")
1363 .destroy()
1364 .await
1365 .expect("failed to destroy");
1366 });
1367 }
1368
1369 #[test_traced]
1370 fn test_segmented_fixed_restore_reads_boundaries_only() {
1371 let executor = deterministic::Runner::default();
1372 executor.start(|context| async move {
1373 let (context, recordings) = RecordingContext::new(context);
1374 let cfg = lazy_recovery_cfg(&context, "segmented-fixed-restore-boundaries");
1375 seed(&context, &cfg, 0..=1).await;
1376
1377 recordings.clear();
1380 let preflight = Journal::<_, u64>::preflight_restore(
1381 context.child("preflight"),
1382 cfg,
1383 1,
1384 16 * u64::SIZE as u64,
1385 )
1386 .await
1387 .expect("failed to preflight");
1388 assert_eq!(recordings.snapshot().reads.len(), 2);
1389 preflight
1390 .finish()
1391 .await
1392 .expect("failed to finish preflight")
1393 .destroy()
1394 .await
1395 .expect("failed to destroy");
1396 });
1397 }
1398
1399 #[test_traced]
1400 fn test_segmented_fixed_restore_retains_empty_checkpoint_section() {
1401 let executor = deterministic::Runner::default();
1402 executor.start(|context| async move {
1403 let cfg = lazy_recovery_cfg(&context, "segmented-fixed-empty-restore");
1404 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1405 .await
1406 .expect("failed to init");
1407 for section in 0..=2 {
1408 (journal, _) = journal
1409 .append(section, §ion)
1410 .await
1411 .expect("failed to append");
1412 }
1413 journal = journal.sync_all().await.expect("failed to sync");
1414 journal = journal
1415 .rewind_section(1, 0)
1416 .await
1417 .expect("failed to empty checkpoint section");
1418 journal = journal.sync(1).await.expect("failed to sync empty section");
1419 drop(journal);
1420
1421 let journal = Journal::<_, u64>::preflight_restore(context.child("restore"), cfg, 1, 0)
1422 .await
1423 .expect("failed to preflight")
1424 .finish()
1425 .await
1426 .expect("failed to finish preflight");
1427
1428 assert_eq!(journal.sections().collect::<Vec<_>>(), vec![0, 1]);
1429 assert_eq!(journal.size(1).expect("missing checkpoint section"), 0);
1430 journal.destroy().await.expect("failed to destroy");
1431 });
1432 }
1433
1434 #[test_traced]
1435 fn test_segmented_fixed_restore_floors_block_below_checkpoint_repair() {
1436 let executor = deterministic::Runner::default();
1437 executor.start(|context| async move {
1438 let cfg = lazy_recovery_cfg(&context, "segmented-fixed-restore-floors");
1439 seed(&context, &cfg, 0..=1).await;
1440
1441 corrupt_page(&context, &cfg.partition, &1u64.to_be_bytes(), 2, 16).await;
1444 let size = 16 * u64::SIZE as u64;
1445 let journal = Journal::<_, u64>::preflight_restore(
1446 context.child("restore"),
1447 cfg.clone(),
1448 1,
1449 size,
1450 )
1451 .await
1452 .expect("failed to preflight")
1453 .finish()
1454 .await
1455 .expect("failed to finish preflight");
1456
1457 let mut replay = journal
1460 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1461 .await
1462 .expect("failed to start replay");
1463 let mut outcome = None;
1464 while let Some(item) = replay.next().await {
1465 if let Err(err) = item {
1466 outcome = Some(err);
1467 break;
1468 }
1469 }
1470 assert!(matches!(
1471 outcome,
1472 Some(Error::Corruption(ref message))
1473 if message.contains("below its 128-byte validation floor")
1474 ));
1475 drop(replay);
1476
1477 let (_, blob_size) = context
1479 .open(&cfg.partition, &1u64.to_be_bytes())
1480 .await
1481 .expect("failed to open");
1482 assert_eq!(blob_size, 8 * 28);
1483 });
1484 }
1485
1486 #[test_traced]
1487 fn test_segmented_fixed_floor_preflight_reads_terminal_entry_only() {
1488 let executor = deterministic::Runner::default();
1489 executor.start(|context| async move {
1490 let (context, recordings) = RecordingContext::new(context);
1491 let cfg = aligned_cfg(&context);
1492 let mut journal = Journal::init(context.child("seed"), cfg.clone())
1493 .await
1494 .expect("failed to init");
1495 for value in 0..2 {
1496 (journal, _) = journal
1497 .append(1, &test_digest(value))
1498 .await
1499 .expect("failed to append");
1500 }
1501 journal = journal.sync(1).await.expect("failed to sync");
1502 drop(journal);
1503
1504 recordings.clear();
1505 let floors = BTreeMap::from([(1, 2)]);
1506 let journal =
1507 Journal::<_, Digest>::preflight_floors(context.child("reopen"), cfg, &floors)
1508 .await
1509 .expect("failed to preflight")
1510 .finish()
1511 .await
1512 .expect("failed to reopen");
1513
1514 assert_eq!(recordings.snapshot().reads.len(), 3);
1517 journal.destroy().await.expect("failed to destroy");
1518 });
1519 }
1520
1521 #[test_traced]
1522 fn test_segmented_fixed_replay_finish_before_drain_fails() {
1523 let executor = deterministic::Runner::default();
1524 executor.start(|context| async move {
1525 let cfg = test_cfg(&context);
1526 let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
1527 .await
1528 .expect("failed to init");
1529 (journal, _) = journal
1530 .append(1, &test_digest(0))
1531 .await
1532 .expect("failed to append");
1533 journal = journal.sync_all().await.expect("failed to sync");
1534
1535 let replay = journal
1536 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1537 .await
1538 .expect("failed to replay");
1539 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
1540 });
1541 }
1542
1543 #[test_traced]
1544 fn test_segmented_fixed_replay() {
1545 let executor = deterministic::Runner::default();
1546 executor.start(|context| async move {
1547 let cfg = test_cfg(&context);
1548 let mut journal = Journal::init(context.child("first"), cfg.clone())
1549 .await
1550 .expect("failed to init");
1551
1552 for i in 0u64..10 {
1553 (journal, _) = journal
1554 .append(1, &test_digest(i))
1555 .await
1556 .expect("failed to append");
1557 }
1558 for i in 10u64..20 {
1559 (journal, _) = journal
1560 .append(2, &test_digest(i))
1561 .await
1562 .expect("failed to append");
1563 }
1564
1565 journal = journal.sync_all().await.expect("failed to sync");
1566 drop(journal);
1567
1568 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1569 .await
1570 .expect("failed to re-init");
1571
1572 let items = {
1573 let mut replay = journal
1574 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1575 .await
1576 .expect("failed to replay");
1577
1578 let mut items = Vec::new();
1579 while let Some(result) = replay.next().await {
1580 match result {
1581 Ok((section, pos, item)) => items.push((section, pos, item)),
1582 Err(err) => panic!("replay error: {err}"),
1583 }
1584 }
1585 journal = replay.finish().expect("failed to finish replay");
1586 items
1587 };
1588
1589 assert_eq!(items.len(), 20);
1590 for (i, item) in items.iter().enumerate().take(10) {
1591 assert_eq!(item.0, 1);
1592 assert_eq!(item.1, i as u64);
1593 assert_eq!(item.2, test_digest(i as u64));
1594 }
1595 for (i, item) in items.iter().enumerate().skip(10).take(10) {
1596 assert_eq!(item.0, 2);
1597 assert_eq!(item.1, (i - 10) as u64);
1598 assert_eq!(item.2, test_digest(i as u64));
1599 }
1600
1601 journal.destroy().await.expect("failed to destroy");
1602 });
1603 }
1604
1605 #[test_traced]
1606 fn test_segmented_fixed_replay_with_start_offset() {
1607 let executor = deterministic::Runner::default();
1609 executor.start(|context| async move {
1610 let cfg = test_cfg(&context);
1611 let mut journal = Journal::init(context.child("first"), cfg.clone())
1612 .await
1613 .expect("failed to init");
1614
1615 for i in 0u64..10 {
1617 (journal, _) = journal
1618 .append(1, &test_digest(i))
1619 .await
1620 .expect("failed to append");
1621 }
1622 for i in 10u64..15 {
1624 (journal, _) = journal
1625 .append(2, &test_digest(i))
1626 .await
1627 .expect("failed to append");
1628 }
1629 journal = journal.sync_all().await.expect("failed to sync");
1630 drop(journal);
1631
1632 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1633 .await
1634 .expect("failed to re-init");
1635
1636 {
1638 let mut replay = journal
1639 .replay(1, 5, NZUsize!(1024), ReadOptions::default())
1640 .await
1641 .expect("failed to replay");
1642
1643 let mut items = Vec::new();
1644 while let Some(result) = replay.next().await {
1645 let (section, pos, item) = result.expect("replay error");
1646 items.push((section, pos, item));
1647 }
1648 journal = replay.finish().expect("failed to finish replay");
1649
1650 assert_eq!(
1651 items.len(),
1652 10,
1653 "Should have 5 items from section 1 + 5 from section 2"
1654 );
1655
1656 for (i, (section, pos, item)) in items.iter().enumerate().take(5) {
1658 assert_eq!(*section, 1);
1659 assert_eq!(*pos, (i + 5) as u64);
1660 assert_eq!(*item, test_digest((i + 5) as u64));
1661 }
1662
1663 for (i, (section, pos, item)) in items.iter().enumerate().skip(5) {
1665 assert_eq!(*section, 2);
1666 assert_eq!(*pos, (i - 5) as u64);
1667 assert_eq!(*item, test_digest((i + 5) as u64));
1668 }
1669 }
1670
1671 {
1673 let mut replay = journal
1674 .replay(1, 9, NZUsize!(1024), ReadOptions::default())
1675 .await
1676 .expect("failed to replay");
1677
1678 let mut items = Vec::new();
1679 while let Some(result) = replay.next().await {
1680 let (section, pos, item) = result.expect("replay error");
1681 items.push((section, pos, item));
1682 }
1683 journal = replay.finish().expect("failed to finish replay");
1684
1685 assert_eq!(
1686 items.len(),
1687 6,
1688 "Should have 1 item from section 1 + 5 from section 2"
1689 );
1690 assert_eq!(items[0], (1, 9, test_digest(9)));
1691 for (i, (section, pos, item)) in items.iter().enumerate().skip(1) {
1692 assert_eq!(*section, 2);
1693 assert_eq!(*pos, (i - 1) as u64);
1694 assert_eq!(*item, test_digest((i + 9) as u64));
1695 }
1696 }
1697
1698 {
1700 let mut replay = journal
1701 .replay(2, 3, NZUsize!(1024), ReadOptions::default())
1702 .await
1703 .expect("failed to replay");
1704
1705 let mut items = Vec::new();
1706 while let Some(result) = replay.next().await {
1707 let (section, pos, item) = result.expect("replay error");
1708 items.push((section, pos, item));
1709 }
1710 journal = replay.finish().expect("failed to finish replay");
1711
1712 assert_eq!(items.len(), 2, "Should have 2 items from section 2");
1713 assert_eq!(items[0], (2, 3, test_digest(13)));
1714 assert_eq!(items[1], (2, 4, test_digest(14)));
1715 }
1716
1717 let result = journal
1720 .replay(1, 100, NZUsize!(1024), ReadOptions::default())
1721 .await;
1722 assert!(matches!(result, Err(Error::ItemOutOfRange(100))));
1723
1724 let journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
1725 .await
1726 .expect("failed to re-init");
1727 let result = journal
1728 .replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
1729 .await;
1730 assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
1731
1732 let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
1733 .await
1734 .expect("failed to re-init");
1735 journal.destroy().await.expect("failed to destroy");
1736 });
1737 }
1738
1739 #[test_traced]
1740 fn test_segmented_fixed_prune() {
1741 let executor = deterministic::Runner::default();
1742 executor.start(|context| async move {
1743 let cfg = test_cfg(&context);
1744 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1745 .await
1746 .expect("failed to init");
1747
1748 for section in 1u64..=5 {
1749 (journal, _) = journal
1750 .append(section, &test_digest(section))
1751 .await
1752 .expect("failed to append");
1753 }
1754 journal = journal.sync_all().await.expect("failed to sync");
1755
1756 (journal, _) = journal.prune(3).await.expect("failed to prune");
1757
1758 let err = journal.get(1, 0).await;
1759 assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
1760
1761 let err = journal.get(2, 0).await;
1762 assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
1763
1764 let item = journal.get(3, 0).await.expect("should exist");
1765 assert_eq!(item, test_digest(3));
1766
1767 journal.destroy().await.expect("failed to destroy");
1768 });
1769 }
1770
1771 #[test_traced]
1772 fn test_segmented_fixed_pruned_after_full_prune() {
1773 let executor = deterministic::Runner::default();
1776 executor.start(|context| async move {
1777 let cfg = test_cfg(&context);
1778 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1779 .await
1780 .expect("failed to init");
1781
1782 for section in 1u64..=3 {
1783 (journal, _) = journal
1784 .append(section, &test_digest(section))
1785 .await
1786 .expect("failed to append");
1787 }
1788 journal = journal.sync_all().await.expect("failed to sync");
1789
1790 (journal, _) = journal.prune(10).await.expect("failed to prune");
1791 assert_eq!(journal.oldest_section(), None);
1792 assert!(journal.pruned(3));
1793 assert!(journal.pruned(9));
1794 assert!(!journal.pruned(10));
1795
1796 journal.destroy().await.expect("failed to destroy");
1797 });
1798 }
1799
1800 #[test_traced]
1801 fn test_segmented_fixed_rewind() {
1802 let executor = deterministic::Runner::default();
1803 executor.start(|context| async move {
1804 let cfg = test_cfg(&context);
1805 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1806 .await
1807 .expect("failed to init");
1808
1809 for section in 1u64..=3 {
1811 (journal, _) = journal
1812 .append(section, &test_digest(section))
1813 .await
1814 .expect("failed to append");
1815 }
1816 journal = journal.sync_all().await.expect("failed to sync");
1817
1818 for section in 1u64..=3 {
1820 let size = journal.size(section).expect("failed to get size");
1821 assert!(size > 0, "section {section} should have data");
1822 }
1823
1824 let size = journal.size(1).expect("failed to get size");
1826 journal = journal.rewind(1, size).await.expect("failed to rewind");
1827
1828 let size = journal.size(1).expect("failed to get size");
1830 assert!(size > 0, "section 1 should still have data");
1831
1832 for section in 2u64..=3 {
1834 let size = journal.size(section).expect("failed to get size");
1835 assert_eq!(size, 0, "section {section} should be removed");
1836 }
1837
1838 let item = journal.get(1, 0).await.expect("failed to get");
1840 assert_eq!(item, test_digest(1));
1841
1842 journal.destroy().await.expect("failed to destroy");
1843 });
1844 }
1845
1846 #[test_traced]
1847 fn test_segmented_fixed_rewind_max_section() {
1848 let executor = deterministic::Runner::default();
1849 executor.start(|context| async move {
1850 let cfg = test_cfg(&context);
1851 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1852 .await
1853 .expect("failed to init");
1854
1855 (journal, _) = journal
1857 .append(u64::MAX, &test_digest(0))
1858 .await
1859 .expect("failed to append");
1860 journal = journal.sync_all().await.expect("failed to sync");
1861
1862 let size = journal.size(u64::MAX).expect("failed to get size");
1864 journal = journal
1865 .rewind(u64::MAX, size)
1866 .await
1867 .expect("failed to rewind");
1868
1869 assert_eq!(journal.size(u64::MAX).expect("failed to get size"), size);
1871 assert_eq!(journal.get(u64::MAX, 0).await.unwrap(), test_digest(0));
1872
1873 journal.destroy().await.expect("failed to destroy");
1874 });
1875 }
1876
1877 #[test_traced]
1878 fn test_segmented_fixed_rewind_many_sections() {
1879 let executor = deterministic::Runner::default();
1880 executor.start(|context| async move {
1881 let cfg = test_cfg(&context);
1882 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1883 .await
1884 .expect("failed to init");
1885
1886 for section in 1u64..=10 {
1888 (journal, _) = journal
1889 .append(section, &test_digest(section))
1890 .await
1891 .expect("failed to append");
1892 }
1893 journal = journal.sync_all().await.expect("failed to sync");
1894
1895 let size = journal.size(5).expect("failed to get size");
1897 journal = journal.rewind(5, size).await.expect("failed to rewind");
1898
1899 for section in 1u64..=5 {
1901 let size = journal.size(section).expect("failed to get size");
1902 assert!(size > 0, "section {section} should still have data");
1903 }
1904
1905 for section in 6u64..=10 {
1907 let size = journal.size(section).expect("failed to get size");
1908 assert_eq!(size, 0, "section {section} should be removed");
1909 }
1910
1911 {
1913 let mut replay = journal
1914 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1915 .await
1916 .expect("failed to replay");
1917 let mut items = Vec::new();
1918 while let Some(result) = replay.next().await {
1919 let (section, _, item) = result.expect("failed to read");
1920 items.push((section, item));
1921 }
1922 journal = replay.finish().expect("failed to finish replay");
1923 assert_eq!(items.len(), 5);
1924 for (i, (section, item)) in items.iter().enumerate() {
1925 assert_eq!(*section, (i + 1) as u64);
1926 assert_eq!(*item, test_digest((i + 1) as u64));
1927 }
1928 }
1929
1930 journal.destroy().await.expect("failed to destroy");
1931 });
1932 }
1933
1934 #[test_traced]
1935 fn test_segmented_fixed_rewind_persistence() {
1936 let executor = deterministic::Runner::default();
1937 executor.start(|context| async move {
1938 let cfg = test_cfg(&context);
1939
1940 let mut journal = Journal::init(context.child("first"), cfg.clone())
1942 .await
1943 .expect("failed to init");
1944 for section in 1u64..=5 {
1945 (journal, _) = journal
1946 .append(section, &test_digest(section))
1947 .await
1948 .expect("failed to append");
1949 }
1950 journal = journal.sync_all().await.expect("failed to sync");
1951
1952 let size = journal.size(2).expect("failed to get size");
1954 journal = journal.rewind(2, size).await.expect("failed to rewind");
1955 journal = journal.sync_all().await.expect("failed to sync");
1956 drop(journal);
1957
1958 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1960 .await
1961 .expect("failed to re-init");
1962
1963 for section in 1u64..=2 {
1965 let size = journal.size(section).expect("failed to get size");
1966 assert!(size > 0, "section {section} should have data after restart");
1967 }
1968
1969 for section in 3u64..=5 {
1971 let size = journal.size(section).expect("failed to get size");
1972 assert_eq!(size, 0, "section {section} should be gone after restart");
1973 }
1974
1975 let item1 = journal.get(1, 0).await.expect("failed to get");
1977 assert_eq!(item1, test_digest(1));
1978 let item2 = journal.get(2, 0).await.expect("failed to get");
1979 assert_eq!(item2, test_digest(2));
1980
1981 journal.destroy().await.expect("failed to destroy");
1982 });
1983 }
1984
1985 #[test_traced]
1986 fn test_segmented_fixed_corruption_recovery() {
1987 let executor = deterministic::Runner::default();
1988 executor.start(|context| async move {
1989 let cfg = test_cfg(&context);
1990 let mut journal = Journal::init(context.child("first"), cfg.clone())
1991 .await
1992 .expect("failed to init");
1993
1994 for i in 0u64..5 {
1995 (journal, _) = journal
1996 .append(1, &test_digest(i))
1997 .await
1998 .expect("failed to append");
1999 }
2000 journal = journal.sync_all().await.expect("failed to sync");
2001 drop(journal);
2002
2003 let (blob, size) = context
2004 .open(&cfg.partition, &1u64.to_be_bytes())
2005 .await
2006 .expect("failed to open blob");
2007 blob.resize(size - 1).await.expect("failed to truncate");
2008 blob.sync().await.expect("failed to sync");
2009
2010 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2011 .await
2012 .expect("failed to re-init");
2013
2014 let count = {
2015 let mut replay = journal
2016 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2017 .await
2018 .expect("failed to replay");
2019
2020 let mut count = 0;
2021 while let Some(result) = replay.next().await {
2022 result.expect("should be ok");
2023 count += 1;
2024 }
2025 journal = replay.finish().expect("failed to finish replay");
2026 count
2027 };
2028 assert_eq!(count, 4);
2029
2030 journal.destroy().await.expect("failed to destroy");
2031 });
2032 }
2033
2034 #[test_traced]
2035 fn test_segmented_fixed_persistence() {
2036 let executor = deterministic::Runner::default();
2037 executor.start(|context| async move {
2038 let cfg = test_cfg(&context);
2039
2040 let mut journal = Journal::init(context.child("first"), cfg.clone())
2042 .await
2043 .expect("failed to init");
2044
2045 for i in 0u64..5 {
2046 (journal, _) = journal
2047 .append(1, &test_digest(i))
2048 .await
2049 .expect("failed to append");
2050 }
2051 journal = journal.sync_all().await.expect("failed to sync");
2052 drop(journal);
2053
2054 let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
2056 .await
2057 .expect("failed to re-init");
2058
2059 for i in 0u64..5 {
2060 let item = journal.get(1, i).await.expect("failed to get");
2061 assert_eq!(item, test_digest(i));
2062 }
2063
2064 journal.destroy().await.expect("failed to destroy");
2065 });
2066 }
2067
2068 #[test_traced]
2069 fn test_segmented_fixed_sync() {
2070 let executor = deterministic::Runner::default();
2071 executor.start(|context| async move {
2072 let cfg = test_cfg(&context);
2073 let mut journal = Journal::init(context.child("first"), cfg.clone())
2074 .await
2075 .expect("failed to init");
2076
2077 for section in 1u64..=3 {
2079 (journal, _) = journal
2080 .append(section, &test_digest(section))
2081 .await
2082 .expect("failed to append");
2083 }
2084
2085 journal
2087 .sync(&[1, 3, 99])
2088 .await
2089 .expect("failed to sync sections");
2090
2091 let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
2093 .await
2094 .expect("failed to re-init");
2095 assert_eq!(
2096 journal.get(1, 0).await.expect("section 1 durable"),
2097 test_digest(1)
2098 );
2099 assert_eq!(
2100 journal.get(3, 0).await.expect("section 3 durable"),
2101 test_digest(3)
2102 );
2103 assert!(matches!(
2104 journal.get(2, 0).await,
2105 Err(Error::ItemOutOfRange(0)) | Err(Error::SectionOutOfRange(2))
2106 ));
2107
2108 journal.destroy().await.expect("failed to destroy");
2109 });
2110 }
2111
2112 #[test_traced]
2113 fn test_segmented_fixed_section_len() {
2114 let executor = deterministic::Runner::default();
2115 executor.start(|context| async move {
2116 let cfg = test_cfg(&context);
2117 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2118 .await
2119 .expect("failed to init");
2120
2121 assert_eq!(journal.section_len(1).unwrap(), 0);
2122
2123 for i in 0u64..5 {
2124 (journal, _) = journal
2125 .append(1, &test_digest(i))
2126 .await
2127 .expect("failed to append");
2128 }
2129
2130 assert_eq!(journal.section_len(1).unwrap(), 5);
2131 assert_eq!(journal.section_len(2).unwrap(), 0);
2132
2133 journal.destroy().await.expect("failed to destroy");
2134 });
2135 }
2136
2137 #[test_traced]
2138 fn test_segmented_fixed_non_contiguous_sections() {
2139 let executor = deterministic::Runner::default();
2142 executor.start(|context| async move {
2143 let cfg = test_cfg(&context);
2144 let mut journal = Journal::init(context.child("first"), cfg.clone())
2145 .await
2146 .expect("failed to init");
2147
2148 (journal, _) = journal
2150 .append(1, &test_digest(100))
2151 .await
2152 .expect("failed to append");
2153 (journal, _) = journal
2154 .append(5, &test_digest(500))
2155 .await
2156 .expect("failed to append");
2157 (journal, _) = journal
2158 .append(10, &test_digest(1000))
2159 .await
2160 .expect("failed to append");
2161 journal = journal.sync_all().await.expect("failed to sync");
2162
2163 assert_eq!(journal.get(1, 0).await.unwrap(), test_digest(100));
2165 assert_eq!(journal.get(5, 0).await.unwrap(), test_digest(500));
2166 assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(1000));
2167
2168 for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
2170 let result = journal.get(missing_section, 0).await;
2171 assert!(
2172 matches!(result, Err(Error::SectionOutOfRange(_))),
2173 "Expected SectionOutOfRange for section {}, got {:?}",
2174 missing_section,
2175 result
2176 );
2177 }
2178
2179 drop(journal);
2181 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2182 .await
2183 .expect("failed to re-init");
2184
2185 {
2187 let mut replay = journal
2188 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2189 .await
2190 .expect("failed to replay");
2191
2192 let mut items = Vec::new();
2193 while let Some(result) = replay.next().await {
2194 let (section, _, item) = result.expect("replay error");
2195 items.push((section, item));
2196 }
2197 journal = replay.finish().expect("failed to finish replay");
2198
2199 assert_eq!(items.len(), 3, "Should have 3 items");
2200 assert_eq!(items[0], (1, test_digest(100)));
2201 assert_eq!(items[1], (5, test_digest(500)));
2202 assert_eq!(items[2], (10, test_digest(1000)));
2203 }
2204
2205 {
2207 let mut replay = journal
2208 .replay(5, 0, NZUsize!(1024), ReadOptions::default())
2209 .await
2210 .expect("failed to replay from section 5");
2211
2212 let mut items = Vec::new();
2213 while let Some(result) = replay.next().await {
2214 let (section, _, item) = result.expect("replay error");
2215 items.push((section, item));
2216 }
2217 journal = replay.finish().expect("failed to finish replay");
2218
2219 assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
2220 assert_eq!(items[0], (5, test_digest(500)));
2221 assert_eq!(items[1], (10, test_digest(1000)));
2222 }
2223
2224 journal.destroy().await.expect("failed to destroy");
2225 });
2226 }
2227
2228 #[test_traced]
2229 fn test_segmented_fixed_empty_section_in_middle() {
2230 let executor = deterministic::Runner::default();
2233 executor.start(|context| async move {
2234 let cfg = test_cfg(&context);
2235 let mut journal = Journal::init(context.child("first"), cfg.clone())
2236 .await
2237 .expect("failed to init");
2238
2239 (journal, _) = journal
2241 .append(1, &test_digest(100))
2242 .await
2243 .expect("failed to append");
2244
2245 (journal, _) = journal
2247 .append(2, &test_digest(200))
2248 .await
2249 .expect("failed to append");
2250 journal = journal.sync(2).await.expect("failed to sync");
2251 journal = journal
2252 .rewind_section(2, 0)
2253 .await
2254 .expect("failed to rewind");
2255
2256 (journal, _) = journal
2258 .append(3, &test_digest(300))
2259 .await
2260 .expect("failed to append");
2261
2262 journal = journal.sync_all().await.expect("failed to sync");
2263
2264 assert_eq!(journal.section_len(1).unwrap(), 1);
2266 assert_eq!(journal.section_len(2).unwrap(), 0);
2267 assert_eq!(journal.section_len(3).unwrap(), 1);
2268
2269 drop(journal);
2271 let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2272 .await
2273 .expect("failed to re-init");
2274
2275 {
2277 let mut replay = journal
2278 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2279 .await
2280 .expect("failed to replay");
2281
2282 let mut items = Vec::new();
2283 while let Some(result) = replay.next().await {
2284 let (section, _, item) = result.expect("replay error");
2285 items.push((section, item));
2286 }
2287 journal = replay.finish().expect("failed to finish replay");
2288
2289 assert_eq!(
2290 items.len(),
2291 2,
2292 "Should have 2 items (skipping empty section)"
2293 );
2294 assert_eq!(items[0], (1, test_digest(100)));
2295 assert_eq!(items[1], (3, test_digest(300)));
2296 }
2297
2298 {
2300 let mut replay = journal
2301 .replay(2, 0, NZUsize!(1024), ReadOptions::default())
2302 .await
2303 .expect("failed to replay from section 2");
2304
2305 let mut items = Vec::new();
2306 while let Some(result) = replay.next().await {
2307 let (section, _, item) = result.expect("replay error");
2308 items.push((section, item));
2309 }
2310 journal = replay.finish().expect("failed to finish replay");
2311
2312 assert_eq!(items.len(), 1, "Should have 1 item from section 3");
2313 assert_eq!(items[0], (3, test_digest(300)));
2314 }
2315
2316 journal.destroy().await.expect("failed to destroy");
2317 });
2318 }
2319
2320 #[test_traced]
2321 fn test_segmented_fixed_validates_pages_before_trailing_bytes() {
2322 let executor = deterministic::Runner::default();
2323 executor.start(|context| async move {
2324 const LOGICAL_PAGE_SIZE: u64 = 5;
2325 const SECTION: u64 = 0;
2326
2327 let cfg = Config {
2328 partition: "segmented-fixed-validate-before-tail-trim".into(),
2329 page_cache: CacheRef::from_pooler(
2330 &context,
2331 NZU16!(LOGICAL_PAGE_SIZE as u16),
2332 NZUsize!(4),
2333 ),
2334 write_buffer: NZUsize!(128),
2335 };
2336 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2337 .await
2338 .unwrap();
2339 for value in [11u64, 22, 33, 44] {
2340 (journal, _) = journal.append(SECTION, &value).await.unwrap();
2341 }
2342 journal = journal.sync_all().await.unwrap();
2343 drop(journal);
2344
2345 let (blob, size) = context
2346 .open(&cfg.partition, &SECTION.to_be_bytes())
2347 .await
2348 .unwrap();
2349 let mut writer = Writer::new(blob, size, 128, cfg.page_cache.clone())
2350 .await
2351 .unwrap();
2352 writer.resize(30).await.unwrap();
2353 writer.sync().await.unwrap();
2354 drop(writer);
2355
2356 corrupt_page(
2366 &context,
2367 &cfg.partition,
2368 &SECTION.to_be_bytes(),
2369 4,
2370 LOGICAL_PAGE_SIZE,
2371 )
2372 .await;
2373
2374 let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
2375 .await
2376 .unwrap();
2377 let journal = replay_all(journal).await;
2378 assert_eq!(journal.section_len(SECTION).unwrap(), 2);
2379 assert_eq!(journal.get(SECTION, 0).await.unwrap(), 11);
2380 assert_eq!(journal.get(SECTION, 1).await.unwrap(), 22);
2381 journal.destroy().await.unwrap();
2382 });
2383 }
2384
2385 #[test_traced]
2386 fn test_segmented_fixed_repairs_torn_interior_page() {
2387 const SECTION: u64 = 0;
2388
2389 let executor = deterministic::Runner::default();
2390 executor.start(|context| async move {
2391 let cfg = aligned_cfg(&context);
2392 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2393 .await
2394 .unwrap();
2395 for value in 0..6 {
2396 (journal, _) = journal.append(SECTION, &value).await.unwrap();
2397 }
2398 journal = journal.sync_all().await.unwrap();
2399 drop(journal);
2400
2401 corrupt_page(&context, &cfg.partition, &SECTION.to_be_bytes(), 1, 16).await;
2411
2412 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
2413 .await
2414 .unwrap();
2415 journal = replay_all(journal).await;
2416 assert_eq!(journal.section_len(SECTION).unwrap(), 2);
2417 assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
2418 assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
2419 assert!(matches!(
2420 journal.get(SECTION, 2).await,
2421 Err(Error::ItemOutOfRange(2))
2422 ));
2423
2424 let position;
2427 (journal, position) = journal.append(SECTION, &99).await.unwrap();
2428 assert_eq!(position, 2);
2429 journal.sync_all().await.unwrap();
2430
2431 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
2432 .await
2433 .unwrap();
2434 assert_eq!(journal.section_len(SECTION).unwrap(), 3);
2435 assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
2436 assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
2437 assert_eq!(journal.get(SECTION, 2).await.unwrap(), 99);
2438 journal.destroy().await.unwrap();
2439 });
2440 }
2441
2442 #[test_traced]
2443 fn test_segmented_fixed_truncation_recovery_across_page_boundary() {
2444 let executor = deterministic::Runner::default();
2456 executor.start(|context| async move {
2457 let cfg = test_cfg(&context);
2458 let mut journal = Journal::init(context.child("first"), cfg.clone())
2459 .await
2460 .expect("failed to init");
2461
2462 for i in 0u64..3 {
2464 (journal, _) = journal
2465 .append(1, &test_digest(i))
2466 .await
2467 .expect("failed to append");
2468 }
2469 journal = journal.sync_all().await.expect("failed to sync");
2470
2471 for i in 0u64..3 {
2473 let item = journal.get(1, i).await.expect("failed to get");
2474 assert_eq!(item, test_digest(i));
2475 }
2476 drop(journal);
2477
2478 let (blob, size) = context
2480 .open(&cfg.partition, &1u64.to_be_bytes())
2481 .await
2482 .expect("failed to open blob");
2483 blob.resize(size - 1).await.expect("failed to truncate");
2484 blob.sync().await.expect("failed to sync");
2485 drop(blob);
2486
2487 let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2490 .await
2491 .expect("failed to re-init");
2492 let journal = replay_all(journal).await;
2493
2494 assert_eq!(journal.section_len(1).unwrap(), 2);
2496
2497 assert_eq!(journal.size(1).unwrap(), 64);
2500
2501 let item0 = journal.get(1, 0).await.expect("failed to get item 0");
2503 assert_eq!(item0, test_digest(0));
2504 let item1 = journal.get(1, 1).await.expect("failed to get item 1");
2505 assert_eq!(item1, test_digest(1));
2506
2507 let err = journal.get(1, 2).await;
2509 assert!(
2510 matches!(err, Err(Error::ItemOutOfRange(2))),
2511 "expected ItemOutOfRange(2), got {:?}",
2512 err
2513 );
2514
2515 journal.destroy().await.expect("failed to destroy");
2516 });
2517 }
2518
2519 #[test_traced]
2520 fn test_journal_clear() {
2521 let executor = deterministic::Runner::default();
2522 executor.start(|context| async move {
2523 let cfg = Config {
2524 partition: "clear-test".into(),
2525 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2526 write_buffer: NZUsize!(1024),
2527 };
2528
2529 let mut journal: Journal<_, Digest> =
2530 Journal::init(context.child("journal"), cfg.clone())
2531 .await
2532 .expect("Failed to initialize journal");
2533
2534 for section in 0..5u64 {
2536 for i in 0..10u64 {
2537 (journal, _) = journal
2538 .append(section, &test_digest(section * 1000 + i))
2539 .await
2540 .expect("Failed to append");
2541 }
2542 journal = journal.sync(section).await.expect("Failed to sync");
2543 }
2544
2545 assert_eq!(journal.get(0, 0).await.unwrap(), test_digest(0));
2547 assert_eq!(journal.get(4, 0).await.unwrap(), test_digest(4000));
2548
2549 journal = journal.clear().await.expect("Failed to clear");
2551
2552 for section in 0..5u64 {
2554 assert!(matches!(
2555 journal.get(section, 0).await,
2556 Err(Error::SectionOutOfRange(s)) if s == section
2557 ));
2558 }
2559
2560 for i in 0..5u64 {
2562 (journal, _) = journal
2563 .append(10, &test_digest(i * 100))
2564 .await
2565 .expect("Failed to append after clear");
2566 }
2567 journal = journal.sync(10).await.expect("Failed to sync after clear");
2568
2569 assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(0));
2571
2572 assert!(matches!(
2574 journal.get(0, 0).await,
2575 Err(Error::SectionOutOfRange(0))
2576 ));
2577
2578 journal.destroy().await.unwrap();
2579 });
2580 }
2581
2582 #[test_traced]
2583 fn test_last_missing_section_returns_error() {
2584 let executor = deterministic::Runner::default();
2585 executor.start(|context| async move {
2586 let cfg = test_cfg(&context);
2587 let journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
2588 .await
2589 .expect("failed to init");
2590
2591 assert!(matches!(
2592 journal.last(0).await,
2593 Err(Error::SectionOutOfRange(0))
2594 ));
2595 assert!(matches!(
2596 journal.last(99).await,
2597 Err(Error::SectionOutOfRange(99))
2598 ));
2599
2600 journal.destroy().await.unwrap();
2601 });
2602 }
2603
2604 #[test_traced]
2605 fn test_last_after_rewind_to_zero() {
2606 let executor = deterministic::Runner::default();
2607 executor.start(|context| async move {
2608 let cfg = test_cfg(&context);
2609 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2610 .await
2611 .expect("failed to init");
2612
2613 (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
2614 (journal, _) = journal.append(0, &test_digest(1)).await.unwrap();
2615 journal = journal.sync(0).await.unwrap();
2616
2617 assert!(journal.last(0).await.unwrap().is_some());
2618
2619 journal = journal.rewind(0, 0).await.unwrap();
2620 assert_eq!(journal.last(0).await.unwrap(), None);
2621
2622 journal.destroy().await.unwrap();
2623 });
2624 }
2625
2626 #[test_traced]
2627 fn test_last_pruned_section_returns_error() {
2628 let executor = deterministic::Runner::default();
2629 executor.start(|context| async move {
2630 let cfg = test_cfg(&context);
2631 let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
2632 .await
2633 .expect("failed to init");
2634
2635 (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
2636 (journal, _) = journal.append(1, &test_digest(1)).await.unwrap();
2637 journal = journal.sync_all().await.unwrap();
2638
2639 (journal, _) = journal.prune(1).await.unwrap();
2640
2641 assert!(matches!(
2642 journal.last(0).await,
2643 Err(Error::AlreadyPrunedToSection(1))
2644 ));
2645 assert!(journal.last(1).await.unwrap().is_some());
2646
2647 journal.destroy().await.unwrap();
2648 });
2649 }
2650
2651 #[test_traced]
2652 fn test_get_many_empty() {
2653 let executor = deterministic::Runner::default();
2654 executor.start(|context| async move {
2655 let cfg = test_cfg(&context);
2656 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2657 (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
2658 assert_eq!(journal.section_len(0).unwrap(), 1);
2659
2660 let mut buf = [];
2661 let (items, hits) = journal.get_many(0, &[], &mut buf).await.unwrap();
2662 assert!(items.is_empty());
2663 assert_eq!(hits, 0);
2664
2665 journal.destroy().await.unwrap();
2666 });
2667 }
2668
2669 #[test_traced]
2670 fn test_get_many_single_section() {
2671 let executor = deterministic::Runner::default();
2672 executor.start(|context| async move {
2673 let cfg = test_cfg(&context);
2674 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2675
2676 for i in 0..5 {
2677 (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
2678 }
2679 assert_eq!(journal.section_len(0).unwrap(), 5);
2680
2681 let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
2684 let mut buf = vec![0u8; 6 * chunk];
2685 let (items, _) = journal
2686 .get_many(0, &[0, 1, 2, 3, 4], &mut buf)
2687 .await
2688 .unwrap();
2689
2690 for (i, item) in items.iter().enumerate() {
2691 assert_eq!(*item, test_digest(i as u64));
2692 }
2693
2694 journal.destroy().await.unwrap();
2695 });
2696 }
2697
2698 #[test_traced]
2699 fn test_get_many_subset() {
2700 let executor = deterministic::Runner::default();
2702 executor.start(|context| async move {
2703 let cfg = test_cfg(&context);
2704 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2705
2706 for i in 0..10 {
2707 (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
2708 }
2709 assert_eq!(journal.section_len(0).unwrap(), 10);
2710
2711 let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
2712 let positions = [1, 4, 7, 9];
2713 let mut buf = vec![0u8; positions.len() * chunk];
2714 let (items, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
2715
2716 for (i, &pos) in positions.iter().enumerate() {
2717 assert_eq!(items[i], test_digest(pos));
2718 }
2719
2720 journal.destroy().await.unwrap();
2721 });
2722 }
2723
2724 #[test_traced]
2725 fn test_get_many_bad_section() {
2726 let executor = deterministic::Runner::default();
2727 executor.start(|context| async move {
2728 let cfg = test_cfg(&context);
2729 let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
2730 .await
2731 .unwrap();
2732
2733 let mut buf = vec![0u8; 64];
2734 let err = journal.get_many(99, &[0], &mut buf).await.unwrap_err();
2735 assert!(matches!(err, Error::SectionOutOfRange(99)));
2736
2737 journal.destroy().await.unwrap();
2738 });
2739 }
2740
2741 #[test_traced]
2742 fn test_get_many_matches_get() {
2743 let executor = deterministic::Runner::default();
2745 executor.start(|context| async move {
2746 let cfg = test_cfg(&context);
2747 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2748
2749 for i in 0..8 {
2750 (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
2751 }
2752 assert_eq!(journal.section_len(0).unwrap(), 8);
2753 journal = journal.sync_all().await.unwrap();
2754
2755 let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
2756 let positions: Vec<u64> = (0..8).collect();
2757 let mut buf = vec![0u8; positions.len() * chunk];
2758 let (batch, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
2759
2760 for pos in &positions {
2761 let single = journal.get(0, *pos).await.unwrap();
2762 assert_eq!(batch[*pos as usize], single);
2763 }
2764
2765 journal.destroy().await.unwrap();
2766 });
2767 }
2768
2769 #[test_traced]
2770 fn test_segmented_fixed_prune_waits_for_in_flight_start_sync() {
2771 let executor = deterministic::Runner::default();
2772 executor.start(|context| async move {
2773 let pending = PendingSyncs::default();
2774 let context = DelayedSyncContext {
2775 inner: context,
2776 pending: pending.clone(),
2777 };
2778 let cfg = test_cfg(&context);
2779 let mut journal = Journal::init(context.child("storage"), cfg)
2780 .await
2781 .expect("failed to init");
2782
2783 (journal, _) = journal
2784 .append(1, &test_digest(0))
2785 .await
2786 .expect("failed to append");
2787 let handle;
2788 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
2789 assert!(!pending.lock().is_empty());
2790
2791 let started = Arc::new(AtomicUsize::new(0));
2792 let completed = Arc::new(AtomicUsize::new(0));
2793 let started_clone = started.clone();
2794 let completed_clone = completed.clone();
2795 let waiter = context.inner.child("prune").spawn(|_| async move {
2796 started_clone.fetch_add(1, Ordering::Relaxed);
2797 let (journal, pruned) = journal.prune(2).await.expect("failed to prune");
2798 assert!(pruned);
2799 completed_clone.fetch_add(1, Ordering::Relaxed);
2800 journal
2801 });
2802
2803 while started.load(Ordering::Relaxed) == 0 {
2804 commonware_runtime::reschedule().await;
2805 }
2806 commonware_runtime::reschedule().await;
2807 assert_eq!(
2808 completed.load(Ordering::Relaxed),
2809 0,
2810 "prune must wait for in-flight syncs on pruned sections"
2811 );
2812
2813 release_pending_syncs(&pending);
2814 handle
2815 .await
2816 .expect("sync handle should complete despite pruning");
2817 while completed.load(Ordering::Relaxed) == 0 {
2818 commonware_runtime::reschedule().await;
2819 }
2820 let journal = waiter.await.expect("prune task failed");
2821 assert_eq!(journal.oldest_section(), None);
2822 });
2823 }
2824
2825 #[test_traced]
2826 fn test_segmented_fixed_destroy_waits_for_in_flight_start_sync() {
2827 let executor = deterministic::Runner::default();
2828 executor.start(|context| async move {
2829 let pending = PendingSyncs::default();
2830 let context = DelayedSyncContext {
2831 inner: context,
2832 pending: pending.clone(),
2833 };
2834 let cfg = test_cfg(&context);
2835 let mut journal = Journal::init(context.child("storage"), cfg)
2836 .await
2837 .expect("failed to init");
2838
2839 (journal, _) = journal
2840 .append(1, &test_digest(0))
2841 .await
2842 .expect("failed to append");
2843 let handle;
2844 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
2845 assert!(!pending.lock().is_empty());
2846
2847 let started = Arc::new(AtomicUsize::new(0));
2848 let completed = Arc::new(AtomicUsize::new(0));
2849 let started_clone = started.clone();
2850 let completed_clone = completed.clone();
2851 let waiter = context.inner.child("destroy").spawn(|_| async move {
2852 started_clone.fetch_add(1, Ordering::Relaxed);
2853 journal.destroy().await.expect("failed to destroy");
2854 completed_clone.fetch_add(1, Ordering::Relaxed);
2855 });
2856
2857 while started.load(Ordering::Relaxed) == 0 {
2858 commonware_runtime::reschedule().await;
2859 }
2860 commonware_runtime::reschedule().await;
2861 assert_eq!(
2862 completed.load(Ordering::Relaxed),
2863 0,
2864 "destroy must wait for in-flight syncs"
2865 );
2866
2867 release_pending_syncs(&pending);
2868 handle
2869 .await
2870 .expect("sync handle should complete despite destruction");
2871 while completed.load(Ordering::Relaxed) == 0 {
2872 commonware_runtime::reschedule().await;
2873 }
2874 waiter.await.expect("destroy task failed");
2875 });
2876 }
2877
2878 #[test_traced]
2879 fn test_segmented_fixed_clear_waits_for_in_flight_start_sync() {
2880 let executor = deterministic::Runner::default();
2881 executor.start(|context| async move {
2882 let pending = PendingSyncs::default();
2883 let context = DelayedSyncContext {
2884 inner: context,
2885 pending: pending.clone(),
2886 };
2887 let cfg = test_cfg(&context);
2888 let mut journal = Journal::init(context.child("storage"), cfg)
2889 .await
2890 .expect("failed to init");
2891
2892 (journal, _) = journal
2893 .append(1, &test_digest(0))
2894 .await
2895 .expect("failed to append");
2896 let handle;
2897 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
2898 assert!(!pending.lock().is_empty());
2899
2900 let started = Arc::new(AtomicUsize::new(0));
2901 let completed = Arc::new(AtomicUsize::new(0));
2902 let started_clone = started.clone();
2903 let completed_clone = completed.clone();
2904 let waiter = context.inner.child("clear").spawn(|_| async move {
2905 started_clone.fetch_add(1, Ordering::Relaxed);
2906 journal = journal.clear().await.expect("failed to clear");
2907 completed_clone.fetch_add(1, Ordering::Relaxed);
2908 journal
2909 });
2910
2911 while started.load(Ordering::Relaxed) == 0 {
2912 commonware_runtime::reschedule().await;
2913 }
2914 commonware_runtime::reschedule().await;
2915 assert_eq!(
2916 completed.load(Ordering::Relaxed),
2917 0,
2918 "clear must wait for in-flight syncs"
2919 );
2920
2921 release_pending_syncs(&pending);
2922 handle
2923 .await
2924 .expect("sync handle should complete despite clearing");
2925 while completed.load(Ordering::Relaxed) == 0 {
2926 commonware_runtime::reschedule().await;
2927 }
2928 let mut journal = waiter.await.expect("clear task failed");
2929
2930 assert_eq!(journal.oldest_section(), None);
2932 let position;
2933 (journal, position) = journal
2934 .append(1, &test_digest(1))
2935 .await
2936 .expect("failed to append after clear");
2937 assert_eq!(position, 0);
2938 journal.destroy().await.expect("failed to destroy");
2939 });
2940 }
2941
2942 #[test_traced]
2943 fn test_segmented_fixed_rewind_waits_for_in_flight_start_sync() {
2944 let executor = deterministic::Runner::default();
2945 executor.start(|context| async move {
2946 let pending = PendingSyncs::default();
2947 let context = DelayedSyncContext {
2948 inner: context,
2949 pending: pending.clone(),
2950 };
2951 let cfg = test_cfg(&context);
2952 let mut journal = Journal::init(context.child("storage"), cfg)
2953 .await
2954 .expect("failed to init");
2955
2956 (journal, _) = journal
2957 .append(1, &test_digest(0))
2958 .await
2959 .expect("failed to append");
2960 (journal, _) = journal
2961 .append(2, &test_digest(1))
2962 .await
2963 .expect("failed to append");
2964 let handle;
2965 (journal, handle) = journal.start_sync(2).await.expect("failed to start sync");
2966 assert!(!pending.lock().is_empty());
2967
2968 let size = journal.size(1).expect("failed to get size");
2969 let started = Arc::new(AtomicUsize::new(0));
2970 let completed = Arc::new(AtomicUsize::new(0));
2971 let started_clone = started.clone();
2972 let completed_clone = completed.clone();
2973 let waiter = context.inner.child("rewind").spawn(move |_| async move {
2974 started_clone.fetch_add(1, Ordering::Relaxed);
2975 journal = journal.rewind(1, size).await.expect("failed to rewind");
2976 completed_clone.fetch_add(1, Ordering::Relaxed);
2977 journal
2978 });
2979
2980 while started.load(Ordering::Relaxed) == 0 {
2981 commonware_runtime::reschedule().await;
2982 }
2983 commonware_runtime::reschedule().await;
2984 assert_eq!(
2985 completed.load(Ordering::Relaxed),
2986 0,
2987 "rewind must wait for in-flight syncs on removed sections"
2988 );
2989
2990 release_pending_syncs(&pending);
2991 handle
2992 .await
2993 .expect("sync handle should complete despite rewind");
2994 while completed.load(Ordering::Relaxed) == 0 {
2995 commonware_runtime::reschedule().await;
2996 }
2997 let journal = waiter.await.expect("rewind task failed");
2998 assert_eq!(journal.size(2).expect("failed to get size"), 0);
2999 journal.destroy().await.expect("failed to destroy");
3000 });
3001 }
3002
3003 #[test_traced]
3004 fn test_segmented_fixed_prune_surfaces_failed_in_flight_start_sync() {
3005 let executor = deterministic::Runner::default();
3006 executor.start(|context| async move {
3007 let pending = PendingSyncs::default();
3008 let context = DelayedSyncContext {
3009 inner: context,
3010 pending: pending.clone(),
3011 };
3012 let cfg = test_cfg(&context);
3013 let mut journal = Journal::init(context.child("storage"), cfg)
3014 .await
3015 .expect("failed to init");
3016
3017 (journal, _) = journal
3018 .append(1, &test_digest(0))
3019 .await
3020 .expect("failed to append");
3021 let handle;
3022 (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
3023 fail_pending_syncs(&pending);
3024
3025 let err = journal
3026 .prune(2)
3027 .await
3028 .expect_err("prune must surface a failed in-flight sync");
3029 assert!(matches!(err, Error::Runtime(RError::Io(_))));
3030
3031 let err = handle.await.expect_err("sync handle should fail");
3032 assert!(matches!(err, RError::Io(_)));
3033 });
3034 }
3035}