1use super::manager::{AppendFactory, Config as ManagerConfig, Manager};
84use crate::journal::{
85 Error,
86 frame::{
87 FrameInfo, decode_item, decode_length_prefix, encode_frame_into, find_frame, read_frame_at,
88 },
89};
90use commonware_codec::{Codec, CodecShared, varint::MAX_U32_VARINT_SIZE};
91use commonware_runtime::{
92 Blob, Buf, Error as RError, Handle, IoBuf, Metrics, ReadOptions, Storage,
93 buffer::paged::{CacheRef, Replay as BlobReplay, Writer},
94};
95use std::{
96 collections::{BTreeSet, VecDeque},
97 io::Cursor,
98 num::NonZeroUsize,
99};
100use tracing::{trace, warn};
101
102#[derive(Clone)]
104pub struct Config<C> {
105 pub partition: String,
108
109 pub compression: Option<u8>,
111
112 pub codec_config: C,
114
115 pub page_cache: CacheRef,
117
118 pub write_buffer: NonZeroUsize,
120}
121
122struct SectionReplay<B: Blob> {
124 section: u64,
125 reader: BlobReplay<B>,
126 skip_bytes: u64,
127 offset: u64,
128 valid_offset: u64,
129 pending: Option<(usize, usize)>,
130}
131
132struct Inner<E: Storage + Metrics, V: Codec> {
134 manager: Manager<E, AppendFactory>,
135
136 unrecovered: BTreeSet<u64>,
138
139 compression: Option<u8>,
141
142 codec_config: V::Cfg,
144}
145
146impl<E: Storage + Metrics, V: Codec> Inner<E, V> {
147 fn writer(&mut self, section: u64) -> &mut Writer<E::Blob> {
150 self.manager
151 .get_mut(section)
152 .expect("replayed section is present")
153 }
154}
155
156impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
157 async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
159 let manager_cfg = ManagerConfig {
160 partition: cfg.partition,
161 factory: AppendFactory {
162 write_buffer: cfg.write_buffer,
163 page_cache_ref: cfg.page_cache,
164 },
165 };
166 let manager = Manager::init(context, manager_cfg).await?;
167 let mut unrecovered = BTreeSet::new();
168 for section in manager.sections() {
169 if manager.size(section)? != 0 {
170 unrecovered.insert(section);
171 }
172 }
173
174 Ok(Self {
175 manager,
176 unrecovered,
177 compression: cfg.compression,
178 codec_config: cfg.codec_config,
179 })
180 }
181
182 async fn read(
184 compressed: bool,
185 cfg: &V::Cfg,
186 blob: &Writer<E::Blob>,
187 offset: u64,
188 ) -> Result<(u64, u32, V), Error> {
189 read_frame_at(blob, offset, cfg, compressed).await
190 }
191
192 fn encode_item(compression: Option<u8>, item: &V) -> Result<(Vec<u8>, u32), Error> {
197 let mut buf = Vec::new();
198 let item_len = encode_frame_into(compression, item, &mut buf)?;
199 Ok((buf, item_len))
200 }
201
202 async fn append(&mut self, section: u64, item: &V) -> Result<(u64, u32), Error> {
204 let (buf, item_len) = Self::encode_item(self.compression, item)?;
205 self.append_raw(section, IoBuf::from(buf))
206 .await
207 .map(|offset| (offset, item_len))
208 }
209
210 async fn append_raw(&mut self, section: u64, buf: IoBuf) -> Result<u64, Error> {
215 assert!(
216 !self.unrecovered.contains(§ion),
217 "section {section} must be replayed before append"
218 );
219 let blob = self.manager.get_or_create(section).await?;
220 let offset = blob.append_owned(buf).await?;
221 trace!(blob = section, offset, "appended item");
222 Ok(offset)
223 }
224
225 async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
227 let blob = self
228 .manager
229 .get(section)?
230 .ok_or(Error::SectionOutOfRange(section))?;
231
232 let (_, _, item) =
234 Self::read(self.compression.is_some(), &self.codec_config, blob, offset).await?;
235 Ok(item)
236 }
237
238 async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
240 if offsets.is_empty() {
241 return Ok(Vec::new());
242 }
243 let blob = self
244 .manager
245 .get(section)?
246 .ok_or(Error::SectionOutOfRange(section))?;
247
248 let compressed = self.compression.is_some();
249 let cfg = &self.codec_config;
250 let mut items = Vec::with_capacity(offsets.len());
251 for &offset in offsets {
252 let (_, _, item) = Self::read(compressed, cfg, blob, offset).await?;
253 items.push(item);
254 }
255 Ok(items)
256 }
257
258 fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
260 let blob = self.manager.get(section).ok()??;
261 let remaining = blob.size().checked_sub(offset)?;
262 let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
263 if header_len == 0 {
264 return None;
265 }
266
267 let mut header = [0u8; MAX_U32_VARINT_SIZE];
269 if !blob.try_read_sync_into(&mut header[..header_len], offset) {
270 return None;
271 }
272 let mut cursor = Cursor::new(&header[..header_len]);
273 let (_, frame_info) = find_frame(&mut cursor, offset).ok()?;
274 let (varint_len, data_len) = match frame_info {
275 FrameInfo::Complete {
276 varint_len,
277 data_len,
278 } => (varint_len, data_len),
279 FrameInfo::Incomplete {
280 varint_len,
281 total_len,
282 ..
283 } => (varint_len, total_len),
284 };
285 let item_len = varint_len.checked_add(data_len)?;
286 if item_len > usize::try_from(remaining).ok()? {
287 return None;
288 }
289
290 let compressed = self.compression.is_some();
292 if item_len <= header_len {
293 return decode_item::<V>(
294 &header[varint_len..varint_len + data_len],
295 &self.codec_config,
296 compressed,
297 )
298 .ok();
299 }
300
301 let mut buf = vec![0u8; item_len];
303 if !blob.try_read_sync_into(&mut buf, offset) {
304 return None;
305 }
306 decode_item::<V>(
307 &buf[varint_len..varint_len + data_len],
308 &self.codec_config,
309 compressed,
310 )
311 .ok()
312 }
313
314 fn size(&self, section: u64) -> Result<u64, Error> {
316 self.manager.size(section)
317 }
318
319 async fn rewind(&mut self, section: u64, size: u64) -> Result<(), Error> {
321 self.manager.rewind(section, size).await?;
322 self.unrecovered.retain(|candidate| *candidate <= section);
323 if size == 0 {
324 self.unrecovered.remove(§ion);
325 }
326 Ok(())
327 }
328
329 async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
331 self.manager.rewind_section(section, size).await?;
332 if size == 0 {
333 self.unrecovered.remove(§ion);
334 }
335 Ok(())
336 }
337
338 async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
340 self.manager.sync(sections).await
341 }
342
343 async fn start_sync(&mut self, sections: impl crate::Sections) -> Result<Handle<()>, Error> {
345 self.manager.start_sync(sections).await
346 }
347
348 async fn sync_all(&mut self) -> Result<(), Error> {
350 self.manager.sync_all().await
351 }
352
353 async fn prune(&mut self, min: u64) -> Result<bool, Error> {
355 let pruned = self.manager.prune(min).await?;
356 if pruned {
357 self.unrecovered.retain(|section| *section >= min);
358 }
359 Ok(pruned)
360 }
361
362 const fn pruned(&self, section: u64) -> bool {
364 self.manager.pruned(section)
365 }
366
367 fn oldest_section(&self) -> Option<u64> {
369 self.manager.oldest_section()
370 }
371
372 fn newest_section(&self) -> Option<u64> {
374 self.manager.newest_section()
375 }
376
377 fn is_empty(&self) -> bool {
379 self.manager.is_empty()
380 }
381
382 fn num_sections(&self) -> usize {
384 self.manager.num_sections()
385 }
386
387 async fn destroy(self) -> Result<(), Error> {
389 self.manager.destroy().await
390 }
391
392 async fn clear(&mut self) -> Result<(), Error> {
394 self.manager.clear().await?;
395 self.unrecovered.clear();
396 Ok(())
397 }
398}
399
400pub struct Journal<E: Storage + Metrics, V: Codec>(Box<Inner<E, V>>);
422
423impl<E: Storage + Metrics, V: CodecShared> std::fmt::Debug for Journal<E, V> {
424 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425 f.debug_struct("Journal")
426 .field("oldest_section", &self.oldest_section())
427 .field("newest_section", &self.newest_section())
428 .finish_non_exhaustive()
429 }
430}
431
432impl<E: Storage + Metrics, V: CodecShared> Journal<E, V> {
433 pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
439 Ok(Self(Box::new(Inner::init(context, cfg).await?)))
440 }
441
442 pub async fn replay(
454 mut self,
455 start_section: u64,
456 start_offset: u64,
457 buffer: NonZeroUsize,
458 read_options: ReadOptions,
459 ) -> Result<Replay<E, V>, Error> {
460 let mut sections = VecDeque::new();
461 for (§ion, blob) in self.0.manager.sections_from(start_section) {
462 let reader = blob.replay(buffer, read_options).await?;
463 let skip_bytes = if section == start_section {
464 start_offset
465 } else {
466 0
467 };
468 sections.push_back(SectionReplay {
469 section,
470 reader,
471 skip_bytes,
472 offset: 0,
473 valid_offset: skip_bytes,
474 pending: None,
475 });
476 }
477 let finished = sections.is_empty();
478 let replay = Replay {
479 journal: self,
480 sections,
481 recovered_from: if start_offset == 0 {
482 Some(start_section)
483 } else {
484 start_section.checked_add(1)
485 },
486 buffer,
487 read_options,
488 finished,
489 errored: false,
490 repairing: false,
491 };
492
493 if let Some(current) = replay.sections.front()
497 && current.section == start_section
498 && start_offset > current.reader.blob_size()
499 {
500 return Err(Error::ItemOutOfRange(start_offset));
501 }
502 Ok(replay)
503 }
504
505 pub async fn append(mut self, section: u64, item: &V) -> Result<(Self, u64, u32), Error> {
514 let (offset, item_len) = self.0.append(section, item).await?;
515 Ok((self, offset, item_len))
516 }
517
518 pub async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
529 self.0.get(section, offset).await
530 }
531
532 pub async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
536 self.0.get_many(section, offsets).await
537 }
538
539 pub fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
541 self.0.try_get_sync(section, offset)
542 }
543
544 pub fn size(&self, section: u64) -> Result<u64, Error> {
548 self.0.size(section)
549 }
550
551 pub async fn rewind(mut self, section: u64, size: u64) -> Result<Self, Error> {
561 self.0.rewind(section, size).await?;
562 Ok(self)
563 }
564
565 pub async fn rewind_section(mut self, section: u64, size: u64) -> Result<Self, Error> {
573 self.0.rewind_section(section, size).await?;
574 Ok(self)
575 }
576
577 pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
582 self.0.sync(sections).await?;
583 Ok(self)
584 }
585
586 pub async fn start_sync(
591 mut self,
592 sections: impl crate::Sections,
593 ) -> Result<(Self, Handle<()>), Error> {
594 let handle = self.0.start_sync(sections).await?;
595 Ok((self, handle))
596 }
597
598 pub async fn sync_all(mut self) -> Result<Self, Error> {
600 self.0.sync_all().await?;
601 Ok(self)
602 }
603
604 pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
606 let pruned = self.0.prune(min).await?;
607 Ok((self, pruned))
608 }
609
610 pub fn pruned(&self, section: u64) -> bool {
615 self.0.pruned(section)
616 }
617
618 pub fn oldest_section(&self) -> Option<u64> {
620 self.0.oldest_section()
621 }
622
623 pub fn newest_section(&self) -> Option<u64> {
625 self.0.newest_section()
626 }
627
628 pub fn is_empty(&self) -> bool {
630 self.0.is_empty()
631 }
632
633 pub fn num_sections(&self) -> usize {
635 self.0.num_sections()
636 }
637
638 pub async fn destroy(self) -> Result<(), Error> {
640 self.0.destroy().await
641 }
642
643 pub async fn clear(mut self) -> Result<Self, Error> {
647 self.0.clear().await?;
648 Ok(self)
649 }
650}
651
652pub struct Replay<E: Storage + Metrics, V: Codec> {
659 journal: Journal<E, V>,
660 sections: VecDeque<SectionReplay<E::Blob>>,
661 recovered_from: Option<u64>,
664 buffer: NonZeroUsize,
665 read_options: ReadOptions,
666 finished: bool,
667 errored: bool,
668 repairing: bool,
669}
670
671impl<E: Storage + Metrics, V: CodecShared> Replay<E, V> {
672 async fn plan_repair(&mut self, source: RError) -> Result<u64, Error> {
675 if !matches!(source, RError::InvalidChecksum) {
678 return Err(source.into());
679 }
680
681 let current = self.sections.front().expect("replayed section is present");
683 let section = current.section;
684 let size = current.reader.blob_size();
685 let valid_offset = current.valid_offset;
686
687 let recoverable = self
689 .journal
690 .0
691 .writer(section)
692 .recoverable_prefix_len(valid_offset, self.buffer, self.read_options)
693 .await?;
694
695 if recoverable >= size {
698 return Err(source.into());
699 }
700
701 if recoverable < valid_offset {
704 return Err(Error::ItemOutOfRange(valid_offset));
705 }
706
707 Ok(recoverable)
708 }
709
710 async fn repair(&mut self, source: RError) -> Result<(), Error> {
712 let recoverable = match self.plan_repair(source).await {
714 Ok(target) => target,
715 Err(err) => {
716 self.sections.pop_front();
717 return Err(err);
718 }
719 };
720
721 let current = self.sections.front().expect("replayed section is present");
722 let (section, valid_offset) = (current.section, current.valid_offset);
723 warn!(
724 section,
725 invalid_size = current.reader.blob_size(),
726 new_size = recoverable,
727 "torn page detected: truncating"
728 );
729
730 self.repairing = true;
733 let current = self
734 .sections
735 .pop_front()
736 .expect("repaired section is present");
737 drop(current.reader);
738 repair_blob(&mut self.journal, section, recoverable).await?;
739 let mut reader = self
740 .journal
741 .0
742 .writer(section)
743 .replay(self.buffer, self.read_options)
744 .await?;
745 reader.seek_to(valid_offset)?;
746 self.sections.push_front(SectionReplay {
747 section,
748 reader,
749 skip_bytes: 0,
750 offset: valid_offset,
751 valid_offset,
752 pending: None,
753 });
754 self.repairing = false;
755 Ok(())
756 }
757
758 async fn repair_tail(&mut self, message: &'static str) -> Result<(), Error> {
760 let current = self.sections.front().expect("replayed section is present");
761 let (section, offset, valid_offset) =
762 (current.section, current.offset, current.valid_offset);
763 warn!(
764 blob = section,
765 bad_offset = offset,
766 new_size = valid_offset,
767 "{message}"
768 );
769
770 self.repairing = true;
774 repair_blob(&mut self.journal, section, valid_offset).await?;
775 self.repairing = false;
776 Ok(())
777 }
778
779 pub async fn next(&mut self) -> Option<Result<(u64, u64, u32, V), Error>> {
786 if self.repairing {
789 self.repairing = false;
790 self.sections.clear();
791 if !self.errored {
792 return self.fail(Error::ReplayInterrupted);
793 }
794 }
795 while let Some(current) = self.sections.front_mut() {
796 let blob_size = current.reader.blob_size();
797
798 let (item_size, varint_len) = match current.pending {
800 Some(header) => header,
801 None => {
802 match current.reader.ensure(MAX_U32_VARINT_SIZE).await {
806 Ok(true) => {}
807 Ok(false) => {
808 if current.reader.remaining() == 0 {
810 self.sections.pop_front();
811 continue;
812 }
813 }
815 Err(err) => {
816 if let Err(err) = self.repair(err).await {
817 return self.fail(err);
818 }
819 continue;
820 }
821 }
822
823 if current.skip_bytes > 0 {
825 let to_skip =
826 current.skip_bytes.min(current.reader.remaining() as u64) as usize;
827 current.reader.advance(to_skip);
828 current.skip_bytes -= to_skip as u64;
829 current.offset += to_skip as u64;
830 continue;
831 }
832
833 let before_remaining = current.reader.remaining();
835 match decode_length_prefix(&mut current.reader) {
836 Ok(header) => {
837 current.pending = Some(header);
840 header
841 }
842 Err(err) => {
843 if current.reader.is_exhausted()
845 || before_remaining < MAX_U32_VARINT_SIZE
846 {
847 if current.valid_offset < blob_size
849 && current.offset < blob_size
850 && let Err(err) = self
851 .repair_tail("trailing bytes detected: truncating")
852 .await
853 {
854 self.sections.pop_front();
855 return self.fail(err);
856 }
857 self.sections.pop_front();
858 continue;
859 }
860 self.sections.pop_front();
861 return self.fail(err);
862 }
863 }
864 }
865 };
866
867 match current.reader.ensure(item_size).await {
869 Ok(true) => {}
870 Ok(false) => {
871 if let Err(err) = self.repair_tail("incomplete item at end: truncating").await {
873 self.sections.pop_front();
874 return self.fail(err);
875 }
876 self.sections.pop_front();
877 continue;
878 }
879 Err(err) => {
880 if let Err(err) = self.repair(err).await {
881 return self.fail(err);
882 }
883 continue;
884 }
885 }
886
887 let item_offset = current.offset;
889 let next_offset = match current
890 .offset
891 .checked_add(varint_len as u64)
892 .and_then(|o| o.checked_add(item_size as u64))
893 {
894 Some(o) => o,
895 None => {
896 self.sections.pop_front();
897 return self.fail(Error::OffsetOverflow);
898 }
899 };
900 match decode_item::<V>(
901 (&mut current.reader).take(item_size),
902 &self.journal.0.codec_config,
903 self.journal.0.compression.is_some(),
904 ) {
905 Ok(decoded) => {
906 current.pending = None;
907 current.valid_offset = next_offset;
908 current.offset = next_offset;
909 return Some(Ok((
910 current.section,
911 item_offset,
912 item_size as u32,
913 decoded,
914 )));
915 }
916 Err(err) => {
917 self.sections.pop_front();
918 return self.fail(err);
919 }
920 }
921 }
922 self.finished = true;
923 None
924 }
925
926 const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, u32, V), Error>> {
928 self.errored = true;
929 Some(Err(err))
930 }
931
932 pub fn finish(mut self) -> Result<Journal<E, V>, Error> {
937 if self.errored || !self.finished {
938 return Err(Error::ReplayFailed);
939 }
940 if let Some(start) = self.recovered_from {
941 self.journal
942 .0
943 .unrecovered
944 .retain(|section| *section < start);
945 }
946 Ok(self.journal)
947 }
948}
949
950async fn repair_blob<E: Storage + Metrics, V: Codec>(
952 journal: &mut Journal<E, V>,
953 section: u64,
954 size: u64,
955) -> Result<(), Error> {
956 let blob = journal.0.writer(section);
957 blob.resize(size).await?;
958 blob.sync().await?;
959 Ok(())
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use commonware_codec::{EncodeSize, Write as _, varint::UInt};
966 use commonware_macros::test_traced;
967 use commonware_runtime::{
968 Blob, BufMut, Runner, Storage, Supervisor as _, WriteOptions,
969 buffer::paged::corrupt_page,
970 deterministic,
971 mocks::{DelayedSyncContext, PendingSyncs, RecordingContext, release_pending_syncs},
972 };
973 use commonware_utils::{NZU16, NZUsize, probability};
974 use std::num::NonZeroU16;
975
976 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
977 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
978
979 async fn journal_with_torn_interior_page(
980 context: &deterministic::Context,
981 partition: &str,
982 later_section: bool,
983 ) -> Journal<deterministic::Context, u64> {
984 const LOGICAL_PAGE_SIZE: u64 = 64;
985 const FIRST_SECTION: u64 = 0;
986 const TORN_SECTION: u64 = 1;
987
988 let cfg = Config {
989 partition: partition.into(),
990 compression: None,
991 codec_config: (),
992 page_cache: CacheRef::from_pooler(
993 context,
994 NZU16!(LOGICAL_PAGE_SIZE as u16),
995 NZUsize!(4),
996 ),
997 write_buffer: NZUsize!(256),
998 };
999 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1000 .await
1001 .unwrap();
1002 (journal, _, _) = journal.append(FIRST_SECTION, &u64::MAX).await.unwrap();
1003 for value in 0..15u64 {
1004 let offset;
1005 (journal, offset, _) = journal.append(TORN_SECTION, &value).await.unwrap();
1006 assert_eq!(offset, value * 9);
1007 }
1008 if later_section {
1009 (journal, _, _) = journal.append(2, &u64::MIN).await.unwrap();
1010 }
1011 journal = journal.sync_all().await.unwrap();
1012 drop(journal);
1013
1014 corrupt_page(
1015 context,
1016 &cfg.partition,
1017 &TORN_SECTION.to_be_bytes(),
1018 1,
1019 LOGICAL_PAGE_SIZE,
1020 )
1021 .await;
1022
1023 Journal::<_, u64>::init(context.child("recover"), cfg)
1024 .await
1025 .unwrap()
1026 }
1027
1028 #[test_traced]
1029 #[should_panic(expected = "must be replayed before append")]
1030 fn test_segmented_variable_rejects_append_before_replay() {
1031 let executor = deterministic::Runner::default();
1032 executor.start(|context| async move {
1033 const PARTITION: &str = "segmented-variable-append-before-replay";
1034 const NEW_SECTION: u64 = 2;
1035 const TORN_SECTION: u64 = 1;
1036
1037 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
1038 let (journal, offset, _) = journal.append(NEW_SECTION, &15).await.unwrap();
1039 assert_eq!(offset, 0);
1040 let (journal, offset, _) = journal.append(NEW_SECTION, &16).await.unwrap();
1041 assert_eq!(offset, 9);
1042 let _ = journal.append(TORN_SECTION, &15).await;
1043 });
1044 }
1045
1046 #[test_traced]
1047 #[should_panic(expected = "must be replayed before append")]
1048 fn test_segmented_variable_partial_replay_keeps_append_guard() {
1049 let executor = deterministic::Runner::default();
1050 executor.start(|context| async move {
1051 const PARTITION: &str = "segmented-variable-partial-replay-append";
1052 const SECTION: u64 = 1;
1053
1054 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
1055 let mut replay = journal
1056 .replay(SECTION, 9, NZUsize!(1024), ReadOptions::default())
1057 .await
1058 .unwrap();
1059 while let Some(item) = replay.next().await {
1060 item.unwrap();
1061 }
1062 let journal = replay.finish().unwrap();
1063 let _ = journal.append(SECTION, &7).await;
1064 });
1065 }
1066
1067 #[test_traced]
1068 #[should_panic(expected = "must be replayed before append")]
1069 fn test_segmented_variable_gates_clean_older_section() {
1070 let executor = deterministic::Runner::default();
1071 executor.start(|context| async move {
1072 const PARTITION: &str = "segmented-variable-gate-clean-older-section";
1073
1074 let journal = journal_with_torn_interior_page(&context, PARTITION, true).await;
1077 let _ = journal.append(0, &7).await;
1078 });
1079 }
1080
1081 #[test_traced]
1082 fn test_segmented_variable_replay_propagates_read_options() {
1083 let executor = deterministic::Runner::default();
1084 executor.start(|context| async move {
1085 let (context, recordings) = RecordingContext::new(context);
1086 let cfg = Config {
1087 partition: "test-partition".into(),
1088 compression: None,
1089 codec_config: (),
1090 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1091 write_buffer: NZUsize!(1024),
1092 };
1093 let mut journal = Journal::init(context.child("storage"), cfg)
1094 .await
1095 .expect("failed to init");
1096
1097 for section in 1..=2 {
1098 (journal, _, _) = journal
1099 .append(section, §ion)
1100 .await
1101 .expect("failed to append");
1102 }
1103
1104 let mut replay = journal
1105 .replay(1, 0, NZUsize!(1036), ReadOptions::DONT_CACHE)
1106 .await
1107 .expect("failed to replay");
1108 recordings.clear();
1109
1110 let (section, offset, _, item) = replay
1112 .next()
1113 .await
1114 .expect("missing first replay item")
1115 .expect("failed to read first replay item");
1116 assert_eq!((section, offset, item), (1, 0, 1));
1117 let reads = recordings.snapshot().reads;
1118 assert!(!reads.is_empty());
1119 assert!(
1120 reads
1121 .iter()
1122 .all(|options| *options == ReadOptions::DONT_CACHE)
1123 );
1124
1125 recordings.clear();
1127 let (section, offset, _, item) = replay
1128 .next()
1129 .await
1130 .expect("missing second replay item")
1131 .expect("failed to read second replay item");
1132 assert_eq!((section, offset, item), (2, 0, 2));
1133 let reads = recordings.snapshot().reads;
1134 assert!(!reads.is_empty());
1135 assert!(
1136 reads
1137 .iter()
1138 .all(|options| *options == ReadOptions::DONT_CACHE)
1139 );
1140 assert!(replay.next().await.is_none());
1141
1142 let journal = replay.finish().expect("failed to finish replay");
1143 journal.destroy().await.expect("failed to destroy");
1144 });
1145 }
1146
1147 #[test_traced]
1148 fn test_journal_append_and_read() {
1149 let executor = deterministic::Runner::default();
1151
1152 executor.start(|context| async move {
1154 let cfg = Config {
1156 partition: "test-partition".into(),
1157 compression: None,
1158 codec_config: (),
1159 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1160 write_buffer: NZUsize!(1024),
1161 };
1162 let index = 1u64;
1163 let data = 10;
1164 let mut journal = Journal::init(context.child("first"), cfg.clone())
1165 .await
1166 .expect("Failed to initialize journal");
1167
1168 (journal, _, _) = journal
1170 .append(index, &data)
1171 .await
1172 .expect("Failed to append data");
1173
1174 let buffer = context.encode();
1176 assert!(buffer.contains("first_tracked 1"));
1177
1178 journal = journal.sync(index).await.expect("Failed to sync journal");
1180 drop(journal);
1181 let journal = Journal::<_, i32>::init(context.child("second"), cfg)
1182 .await
1183 .expect("Failed to re-initialize journal");
1184
1185 let mut items = Vec::new();
1187 let mut replay = journal
1188 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1189 .await
1190 .expect("unable to setup replay");
1191 while let Some(result) = replay.next().await {
1192 match result {
1193 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1194 Err(err) => panic!("Failed to read item: {err}"),
1195 }
1196 }
1197
1198 assert_eq!(items.len(), 1);
1200 assert_eq!(items[0].0, index);
1201 assert_eq!(items[0].1, data);
1202
1203 let buffer = context.encode();
1205 assert!(buffer.contains("second_tracked 1"));
1206 });
1207 }
1208
1209 #[test_traced]
1210 fn test_journal_multiple_appends_and_reads() {
1211 let executor = deterministic::Runner::default();
1213
1214 executor.start(|context| async move {
1216 let cfg = Config {
1218 partition: "test-partition".into(),
1219 compression: None,
1220 codec_config: (),
1221 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1222 write_buffer: NZUsize!(1024),
1223 };
1224
1225 let mut journal = Journal::init(context.child("first"), cfg.clone())
1227 .await
1228 .expect("Failed to initialize journal");
1229
1230 let data_items = vec![(1u64, 1), (1u64, 2), (2u64, 3), (3u64, 4)];
1232 for (index, data) in &data_items {
1233 (journal, _, _) = journal
1234 .append(*index, data)
1235 .await
1236 .expect("Failed to append data");
1237 journal = journal.sync(*index).await.expect("Failed to sync blob");
1238 }
1239
1240 let buffer = context.encode();
1242 assert!(buffer.contains("first_tracked 3"));
1243 assert!(buffer.contains("first_synced_total 4"));
1244
1245 drop(journal);
1247 let mut journal = Journal::init(context.child("second"), cfg)
1248 .await
1249 .expect("Failed to re-initialize journal");
1250
1251 let mut items = Vec::<(u64, u32)>::new();
1253 {
1254 let mut replay = journal
1255 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1256 .await
1257 .expect("unable to setup replay");
1258 while let Some(result) = replay.next().await {
1259 match result {
1260 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1261 Err(err) => panic!("Failed to read item: {err}"),
1262 }
1263 }
1264 journal = replay.finish().expect("failed to finish replay");
1265 }
1266
1267 assert_eq!(items.len(), data_items.len());
1269 for ((expected_index, expected_data), (actual_index, actual_data)) in
1270 data_items.iter().zip(items.iter())
1271 {
1272 assert_eq!(actual_index, expected_index);
1273 assert_eq!(actual_data, expected_data);
1274 }
1275
1276 journal.destroy().await.expect("Failed to destroy journal");
1278 });
1279 }
1280
1281 #[test_traced]
1282 fn test_journal_prune_blobs() {
1283 let executor = deterministic::Runner::default();
1285
1286 executor.start(|context| async move {
1288 let cfg = Config {
1290 partition: "test-partition".into(),
1291 compression: None,
1292 codec_config: (),
1293 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1294 write_buffer: NZUsize!(1024),
1295 };
1296
1297 let mut journal = Journal::init(context.child("first"), cfg.clone())
1299 .await
1300 .expect("Failed to initialize journal");
1301
1302 for index in 1u64..=5u64 {
1304 (journal, _, _) = journal
1305 .append(index, &index)
1306 .await
1307 .expect("Failed to append data");
1308 journal = journal.sync(index).await.expect("Failed to sync blob");
1309 }
1310
1311 let data = 99;
1313 (journal, _, _) = journal
1314 .append(2u64, &data)
1315 .await
1316 .expect("Failed to append data");
1317 journal = journal.sync(2u64).await.expect("Failed to sync blob");
1318
1319 (journal, _) = journal.prune(3).await.expect("Failed to prune blobs");
1321
1322 let buffer = context.encode();
1324 assert!(buffer.contains("first_pruned_total 2"));
1325
1326 (journal, _) = journal.prune(2).await.expect("Failed to no-op prune");
1328 let buffer = context.encode();
1329 assert!(buffer.contains("first_pruned_total 2"));
1330
1331 drop(journal);
1333 let mut journal = Journal::init(context.child("second"), cfg.clone())
1334 .await
1335 .expect("Failed to re-initialize journal");
1336
1337 let mut items = Vec::<(u64, u64)>::new();
1339 {
1340 let mut replay = journal
1341 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1342 .await
1343 .expect("unable to setup replay");
1344 while let Some(result) = replay.next().await {
1345 match result {
1346 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1347 Err(err) => panic!("Failed to read item: {err}"),
1348 }
1349 }
1350 journal = replay.finish().expect("failed to finish replay");
1351 }
1352
1353 assert_eq!(items.len(), 3);
1355 let expected_indices = [3u64, 4u64, 5u64];
1356 for (item, expected_index) in items.iter().zip(expected_indices.iter()) {
1357 assert_eq!(item.0, *expected_index);
1358 }
1359
1360 (journal, _) = journal.prune(6).await.expect("Failed to prune blobs");
1362
1363 drop(journal);
1365
1366 assert!(
1371 context
1372 .scan(&cfg.partition)
1373 .await
1374 .expect("Failed to list blobs")
1375 .is_empty()
1376 );
1377 });
1378 }
1379
1380 #[test_traced]
1381 fn test_journal_prune_guard() {
1382 let executor = deterministic::Runner::default();
1383
1384 executor.start(|context| async move {
1385 let cfg = Config {
1386 partition: "test-partition".into(),
1387 compression: None,
1388 codec_config: (),
1389 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1390 write_buffer: NZUsize!(1024),
1391 };
1392
1393 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1394 .await
1395 .expect("Failed to initialize journal");
1396
1397 for section in 1u64..=5u64 {
1399 (journal, _, _) = journal
1400 .append(section, &(section as i32))
1401 .await
1402 .expect("Failed to append data");
1403 journal = journal.sync(section).await.expect("Failed to sync");
1404 }
1405
1406 (journal, _) = journal.prune(3).await.expect("Failed to prune");
1408
1409 assert!(journal.pruned(1));
1411 assert!(journal.pruned(2));
1412 assert!(!journal.pruned(3));
1413
1414 match journal.0.append(1, &100).await {
1418 Err(Error::AlreadyPrunedToSection(3)) => {}
1419 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1420 }
1421
1422 match journal.0.append(2, &100).await {
1423 Err(Error::AlreadyPrunedToSection(3)) => {}
1424 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1425 }
1426
1427 match journal.get(1, 0).await {
1429 Err(Error::AlreadyPrunedToSection(3)) => {}
1430 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1431 }
1432
1433 match journal.size(1) {
1435 Err(Error::AlreadyPrunedToSection(3)) => {}
1436 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1437 }
1438
1439 match journal.0.rewind(2, 0).await {
1441 Err(Error::AlreadyPrunedToSection(3)) => {}
1442 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1443 }
1444
1445 match journal.0.rewind_section(1, 0).await {
1447 Err(Error::AlreadyPrunedToSection(3)) => {}
1448 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1449 }
1450
1451 match journal.0.sync(2).await {
1453 Err(Error::AlreadyPrunedToSection(3)) => {}
1454 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1455 }
1456
1457 assert!(journal.get(3, 0).await.is_ok());
1459 assert!(journal.get(4, 0).await.is_ok());
1460 assert!(journal.get(5, 0).await.is_ok());
1461 assert!(journal.size(3).is_ok());
1462 assert!(journal.0.sync(4).await.is_ok());
1463
1464 (journal, _, _) = journal
1466 .append(3, &999)
1467 .await
1468 .expect("Should be able to append to section 3");
1469
1470 (journal, _) = journal.prune(5).await.expect("Failed to prune");
1472
1473 assert!(journal.pruned(4));
1475 assert!(!journal.pruned(5));
1476 match journal.get(3, 0).await {
1477 Err(Error::AlreadyPrunedToSection(5)) => {}
1478 other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1479 }
1480
1481 match journal.get(4, 0).await {
1482 Err(Error::AlreadyPrunedToSection(5)) => {}
1483 other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1484 }
1485
1486 assert!(journal.get(5, 0).await.is_ok());
1488 });
1489 }
1490
1491 #[test_traced]
1492 fn test_journal_prune_guard_across_restart() {
1493 let executor = deterministic::Runner::default();
1494
1495 executor.start(|context| async move {
1496 let cfg = Config {
1497 partition: "test-partition".into(),
1498 compression: None,
1499 codec_config: (),
1500 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1501 write_buffer: NZUsize!(1024),
1502 };
1503
1504 {
1506 let mut journal = Journal::init(context.child("first"), cfg.clone())
1507 .await
1508 .expect("Failed to initialize journal");
1509
1510 for section in 1u64..=5u64 {
1511 (journal, _, _) = journal
1512 .append(section, &(section as i32))
1513 .await
1514 .expect("Failed to append data");
1515 journal = journal.sync(section).await.expect("Failed to sync");
1516 }
1517
1518 journal.prune(3).await.expect("Failed to prune");
1519 }
1520
1521 {
1523 let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
1524 .await
1525 .expect("Failed to re-initialize journal");
1526
1527 assert!(!journal.pruned(1));
1529 assert!(!journal.pruned(2));
1530
1531 match journal.get(1, 0).await {
1534 Err(Error::SectionOutOfRange(1)) => {}
1535 other => panic!("Expected SectionOutOfRange(1), got {other:?}"),
1536 }
1537
1538 match journal.get(2, 0).await {
1539 Err(Error::SectionOutOfRange(2)) => {}
1540 other => panic!("Expected SectionOutOfRange(2), got {other:?}"),
1541 }
1542
1543 assert!(journal.get(3, 0).await.is_ok());
1545 assert!(journal.get(4, 0).await.is_ok());
1546 assert!(journal.get(5, 0).await.is_ok());
1547 }
1548 });
1549 }
1550
1551 #[test_traced]
1552 fn test_journal_with_invalid_blob_name() {
1553 let executor = deterministic::Runner::default();
1555
1556 executor.start(|context| async move {
1558 let cfg = Config {
1560 partition: "test-partition".into(),
1561 compression: None,
1562 codec_config: (),
1563 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1564 write_buffer: NZUsize!(1024),
1565 };
1566
1567 let invalid_blob_name = b"invalid"; let (blob, _) = context
1570 .open(&cfg.partition, invalid_blob_name)
1571 .await
1572 .expect("Failed to create blob with invalid name");
1573 blob.sync().await.expect("Failed to sync blob");
1574
1575 let result = Journal::<_, u64>::init(context, cfg).await;
1577
1578 assert!(matches!(result, Err(Error::InvalidBlobName(_))));
1580 });
1581 }
1582
1583 #[test_traced]
1584 fn test_journal_read_size_missing() {
1585 let executor = deterministic::Runner::default();
1587
1588 executor.start(|context| async move {
1590 let cfg = Config {
1592 partition: "test-partition".into(),
1593 compression: None,
1594 codec_config: (),
1595 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1596 write_buffer: NZUsize!(1024),
1597 };
1598
1599 let section = 1u64;
1601 let blob_name = section.to_be_bytes();
1602 let (blob, _) = context
1603 .open(&cfg.partition, &blob_name)
1604 .await
1605 .expect("Failed to create blob");
1606
1607 let mut incomplete_data = Vec::new();
1609 UInt(u32::MAX).write(&mut incomplete_data);
1610 incomplete_data.truncate(1);
1611 blob.write_at(0, incomplete_data, WriteOptions::SYNC)
1612 .await
1613 .expect("Failed to write incomplete data");
1614
1615 let journal = Journal::init(context, cfg)
1617 .await
1618 .expect("Failed to initialize journal");
1619
1620 let mut replay = journal
1622 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1623 .await
1624 .expect("unable to setup replay");
1625 let mut items = Vec::<(u64, u64)>::new();
1626 while let Some(result) = replay.next().await {
1627 match result {
1628 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1629 Err(err) => panic!("Failed to read item: {err}"),
1630 }
1631 }
1632 assert!(items.is_empty());
1633 });
1634 }
1635
1636 #[test_traced]
1637 fn test_journal_replay_empty_finishes_immediately() {
1638 let executor = deterministic::Runner::default();
1639 executor.start(|context| async move {
1640 let cfg = Config {
1641 partition: "test-partition".into(),
1642 compression: None,
1643 codec_config: (),
1644 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1645 write_buffer: NZUsize!(1024),
1646 };
1647 let journal = Journal::<_, i32>::init(context.child("storage"), cfg)
1648 .await
1649 .expect("Failed to initialize journal");
1650
1651 let replay = journal
1653 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1654 .await
1655 .expect("Failed to replay");
1656 let journal = replay.finish().expect("failed to finish replay");
1657 journal.destroy().await.expect("Failed to destroy");
1658 });
1659 }
1660
1661 #[test_traced]
1662 fn test_journal_replay_finish_before_drain_fails() {
1663 let executor = deterministic::Runner::default();
1664 executor.start(|context| async move {
1665 let cfg = Config {
1666 partition: "test-partition".into(),
1667 compression: None,
1668 codec_config: (),
1669 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1670 write_buffer: NZUsize!(1024),
1671 };
1672 let mut journal = Journal::init(context.child("storage"), cfg)
1673 .await
1674 .expect("Failed to initialize journal");
1675 (journal, _, _) = journal.append(1, &7i32).await.expect("Failed to append");
1676 journal = journal.sync(1).await.expect("Failed to sync");
1677
1678 let replay = journal
1679 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1680 .await
1681 .expect("Failed to replay");
1682 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
1683 });
1684 }
1685
1686 #[test_traced]
1687 fn test_journal_replay_reports_resize_error_on_trailing_bytes() {
1688 let executor = deterministic::Runner::default();
1689 executor.start(|context| async move {
1690 let cfg = Config {
1691 partition: "test-partition".into(),
1692 compression: None,
1693 codec_config: (),
1694 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1695 write_buffer: NZUsize!(1024),
1696 };
1697
1698 let section = 1u64;
1701 let item = [10u8; 1021];
1702 let item_record_size =
1703 UInt(item.encode_size() as u32).encode_size() + item.encode_size();
1704 assert_eq!(item_record_size, PAGE_SIZE.get() as usize - 1);
1705
1706 let mut journal = Journal::init(context.child("first"), cfg.clone())
1707 .await
1708 .expect("Failed to initialize journal");
1709 (journal, _, _) = journal
1710 .append(section, &item)
1711 .await
1712 .expect("Failed to append item");
1713 journal
1714 .0
1715 .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
1716 .await
1717 .expect("Failed to append trailing bytes");
1718 journal = journal.sync(section).await.expect("Failed to sync journal");
1719 drop(journal);
1720
1721 let journal = Journal::init(context.child("second"), cfg)
1722 .await
1723 .expect("Failed to re-initialize journal");
1724 *context.storage_fault_config().write() = deterministic::FaultConfig {
1725 resize_rate: Some(deterministic::ResizeConfig {
1726 failure_rate: probability!(1.0),
1727 partial_rate: probability!(0.0),
1728 }),
1729 ..Default::default()
1730 };
1731
1732 let mut replay = journal
1733 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1734 .await
1735 .expect("unable to setup replay");
1736
1737 let first = replay
1738 .next()
1739 .await
1740 .expect("expected item before trailing bytes")
1741 .expect("failed to replay valid item");
1742 assert_eq!(first, (section, 0, item.encode_size() as u32, item));
1743
1744 match replay.next().await {
1746 Some(Err(_)) => {}
1747 other => {
1748 panic!("expected resize error while repairing trailing bytes, got {other:?}")
1749 }
1750 }
1751 assert!(replay.next().await.is_none());
1752 });
1753 }
1754
1755 #[test_traced]
1756 fn test_journal_replay_finish_after_error_fails() {
1757 let executor = deterministic::Runner::default();
1758 executor.start(|context| async move {
1759 let cfg = Config {
1760 partition: "test-partition".into(),
1761 compression: None,
1762 codec_config: (),
1763 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1764 write_buffer: NZUsize!(1024),
1765 };
1766
1767 let section = 1u64;
1770 let item = [10u8; 1021];
1771 let mut journal = Journal::init(context.child("first"), cfg.clone())
1772 .await
1773 .expect("Failed to initialize journal");
1774 (journal, _, _) = journal
1775 .append(section, &item)
1776 .await
1777 .expect("Failed to append item");
1778 journal
1779 .0
1780 .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
1781 .await
1782 .expect("Failed to append trailing bytes");
1783 journal = journal.sync(section).await.expect("Failed to sync journal");
1784 drop(journal);
1785
1786 let journal = Journal::<_, [u8; 1021]>::init(context.child("second"), cfg)
1787 .await
1788 .expect("Failed to re-initialize journal");
1789 *context.storage_fault_config().write() = deterministic::FaultConfig {
1790 resize_rate: Some(deterministic::ResizeConfig {
1791 failure_rate: probability!(1.0),
1792 partial_rate: probability!(0.0),
1793 }),
1794 ..Default::default()
1795 };
1796
1797 let mut replay = journal
1798 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1799 .await
1800 .expect("unable to setup replay");
1801 let _ = replay
1802 .next()
1803 .await
1804 .expect("expected item before trailing bytes")
1805 .expect("failed to replay valid item");
1806 assert!(matches!(replay.next().await, Some(Err(_))));
1807 assert!(replay.next().await.is_none());
1808
1809 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
1811 });
1812 }
1813
1814 #[test_traced]
1815 fn test_journal_replay_dropped_during_repair_fails_replay() {
1816 let executor = deterministic::Runner::default();
1817 executor.start(|context| async move {
1818 let cfg = Config {
1819 partition: "test-partition".into(),
1820 compression: None,
1821 codec_config: (),
1822 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1823 write_buffer: NZUsize!(1024),
1824 };
1825
1826 let section = 1u64;
1829 let item = [10u8; 1021];
1830 let mut journal = Journal::init(context.child("first"), cfg.clone())
1831 .await
1832 .expect("Failed to initialize journal");
1833 (journal, _, _) = journal
1834 .append(section, &item)
1835 .await
1836 .expect("Failed to append item");
1837 journal
1838 .0
1839 .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
1840 .await
1841 .expect("Failed to append trailing bytes");
1842 journal = journal.sync(section).await.expect("Failed to sync journal");
1843 drop(journal);
1844
1845 let pending = PendingSyncs::default();
1847 let gated = DelayedSyncContext {
1848 inner: context.child("second"),
1849 pending: pending.clone(),
1850 };
1851 let journal = Journal::<_, [u8; 1021]>::init(gated, cfg.clone())
1852 .await
1853 .expect("Failed to re-initialize journal");
1854 let mut replay = journal
1855 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1856 .await
1857 .expect("unable to setup replay");
1858 let _ = replay
1859 .next()
1860 .await
1861 .expect("expected item before trailing bytes")
1862 .expect("failed to replay valid item");
1863 pending.arm();
1864 {
1865 let fut = replay.next();
1866 futures::pin_mut!(fut);
1867 assert!(
1868 futures::poll!(fut.as_mut()).is_pending(),
1869 "repair must suspend on the gated sync"
1870 );
1871 }
1872 release_pending_syncs(&pending);
1873
1874 assert!(matches!(
1876 replay.next().await,
1877 Some(Err(Error::ReplayInterrupted))
1878 ));
1879 assert!(replay.next().await.is_none());
1880 drop(replay);
1881
1882 let journal = Journal::<_, [u8; 1021]>::init(context.child("third"), cfg)
1884 .await
1885 .expect("Failed to re-initialize journal");
1886 let mut replay = journal
1887 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1888 .await
1889 .expect("unable to setup replay");
1890 let first = replay
1891 .next()
1892 .await
1893 .expect("expected item after recovery")
1894 .expect("failed to replay valid item");
1895 assert_eq!(first, (section, 0, item.encode_size() as u32, item));
1896 assert!(replay.next().await.is_none());
1897 let journal = replay.finish().expect("failed to finish replay");
1898 journal.destroy().await.expect("Failed to destroy");
1899 });
1900 }
1901
1902 #[test_traced]
1903 fn test_journal_read_item_missing() {
1904 let executor = deterministic::Runner::default();
1906
1907 executor.start(|context| async move {
1909 let cfg = Config {
1911 partition: "test-partition".into(),
1912 compression: None,
1913 codec_config: (),
1914 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1915 write_buffer: NZUsize!(1024),
1916 };
1917
1918 let section = 1u64;
1920 let blob_name = section.to_be_bytes();
1921 let (blob, _) = context
1922 .open(&cfg.partition, &blob_name)
1923 .await
1924 .expect("Failed to create blob");
1925
1926 let item_size: u32 = 10; let mut buf = Vec::new();
1929 UInt(item_size).write(&mut buf); let data = [2u8; 5];
1931 BufMut::put_slice(&mut buf, &data);
1932 blob.write_at(0, buf, WriteOptions::SYNC)
1933 .await
1934 .expect("Failed to write incomplete item");
1935
1936 let journal = Journal::init(context, cfg)
1938 .await
1939 .expect("Failed to initialize journal");
1940
1941 let mut replay = journal
1943 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1944 .await
1945 .expect("unable to setup replay");
1946 let mut items = Vec::<(u64, u64)>::new();
1947 while let Some(result) = replay.next().await {
1948 match result {
1949 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1950 Err(err) => panic!("Failed to read item: {err}"),
1951 }
1952 }
1953 assert!(items.is_empty());
1954 });
1955 }
1956
1957 #[test_traced]
1958 fn test_journal_read_checksum_missing() {
1959 let executor = deterministic::Runner::default();
1961
1962 executor.start(|context| async move {
1964 let cfg = Config {
1966 partition: "test-partition".into(),
1967 compression: None,
1968 codec_config: (),
1969 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1970 write_buffer: NZUsize!(1024),
1971 };
1972
1973 let section = 1u64;
1975 let blob_name = section.to_be_bytes();
1976 let (blob, _) = context
1977 .open(&cfg.partition, &blob_name)
1978 .await
1979 .expect("Failed to create blob");
1980
1981 let item_data = b"Test data";
1983 let item_size = item_data.len() as u32;
1984
1985 let mut buf = Vec::new();
1987 UInt(item_size).write(&mut buf);
1988 BufMut::put_slice(&mut buf, item_data);
1989 blob.write_at(0, buf, WriteOptions::SYNC)
1990 .await
1991 .expect("Failed to write item without checksum");
1992
1993 let journal = Journal::init(context, cfg)
1995 .await
1996 .expect("Failed to initialize journal");
1997
1998 let mut replay = journal
2002 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2003 .await
2004 .expect("unable to setup replay");
2005 let mut items = Vec::<(u64, u64)>::new();
2006 while let Some(result) = replay.next().await {
2007 match result {
2008 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2009 Err(err) => panic!("Failed to read item: {err}"),
2010 }
2011 }
2012 assert!(items.is_empty());
2013 });
2014 }
2015
2016 #[test_traced]
2017 fn test_journal_read_checksum_mismatch() {
2018 let executor = deterministic::Runner::default();
2020
2021 executor.start(|context| async move {
2023 let cfg = Config {
2025 partition: "test-partition".into(),
2026 compression: None,
2027 codec_config: (),
2028 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2029 write_buffer: NZUsize!(1024),
2030 };
2031
2032 let section = 1u64;
2034 let blob_name = section.to_be_bytes();
2035 let (blob, _) = context
2036 .open(&cfg.partition, &blob_name)
2037 .await
2038 .expect("Failed to create blob");
2039
2040 let item_data = b"Test data";
2042 let item_size = item_data.len() as u32;
2043 let incorrect_checksum: u32 = 0xDEADBEEF;
2044
2045 let mut buf = Vec::new();
2047 UInt(item_size).write(&mut buf);
2048 BufMut::put_slice(&mut buf, item_data);
2049 buf.put_u32(incorrect_checksum);
2050 blob.write_at(0, buf, WriteOptions::SYNC)
2051 .await
2052 .expect("Failed to write item with bad checksum");
2053
2054 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2056 .await
2057 .expect("Failed to initialize journal");
2058
2059 {
2061 let mut replay = journal
2062 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2063 .await
2064 .expect("unable to setup replay");
2065 let mut items = Vec::<(u64, u64)>::new();
2066 while let Some(result) = replay.next().await {
2067 match result {
2068 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2069 Err(err) => panic!("Failed to read item: {err}"),
2070 }
2071 }
2072 journal = replay.finish().expect("failed to finish replay");
2073 assert!(items.is_empty());
2074 }
2075 drop(journal);
2076
2077 let (_, blob_size) = context
2079 .open(&cfg.partition, §ion.to_be_bytes())
2080 .await
2081 .expect("Failed to open blob");
2082 assert_eq!(blob_size, 0);
2083 });
2084 }
2085
2086 #[test_traced]
2087 fn test_segmented_variable_replay_repairs_torn_interior_page_when_reached() {
2088 let executor = deterministic::Runner::default();
2089 executor.start(|context| async move {
2090 const FIRST_SECTION: u64 = 0;
2091 const PARTITION: &str = "segmented-variable-torn-interior";
2092 const TORN_SECTION: u64 = 1;
2093
2094 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
2103 let (_, original_size) = context
2104 .open(PARTITION, &TORN_SECTION.to_be_bytes())
2105 .await
2106 .unwrap();
2107 let mut replay = journal
2108 .replay(FIRST_SECTION, 0, NZUsize!(1024), ReadOptions::default())
2109 .await
2110 .unwrap();
2111
2112 let (_, size) = context
2113 .open(PARTITION, &TORN_SECTION.to_be_bytes())
2114 .await
2115 .unwrap();
2116 assert_eq!(
2117 size, original_size,
2118 "replay setup must not repair a later section"
2119 );
2120
2121 let (section, offset, _, value) = replay.next().await.unwrap().unwrap();
2122 assert_eq!((section, offset, value), (FIRST_SECTION, 0, u64::MAX));
2123 let (_, size) = context
2124 .open(PARTITION, &TORN_SECTION.to_be_bytes())
2125 .await
2126 .unwrap();
2127 assert_eq!(
2128 size, original_size,
2129 "consuming an earlier section must not repair a later section"
2130 );
2131
2132 let mut values = Vec::new();
2133 while let Some(result) = replay.next().await {
2134 let (section, offset, _, value) = result.unwrap();
2135 assert_eq!(section, TORN_SECTION);
2136 assert_eq!(offset, value * 9);
2137 values.push(value);
2138 }
2139 assert_eq!(values, (0..7).collect::<Vec<_>>());
2140
2141 let journal = replay.finish().unwrap();
2142 assert_eq!(journal.size(TORN_SECTION).unwrap(), 63);
2143 let (journal, offset, _) = journal.append(TORN_SECTION, &7).await.unwrap();
2144 assert_eq!(offset, 63);
2145 journal.destroy().await.unwrap();
2146 });
2147 }
2148
2149 #[test_traced]
2150 fn test_segmented_variable_replay_rejects_start_beyond_torn_prefix_without_repair() {
2151 let executor = deterministic::Runner::default();
2152 executor.start(|context| async move {
2153 const PARTITION: &str = "segmented-variable-torn-start";
2154 const SECTION: u64 = 1;
2155 const START_OFFSET: u64 = 72;
2156
2157 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
2158 let (_, original_size) = context
2159 .open(PARTITION, &SECTION.to_be_bytes())
2160 .await
2161 .unwrap();
2162 let mut replay = journal
2163 .replay(
2164 SECTION,
2165 START_OFFSET,
2166 NZUsize!(1024),
2167 ReadOptions::default(),
2168 )
2169 .await
2170 .expect("apparent tail still covers the requested start");
2171
2172 assert!(matches!(
2173 replay.next().await,
2174 Some(Err(Error::ItemOutOfRange(START_OFFSET)))
2175 ));
2176 let (_, size) = context
2177 .open(PARTITION, &SECTION.to_be_bytes())
2178 .await
2179 .unwrap();
2180 assert_eq!(
2181 size, original_size,
2182 "an unvalidated start offset must not become a repair boundary"
2183 );
2184 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2185 });
2186 }
2187
2188 #[test_traced]
2189 fn test_segmented_variable_replay_stops_after_failed_interior_repair() {
2190 let executor = deterministic::Runner::default();
2191 executor.start(|context| async move {
2192 let journal = journal_with_torn_interior_page(
2193 &context,
2194 "segmented-variable-failed-interior-repair",
2195 true,
2196 )
2197 .await;
2198 *context.storage_fault_config().write() = deterministic::FaultConfig {
2199 resize_rate: Some(deterministic::ResizeConfig {
2200 failure_rate: probability!(1.0),
2201 partial_rate: probability!(0.0),
2202 }),
2203 ..Default::default()
2204 };
2205
2206 let mut replay = journal
2209 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2210 .await
2211 .unwrap();
2212 assert!(matches!(replay.next().await, Some(Ok((0, 0, _, u64::MAX)))));
2213 assert!(matches!(replay.next().await, Some(Err(Error::Runtime(_)))));
2214 assert!(replay.next().await.is_none());
2215 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2216 });
2217 }
2218
2219 #[test_traced]
2220 fn test_segmented_variable_replay_stops_after_failed_tail_repair() {
2221 let executor = deterministic::Runner::default();
2222 executor.start(|context| async move {
2223 let journal = journal_with_torn_interior_page(
2224 &context,
2225 "segmented-variable-failed-tail-repair",
2226 true,
2227 )
2228 .await;
2229 let mut replay = journal
2230 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2231 .await
2232 .unwrap();
2233
2234 assert!(matches!(replay.next().await, Some(Ok((0, 0, _, u64::MAX)))));
2235 for expected in 0..7 {
2236 let (section, offset, _, value) = replay.next().await.unwrap().unwrap();
2237 assert_eq!((section, offset, value), (1, expected * 9, expected));
2238 }
2239
2240 *context.storage_fault_config().write() = deterministic::FaultConfig {
2241 write_rate: Some(deterministic::WriteConfig {
2242 failure_rate: probability!(1.0),
2243 retention_rate: probability!(1.0),
2244 mode: deterministic::PartialWriteMode::Prefix,
2245 }),
2246 ..Default::default()
2247 };
2248 assert!(matches!(replay.next().await, Some(Err(Error::Runtime(_)))));
2249 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
2250
2251 assert!(replay.next().await.is_none());
2252 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2253 });
2254 }
2255
2256 #[test_traced]
2257 fn test_journal_truncation_recovery() {
2258 let executor = deterministic::Runner::default();
2260
2261 executor.start(|context| async move {
2263 let cfg = Config {
2265 partition: "test-partition".into(),
2266 compression: None,
2267 codec_config: (),
2268 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2269 write_buffer: NZUsize!(1024),
2270 };
2271
2272 let mut journal = Journal::init(context.child("first"), cfg.clone())
2274 .await
2275 .expect("Failed to initialize journal");
2276
2277 (journal, _, _) = journal.append(1, &1).await.expect("Failed to append data");
2279
2280 let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
2282 for (index, data) in &data_items {
2283 (journal, _, _) = journal
2284 .append(*index, data)
2285 .await
2286 .expect("Failed to append data");
2287 journal = journal.sync(*index).await.expect("Failed to sync blob");
2288 }
2289
2290 journal = journal.sync_all().await.expect("Failed to sync");
2292 drop(journal);
2293
2294 let (blob, blob_size) = context
2296 .open(&cfg.partition, &2u64.to_be_bytes())
2297 .await
2298 .expect("Failed to open blob");
2299 blob.resize(blob_size - 4)
2300 .await
2301 .expect("Failed to corrupt blob");
2302 blob.sync().await.expect("Failed to sync blob");
2303
2304 let mut journal = Journal::init(context.child("second"), cfg.clone())
2306 .await
2307 .expect("Failed to re-initialize journal");
2308
2309 let mut items = Vec::<(u64, u32)>::new();
2311 {
2312 let mut replay = journal
2313 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2314 .await
2315 .expect("unable to setup replay");
2316 while let Some(result) = replay.next().await {
2317 match result {
2318 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2319 Err(err) => panic!("Failed to read item: {err}"),
2320 }
2321 }
2322 journal = replay.finish().expect("failed to finish replay");
2323 }
2324 drop(journal);
2325
2326 assert_eq!(items.len(), 1);
2328 assert_eq!(items[0].0, 1);
2329 assert_eq!(items[0].1, 1);
2330
2331 let (_, blob_size) = context
2333 .open(&cfg.partition, &2u64.to_be_bytes())
2334 .await
2335 .expect("Failed to open blob");
2336 assert_eq!(blob_size, 0);
2337
2338 let mut journal = Journal::init(context.child("third"), cfg.clone())
2340 .await
2341 .expect("Failed to re-initialize journal");
2342
2343 let mut items = Vec::<(u64, u32)>::new();
2345 {
2346 let mut replay = journal
2347 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2348 .await
2349 .expect("unable to setup replay");
2350 while let Some(result) = replay.next().await {
2351 match result {
2352 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2353 Err(err) => panic!("Failed to read item: {err}"),
2354 }
2355 }
2356 journal = replay.finish().expect("failed to finish replay");
2357 }
2358
2359 assert_eq!(items.len(), 1);
2361 assert_eq!(items[0].0, 1);
2362 assert_eq!(items[0].1, 1);
2363
2364 (journal, _, _) = journal.append(2, &5).await.expect("Failed to append data");
2366 journal = journal.sync(2).await.expect("Failed to sync blob");
2367
2368 let item = journal.get(2, 0).await.expect("Failed to get item");
2370 assert_eq!(item, 5);
2371
2372 drop(journal);
2374
2375 let journal = Journal::init(context.child("storage"), cfg.clone())
2377 .await
2378 .expect("Failed to re-initialize journal");
2379
2380 let mut items = Vec::<(u64, u32)>::new();
2382 {
2383 let mut replay = journal
2384 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2385 .await
2386 .expect("unable to setup replay");
2387 while let Some(result) = replay.next().await {
2388 match result {
2389 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2390 Err(err) => panic!("Failed to read item: {err}"),
2391 }
2392 }
2393 }
2394
2395 assert_eq!(items.len(), 2);
2397 assert_eq!(items[0].0, 1);
2398 assert_eq!(items[0].1, 1);
2399 assert_eq!(items[1].0, 2);
2400 assert_eq!(items[1].1, 5);
2401 });
2402 }
2403
2404 #[test_traced]
2405 fn test_journal_handling_extra_data() {
2406 let executor = deterministic::Runner::default();
2408
2409 executor.start(|context| async move {
2411 let cfg = Config {
2413 partition: "test-partition".into(),
2414 compression: None,
2415 codec_config: (),
2416 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2417 write_buffer: NZUsize!(1024),
2418 };
2419
2420 let mut journal = Journal::init(context.child("first"), cfg.clone())
2422 .await
2423 .expect("Failed to initialize journal");
2424
2425 (journal, _, _) = journal.append(1, &1).await.expect("Failed to append data");
2427
2428 let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
2430 for (index, data) in &data_items {
2431 (journal, _, _) = journal
2432 .append(*index, data)
2433 .await
2434 .expect("Failed to append data");
2435 journal = journal.sync(*index).await.expect("Failed to sync blob");
2436 }
2437
2438 journal = journal.sync_all().await.expect("Failed to sync");
2440 drop(journal);
2441
2442 let (blob, blob_size) = context
2444 .open(&cfg.partition, &2u64.to_be_bytes())
2445 .await
2446 .expect("Failed to open blob");
2447 blob.write_at(blob_size, vec![0u8; 16], WriteOptions::SYNC)
2448 .await
2449 .expect("Failed to add extra data");
2450
2451 let journal = Journal::init(context.child("second"), cfg)
2453 .await
2454 .expect("Failed to re-initialize journal");
2455
2456 let mut items = Vec::<(u64, i32)>::new();
2458 let mut replay = journal
2459 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2460 .await
2461 .expect("unable to setup replay");
2462 while let Some(result) = replay.next().await {
2463 match result {
2464 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2465 Err(err) => panic!("Failed to read item: {err}"),
2466 }
2467 }
2468 });
2469 }
2470
2471 #[test_traced]
2472 fn test_journal_rewind() {
2473 let executor = deterministic::Runner::default();
2475 executor.start(|context| async move {
2476 let cfg = Config {
2478 partition: "test-partition".into(),
2479 compression: None,
2480 codec_config: (),
2481 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2482 write_buffer: NZUsize!(1024),
2483 };
2484 let mut journal = Journal::init(context, cfg).await.unwrap();
2485
2486 let size = journal.size(1).unwrap();
2488 assert_eq!(size, 0);
2489
2490 (journal, _, _) = journal.append(1, &42i32).await.unwrap();
2492
2493 let size = journal.size(1).unwrap();
2495 assert!(size > 0);
2496
2497 (journal, _, _) = journal.append(1, &43i32).await.unwrap();
2499 let new_size = journal.size(1).unwrap();
2500 assert!(new_size > size);
2501
2502 let size = journal.size(2).unwrap();
2504 assert_eq!(size, 0);
2505
2506 (journal, _, _) = journal.append(2, &44i32).await.unwrap();
2508
2509 let size = journal.size(2).unwrap();
2511 assert!(size > 0);
2512
2513 journal = journal.rewind(1, 0).await.unwrap();
2515
2516 let size = journal.size(1).unwrap();
2518 assert_eq!(size, 0);
2519
2520 let size = journal.size(2).unwrap();
2522 assert_eq!(size, 0);
2523 });
2524 }
2525
2526 #[test_traced]
2527 fn test_journal_rewind_max_section() {
2528 let executor = deterministic::Runner::default();
2529 executor.start(|context| async move {
2530 let cfg = Config {
2531 partition: "test-partition".into(),
2532 compression: None,
2533 codec_config: (),
2534 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2535 write_buffer: NZUsize!(1024),
2536 };
2537 let mut journal = Journal::init(context, cfg).await.unwrap();
2538
2539 let offset;
2541 (journal, offset, _) = journal.append(u64::MAX, &42i32).await.unwrap();
2542 let size = journal.size(u64::MAX).unwrap();
2543 assert!(size > 0);
2544
2545 journal = journal.rewind(u64::MAX, size).await.unwrap();
2547
2548 assert_eq!(journal.size(u64::MAX).unwrap(), size);
2550 assert_eq!(journal.get(u64::MAX, offset).await.unwrap(), 42i32);
2551 });
2552 }
2553
2554 #[test_traced]
2555 fn test_journal_rewind_section() {
2556 let executor = deterministic::Runner::default();
2558 executor.start(|context| async move {
2559 let cfg = Config {
2561 partition: "test-partition".into(),
2562 compression: None,
2563 codec_config: (),
2564 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2565 write_buffer: NZUsize!(1024),
2566 };
2567 let mut journal = Journal::init(context, cfg).await.unwrap();
2568
2569 let size = journal.size(1).unwrap();
2571 assert_eq!(size, 0);
2572
2573 (journal, _, _) = journal.append(1, &42i32).await.unwrap();
2575
2576 let size = journal.size(1).unwrap();
2578 assert!(size > 0);
2579
2580 (journal, _, _) = journal.append(1, &43i32).await.unwrap();
2582 let new_size = journal.size(1).unwrap();
2583 assert!(new_size > size);
2584
2585 let size = journal.size(2).unwrap();
2587 assert_eq!(size, 0);
2588
2589 (journal, _, _) = journal.append(2, &44i32).await.unwrap();
2591
2592 let size = journal.size(2).unwrap();
2594 assert!(size > 0);
2595
2596 journal = journal.rewind_section(1, 0).await.unwrap();
2598
2599 let size = journal.size(1).unwrap();
2601 assert_eq!(size, 0);
2602
2603 let size = journal.size(2).unwrap();
2605 assert!(size > 0);
2606 });
2607 }
2608
2609 #[test_traced]
2610 fn test_journal_small_items() {
2611 let executor = deterministic::Runner::default();
2612 executor.start(|context| async move {
2613 let cfg = Config {
2614 partition: "test-partition".into(),
2615 compression: None,
2616 codec_config: (),
2617 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2618 write_buffer: NZUsize!(1024),
2619 };
2620
2621 let mut journal = Journal::init(context.child("first"), cfg.clone())
2622 .await
2623 .expect("Failed to initialize journal");
2624
2625 let num_items = 100;
2627 let mut offsets = Vec::new();
2628 for i in 0..num_items {
2629 let offset;
2630 let size;
2631 (journal, offset, size) = journal
2632 .append(1, &(i as u8))
2633 .await
2634 .expect("Failed to append data");
2635 assert_eq!(size, 1, "u8 should encode to 1 byte");
2636 offsets.push(offset);
2637 }
2638 journal = journal.sync(1).await.expect("Failed to sync");
2639
2640 for (i, &offset) in offsets.iter().enumerate() {
2642 let item: u8 = journal.get(1, offset).await.expect("Failed to get item");
2643 assert_eq!(item, i as u8, "Item mismatch at offset {offset}");
2644 }
2645
2646 drop(journal);
2648 let journal = Journal::<_, u8>::init(context.child("second"), cfg)
2649 .await
2650 .expect("Failed to re-initialize journal");
2651
2652 let mut replay = journal
2654 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2655 .await
2656 .expect("Failed to setup replay");
2657
2658 let mut count = 0;
2659 while let Some(result) = replay.next().await {
2660 let (section, offset, size, item) = result.expect("Failed to replay item");
2661 assert_eq!(section, 1);
2662 assert_eq!(offset, offsets[count]);
2663 assert_eq!(size, 1);
2664 assert_eq!(item, count as u8);
2665 count += 1;
2666 }
2667 assert_eq!(count, num_items, "Should replay all items");
2668 });
2669 }
2670
2671 #[test_traced]
2672 fn test_journal_rewind_many_sections() {
2673 let executor = deterministic::Runner::default();
2674 executor.start(|context| async move {
2675 let cfg = Config {
2676 partition: "test-partition".into(),
2677 compression: None,
2678 codec_config: (),
2679 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2680 write_buffer: NZUsize!(1024),
2681 };
2682 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2683 .await
2684 .unwrap();
2685
2686 for section in 1u64..=10 {
2688 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
2689 }
2690 journal = journal.sync_all().await.unwrap();
2691
2692 for section in 1u64..=10 {
2694 let size = journal.size(section).unwrap();
2695 assert!(size > 0, "section {section} should have data");
2696 }
2697
2698 let size = journal.size(5).unwrap();
2700 journal = journal.rewind(5, size).await.unwrap();
2701
2702 for section in 1u64..=5 {
2704 let size = journal.size(section).unwrap();
2705 assert!(size > 0, "section {section} should still have data");
2706 }
2707
2708 for section in 6u64..=10 {
2710 let size = journal.size(section).unwrap();
2711 assert_eq!(size, 0, "section {section} should be removed");
2712 }
2713
2714 {
2716 let mut replay = journal
2717 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2718 .await
2719 .unwrap();
2720 let mut items = Vec::new();
2721 while let Some(result) = replay.next().await {
2722 let (section, _, _, item) = result.unwrap();
2723 items.push((section, item));
2724 }
2725 journal = replay.finish().expect("failed to finish replay");
2726 assert_eq!(items.len(), 5);
2727 for (i, (section, item)) in items.iter().enumerate() {
2728 assert_eq!(*section, (i + 1) as u64);
2729 assert_eq!(*item, (i + 1) as i32);
2730 }
2731 }
2732
2733 journal.destroy().await.unwrap();
2734 });
2735 }
2736
2737 #[test_traced]
2738 fn test_journal_rewind_partial_truncation() {
2739 let executor = deterministic::Runner::default();
2740 executor.start(|context| async move {
2741 let cfg = Config {
2742 partition: "test-partition".into(),
2743 compression: None,
2744 codec_config: (),
2745 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2746 write_buffer: NZUsize!(1024),
2747 };
2748 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2749 .await
2750 .unwrap();
2751
2752 let mut sizes = Vec::new();
2754 for i in 0..5 {
2755 (journal, _, _) = journal.append(1, &i).await.unwrap();
2756 journal = journal.sync(1).await.unwrap();
2757 sizes.push(journal.size(1).unwrap());
2758 }
2759
2760 let target_size = sizes[2];
2762 journal = journal.rewind(1, target_size).await.unwrap();
2763
2764 let new_size = journal.size(1).unwrap();
2766 assert_eq!(new_size, target_size);
2767
2768 {
2770 let mut replay = journal
2771 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2772 .await
2773 .unwrap();
2774 let mut items = Vec::new();
2775 while let Some(result) = replay.next().await {
2776 let (_, _, _, item) = result.unwrap();
2777 items.push(item);
2778 }
2779 journal = replay.finish().expect("failed to finish replay");
2780 assert_eq!(items.len(), 3);
2781 for (i, item) in items.iter().enumerate() {
2782 assert_eq!(*item, i as i32);
2783 }
2784 }
2785
2786 journal.destroy().await.unwrap();
2787 });
2788 }
2789
2790 #[test_traced]
2791 fn test_journal_rewind_nonexistent_target() {
2792 let executor = deterministic::Runner::default();
2793 executor.start(|context| async move {
2794 let cfg = Config {
2795 partition: "test-partition".into(),
2796 compression: None,
2797 codec_config: (),
2798 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2799 write_buffer: NZUsize!(1024),
2800 };
2801 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2802 .await
2803 .unwrap();
2804
2805 for section in 5u64..=7 {
2807 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
2808 }
2809 journal = journal.sync_all().await.unwrap();
2810
2811 journal = journal.rewind(3, 0).await.unwrap();
2813
2814 for section in 5u64..=7 {
2816 let size = journal.size(section).unwrap();
2817 assert_eq!(size, 0, "section {section} should be removed");
2818 }
2819
2820 {
2822 let mut replay = journal
2823 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2824 .await
2825 .unwrap();
2826 assert!(replay.next().await.is_none());
2827 journal = replay.finish().expect("failed to finish replay");
2828 }
2829
2830 journal.destroy().await.unwrap();
2831 });
2832 }
2833
2834 #[test_traced]
2835 fn test_journal_rewind_persistence() {
2836 let executor = deterministic::Runner::default();
2837 executor.start(|context| async move {
2838 let cfg = Config {
2839 partition: "test-partition".into(),
2840 compression: None,
2841 codec_config: (),
2842 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2843 write_buffer: NZUsize!(1024),
2844 };
2845
2846 let mut journal = Journal::init(context.child("first"), cfg.clone())
2848 .await
2849 .unwrap();
2850 for section in 1u64..=5 {
2851 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
2852 }
2853 journal = journal.sync_all().await.unwrap();
2854
2855 let size = journal.size(2).unwrap();
2857 journal = journal.rewind(2, size).await.unwrap();
2858 journal = journal.sync_all().await.unwrap();
2859 drop(journal);
2860
2861 let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
2863 .await
2864 .unwrap();
2865
2866 for section in 1u64..=2 {
2868 let size = journal.size(section).unwrap();
2869 assert!(size > 0, "section {section} should have data after restart");
2870 }
2871
2872 for section in 3u64..=5 {
2874 let size = journal.size(section).unwrap();
2875 assert_eq!(size, 0, "section {section} should be gone after restart");
2876 }
2877
2878 {
2880 let mut replay = journal
2881 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2882 .await
2883 .unwrap();
2884 let mut items = Vec::new();
2885 while let Some(result) = replay.next().await {
2886 let (section, _, _, item) = result.unwrap();
2887 items.push((section, item));
2888 }
2889 journal = replay.finish().expect("failed to finish replay");
2890 assert_eq!(items.len(), 2);
2891 assert_eq!(items[0], (1, 1));
2892 assert_eq!(items[1], (2, 2));
2893 }
2894
2895 journal.destroy().await.unwrap();
2896 });
2897 }
2898
2899 #[test_traced]
2900 fn test_journal_rewind_to_zero_removes_all_newer() {
2901 let executor = deterministic::Runner::default();
2902 executor.start(|context| async move {
2903 let cfg = Config {
2904 partition: "test-partition".into(),
2905 compression: None,
2906 codec_config: (),
2907 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2908 write_buffer: NZUsize!(1024),
2909 };
2910 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2911 .await
2912 .unwrap();
2913
2914 for section in 1u64..=3 {
2916 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
2917 }
2918 journal = journal.sync_all().await.unwrap();
2919
2920 journal = journal.rewind(1, 0).await.unwrap();
2922
2923 let size = journal.size(1).unwrap();
2925 assert_eq!(size, 0, "section 1 should be empty");
2926
2927 for section in 2u64..=3 {
2929 let size = journal.size(section).unwrap();
2930 assert_eq!(size, 0, "section {section} should be removed");
2931 }
2932
2933 {
2935 let mut replay = journal
2936 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2937 .await
2938 .unwrap();
2939 assert!(replay.next().await.is_none());
2940 journal = replay.finish().expect("failed to finish replay");
2941 }
2942
2943 journal.destroy().await.unwrap();
2944 });
2945 }
2946
2947 #[test_traced]
2948 fn test_journal_replay_start_offset_with_trailing_bytes() {
2949 let executor = deterministic::Runner::default();
2951 executor.start(|context| async move {
2952 let cfg = Config {
2953 partition: "test-partition".into(),
2954 compression: None,
2955 codec_config: (),
2956 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2957 write_buffer: NZUsize!(1024),
2958 };
2959 let mut journal = Journal::init(context.child("first"), cfg.clone())
2960 .await
2961 .expect("Failed to initialize journal");
2962
2963 for i in 0..5i32 {
2965 (journal, _, _) = journal.append(1, &i).await.unwrap();
2966 }
2967 journal = journal.sync(1).await.unwrap();
2968 let valid_logical_size = journal.size(1).unwrap();
2969 drop(journal);
2970
2971 let (blob, physical_size_before) = context
2973 .open(&cfg.partition, &1u64.to_be_bytes())
2974 .await
2975 .unwrap();
2976
2977 blob.write_at(physical_size_before, vec![0xFF, 0xFF], WriteOptions::SYNC)
2980 .await
2981 .unwrap();
2982
2983 let start_offset = valid_logical_size;
2987 {
2988 let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
2989 .await
2990 .unwrap();
2991
2992 let mut replay = journal
2993 .replay(1, start_offset, NZUsize!(1024), ReadOptions::default())
2994 .await
2995 .unwrap();
2996
2997 while let Some(_result) = replay.next().await {}
2999 }
3000
3001 let (_, physical_size_after) = context
3003 .open(&cfg.partition, &1u64.to_be_bytes())
3004 .await
3005 .unwrap();
3006
3007 assert!(
3010 physical_size_after >= physical_size_before,
3011 "Valid data was lost! Physical blob truncated from {physical_size_before} to \
3012 {physical_size_after}. Logical valid size was {valid_logical_size}. \
3013 This indicates valid_offset was incorrectly initialized to 0 instead of start_offset."
3014 );
3015 });
3016 }
3017
3018 #[test_traced]
3019 fn test_journal_replay_rejects_start_offset_past_section() {
3020 let executor = deterministic::Runner::default();
3021 executor.start(|context| async move {
3022 let cfg = Config {
3023 partition: "test-partition".into(),
3024 compression: None,
3025 codec_config: (),
3026 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3027 write_buffer: NZUsize!(1024),
3028 };
3029 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3030 (journal, _, _) = journal.append(1, &7i32).await.unwrap();
3031
3032 let result = journal
3034 .replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
3035 .await;
3036 assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
3037 });
3038 }
3039
3040 #[test_traced]
3041 fn test_journal_large_item_spanning_pages() {
3042 const LARGE_SIZE: usize = 2048;
3044 type LargeItem = [u8; LARGE_SIZE];
3045
3046 let executor = deterministic::Runner::default();
3047 executor.start(|context| async move {
3048 let cfg = Config {
3049 partition: "test-partition".into(),
3050 compression: None,
3051 codec_config: (),
3052 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3053 write_buffer: NZUsize!(4096),
3054 };
3055 let mut journal = Journal::init(context.child("first"), cfg.clone())
3056 .await
3057 .expect("Failed to initialize journal");
3058
3059 let mut large_data: LargeItem = [0u8; LARGE_SIZE];
3061 for (i, byte) in large_data.iter_mut().enumerate() {
3062 *byte = (i % 256) as u8;
3063 }
3064 assert!(
3065 LARGE_SIZE > PAGE_SIZE.get() as usize,
3066 "Item must be larger than page size"
3067 );
3068
3069 let offset;
3071 let size;
3072 (journal, offset, size) = journal
3073 .append(1, &large_data)
3074 .await
3075 .expect("Failed to append large item");
3076 assert_eq!(size as usize, LARGE_SIZE);
3077 journal = journal.sync(1).await.expect("Failed to sync");
3078
3079 let retrieved: LargeItem = journal
3081 .get(1, offset)
3082 .await
3083 .expect("Failed to get large item");
3084 assert_eq!(retrieved, large_data, "Random access read mismatch");
3085
3086 drop(journal);
3088 let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
3089 .await
3090 .expect("Failed to re-initialize journal");
3091
3092 {
3094 let mut replay = journal
3095 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3096 .await
3097 .expect("Failed to setup replay");
3098
3099 let mut items = Vec::new();
3100 while let Some(result) = replay.next().await {
3101 let (section, off, sz, item) = result.expect("Failed to replay item");
3102 items.push((section, off, sz, item));
3103 }
3104 journal = replay.finish().expect("failed to finish replay");
3105
3106 assert_eq!(items.len(), 1, "Should have exactly one item");
3107 let (section, off, sz, item) = &items[0];
3108 assert_eq!(*section, 1);
3109 assert_eq!(*off, offset);
3110 assert_eq!(*sz as usize, LARGE_SIZE);
3111 assert_eq!(*item, large_data, "Replay read mismatch");
3112 }
3113
3114 journal.destroy().await.unwrap();
3115 });
3116 }
3117
3118 #[test_traced]
3119 fn test_journal_large_item_direct_path() {
3120 const LARGE_SIZE: usize = 2048;
3125 type LargeItem = [u8; LARGE_SIZE];
3126
3127 let executor = deterministic::Runner::default();
3128 executor.start(|context| async move {
3129 let cfg = Config {
3130 partition: "test-partition".into(),
3131 compression: None,
3132 codec_config: (),
3133 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3134 write_buffer: NZUsize!(1024),
3135 };
3136 let mut journal = Journal::init(context.child("first"), cfg.clone())
3137 .await
3138 .expect("Failed to initialize journal");
3139
3140 let mut first: LargeItem = [0u8; LARGE_SIZE];
3141 for (i, byte) in first.iter_mut().enumerate() {
3142 *byte = (i % 256) as u8;
3143 }
3144 let mut second: LargeItem = [0u8; LARGE_SIZE];
3145 for (i, byte) in second.iter_mut().enumerate() {
3146 *byte = ((i + 7) % 251) as u8;
3147 }
3148
3149 let first_offset;
3150 (journal, first_offset, _) = journal
3151 .append(1, &first)
3152 .await
3153 .expect("Failed to append first item");
3154 let second_offset;
3155 (journal, second_offset, _) = journal
3156 .append(1, &second)
3157 .await
3158 .expect("Failed to append second item");
3159
3160 let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
3162 assert_eq!(retrieved, first);
3163 let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
3164 assert_eq!(retrieved, second);
3165
3166 journal = journal.sync(1).await.expect("Failed to sync");
3168 drop(journal);
3169 let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
3170 .await
3171 .expect("Failed to re-initialize journal");
3172
3173 let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
3174 assert_eq!(retrieved, first);
3175 let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
3176 assert_eq!(retrieved, second);
3177
3178 {
3179 let mut replay = journal
3180 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3181 .await
3182 .expect("Failed to setup replay");
3183
3184 let mut items = Vec::new();
3185 while let Some(result) = replay.next().await {
3186 let (section, off, _, item) = result.expect("Failed to replay item");
3187 items.push((section, off, item));
3188 }
3189 journal = replay.finish().expect("failed to finish replay");
3190 assert_eq!(items.len(), 2);
3191 assert_eq!(items[0], (1, first_offset, first));
3192 assert_eq!(items[1], (1, second_offset, second));
3193 }
3194
3195 journal.destroy().await.unwrap();
3196 });
3197 }
3198
3199 #[test_traced]
3200 fn test_journal_non_contiguous_sections() {
3201 let executor = deterministic::Runner::default();
3204 executor.start(|context| async move {
3205 let cfg = Config {
3206 partition: "test-partition".into(),
3207 compression: None,
3208 codec_config: (),
3209 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3210 write_buffer: NZUsize!(1024),
3211 };
3212 let mut journal = Journal::init(context.child("first"), cfg.clone())
3213 .await
3214 .expect("Failed to initialize journal");
3215
3216 let sections_and_data = [(1u64, 100i32), (5u64, 500i32), (10u64, 1000i32)];
3218 let mut offsets = Vec::new();
3219
3220 for (section, data) in §ions_and_data {
3221 let offset;
3222 (journal, offset, _) = journal
3223 .append(*section, data)
3224 .await
3225 .expect("Failed to append");
3226 offsets.push(offset);
3227 }
3228 journal = journal.sync_all().await.expect("Failed to sync");
3229
3230 for (i, (section, expected_data)) in sections_and_data.iter().enumerate() {
3232 let retrieved: i32 = journal
3233 .get(*section, offsets[i])
3234 .await
3235 .expect("Failed to get item");
3236 assert_eq!(retrieved, *expected_data);
3237 }
3238
3239 for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
3241 let result = journal.get(missing_section, 0).await;
3242 assert!(
3243 matches!(result, Err(Error::SectionOutOfRange(_))),
3244 "Expected SectionOutOfRange for section {}, got {:?}",
3245 missing_section,
3246 result
3247 );
3248 }
3249
3250 drop(journal);
3252 let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3253 .await
3254 .expect("Failed to re-initialize journal");
3255
3256 {
3258 let mut replay = journal
3259 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3260 .await
3261 .expect("Failed to setup replay");
3262
3263 let mut items = Vec::new();
3264 while let Some(result) = replay.next().await {
3265 let (section, _, _, item) = result.expect("Failed to replay item");
3266 items.push((section, item));
3267 }
3268 journal = replay.finish().expect("failed to finish replay");
3269
3270 assert_eq!(items.len(), 3, "Should have 3 items");
3271 assert_eq!(items[0], (1, 100));
3272 assert_eq!(items[1], (5, 500));
3273 assert_eq!(items[2], (10, 1000));
3274 }
3275
3276 {
3278 let mut replay = journal
3279 .replay(5, 0, NZUsize!(1024), ReadOptions::default())
3280 .await
3281 .expect("Failed to setup replay from section 5");
3282
3283 let mut items = Vec::new();
3284 while let Some(result) = replay.next().await {
3285 let (section, _, _, item) = result.expect("Failed to replay item");
3286 items.push((section, item));
3287 }
3288 journal = replay.finish().expect("failed to finish replay");
3289
3290 assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
3291 assert_eq!(items[0], (5, 500));
3292 assert_eq!(items[1], (10, 1000));
3293 }
3294
3295 {
3297 let mut replay = journal
3298 .replay(3, 0, NZUsize!(1024), ReadOptions::default())
3299 .await
3300 .expect("Failed to setup replay from section 3");
3301
3302 let mut items = Vec::new();
3303 while let Some(result) = replay.next().await {
3304 let (section, _, _, item) = result.expect("Failed to replay item");
3305 items.push((section, item));
3306 }
3307 journal = replay.finish().expect("failed to finish replay");
3308
3309 assert_eq!(items.len(), 2);
3311 assert_eq!(items[0], (5, 500));
3312 assert_eq!(items[1], (10, 1000));
3313 }
3314
3315 journal.destroy().await.unwrap();
3316 });
3317 }
3318
3319 #[test_traced]
3320 fn test_journal_empty_section_in_middle() {
3321 let executor = deterministic::Runner::default();
3324 executor.start(|context| async move {
3325 let cfg = Config {
3326 partition: "test-partition".into(),
3327 compression: None,
3328 codec_config: (),
3329 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3330 write_buffer: NZUsize!(1024),
3331 };
3332 let mut journal = Journal::init(context.child("first"), cfg.clone())
3333 .await
3334 .expect("Failed to initialize journal");
3335
3336 (journal, _, _) = journal.append(1, &100i32).await.expect("Failed to append");
3338
3339 (journal, _, _) = journal.append(2, &200i32).await.expect("Failed to append");
3342 journal = journal.sync(2).await.expect("Failed to sync");
3343 journal = journal
3344 .rewind_section(2, 0)
3345 .await
3346 .expect("Failed to rewind");
3347
3348 (journal, _, _) = journal.append(3, &300i32).await.expect("Failed to append");
3350
3351 journal = journal.sync_all().await.expect("Failed to sync");
3352
3353 assert!(journal.size(1).unwrap() > 0);
3355 assert_eq!(journal.size(2).unwrap(), 0);
3356 assert!(journal.size(3).unwrap() > 0);
3357
3358 drop(journal);
3360 let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3361 .await
3362 .expect("Failed to re-initialize journal");
3363
3364 {
3366 let mut replay = journal
3367 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3368 .await
3369 .expect("Failed to setup replay");
3370
3371 let mut items = Vec::new();
3372 while let Some(result) = replay.next().await {
3373 let (section, _, _, item) = result.expect("Failed to replay item");
3374 items.push((section, item));
3375 }
3376 journal = replay.finish().expect("failed to finish replay");
3377
3378 assert_eq!(
3379 items.len(),
3380 2,
3381 "Should have 2 items (skipping empty section)"
3382 );
3383 assert_eq!(items[0], (1, 100));
3384 assert_eq!(items[1], (3, 300));
3385 }
3386
3387 {
3389 let mut replay = journal
3390 .replay(2, 0, NZUsize!(1024), ReadOptions::default())
3391 .await
3392 .expect("Failed to setup replay from section 2");
3393
3394 let mut items = Vec::new();
3395 while let Some(result) = replay.next().await {
3396 let (section, _, _, item) = result.expect("Failed to replay item");
3397 items.push((section, item));
3398 }
3399 journal = replay.finish().expect("failed to finish replay");
3400
3401 assert_eq!(items.len(), 1, "Should have 1 item from section 3");
3402 assert_eq!(items[0], (3, 300));
3403 }
3404
3405 journal.destroy().await.unwrap();
3406 });
3407 }
3408
3409 #[test_traced]
3410 fn test_journal_item_exactly_page_size() {
3411 const ITEM_SIZE: usize = PAGE_SIZE.get() as usize;
3414 type ExactItem = [u8; ITEM_SIZE];
3415
3416 let executor = deterministic::Runner::default();
3417 executor.start(|context| async move {
3418 let cfg = Config {
3419 partition: "test-partition".into(),
3420 compression: None,
3421 codec_config: (),
3422 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3423 write_buffer: NZUsize!(4096),
3424 };
3425 let mut journal = Journal::init(context.child("first"), cfg.clone())
3426 .await
3427 .expect("Failed to initialize journal");
3428
3429 let mut exact_data: ExactItem = [0u8; ITEM_SIZE];
3431 for (i, byte) in exact_data.iter_mut().enumerate() {
3432 *byte = (i % 256) as u8;
3433 }
3434
3435 let offset;
3437 let size;
3438 (journal, offset, size) = journal
3439 .append(1, &exact_data)
3440 .await
3441 .expect("Failed to append exact item");
3442 assert_eq!(size as usize, ITEM_SIZE);
3443 journal = journal.sync(1).await.expect("Failed to sync");
3444
3445 let retrieved: ExactItem = journal
3447 .get(1, offset)
3448 .await
3449 .expect("Failed to get exact item");
3450 assert_eq!(retrieved, exact_data, "Random access read mismatch");
3451
3452 drop(journal);
3454 let mut journal = Journal::<_, ExactItem>::init(context.child("second"), cfg.clone())
3455 .await
3456 .expect("Failed to re-initialize journal");
3457
3458 {
3460 let mut replay = journal
3461 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3462 .await
3463 .expect("Failed to setup replay");
3464
3465 let mut items = Vec::new();
3466 while let Some(result) = replay.next().await {
3467 let (section, off, sz, item) = result.expect("Failed to replay item");
3468 items.push((section, off, sz, item));
3469 }
3470 journal = replay.finish().expect("failed to finish replay");
3471
3472 assert_eq!(items.len(), 1, "Should have exactly one item");
3473 let (section, off, sz, item) = &items[0];
3474 assert_eq!(*section, 1);
3475 assert_eq!(*off, offset);
3476 assert_eq!(*sz as usize, ITEM_SIZE);
3477 assert_eq!(*item, exact_data, "Replay read mismatch");
3478 }
3479
3480 journal.destroy().await.unwrap();
3481 });
3482 }
3483
3484 #[test_traced]
3485 fn test_journal_varint_spanning_page_boundary() {
3486 const SMALL_PAGE: NonZeroU16 = NZU16!(16);
3494
3495 let executor = deterministic::Runner::default();
3496 executor.start(|context| async move {
3497 let cfg = Config {
3498 partition: "test-partition".into(),
3499 compression: None,
3500 codec_config: (),
3501 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE, PAGE_CACHE_SIZE),
3502 write_buffer: NZUsize!(1024),
3503 };
3504 let mut journal: Journal<_, [u8; 128]> =
3505 Journal::init(context.child("first"), cfg.clone())
3506 .await
3507 .expect("Failed to initialize journal");
3508
3509 let item1: [u8; 128] = [1u8; 128];
3511 let item2: [u8; 128] = [2u8; 128];
3512 let item3: [u8; 128] = [3u8; 128];
3513
3514 let offset1;
3517 (journal, offset1, _) = journal.append(1, &item1).await.expect("Failed to append");
3518 let offset2;
3519 (journal, offset2, _) = journal.append(1, &item2).await.expect("Failed to append");
3520 let offset3;
3521 (journal, offset3, _) = journal.append(1, &item3).await.expect("Failed to append");
3522
3523 journal = journal.sync(1).await.expect("Failed to sync");
3524
3525 let retrieved1: [u8; 128] = journal.get(1, offset1).await.expect("Failed to get");
3527 let retrieved2: [u8; 128] = journal.get(1, offset2).await.expect("Failed to get");
3528 let retrieved3: [u8; 128] = journal.get(1, offset3).await.expect("Failed to get");
3529 assert_eq!(retrieved1, item1);
3530 assert_eq!(retrieved2, item2);
3531 assert_eq!(retrieved3, item3);
3532
3533 drop(journal);
3535 let mut journal: Journal<_, [u8; 128]> =
3536 Journal::init(context.child("second"), cfg.clone())
3537 .await
3538 .expect("Failed to re-initialize journal");
3539
3540 {
3542 let mut replay = journal
3543 .replay(0, 0, NZUsize!(64), ReadOptions::default())
3544 .await
3545 .expect("Failed to setup replay");
3546
3547 let mut items = Vec::new();
3548 while let Some(result) = replay.next().await {
3549 let (section, off, _, item) = result.expect("Failed to replay item");
3550 items.push((section, off, item));
3551 }
3552 journal = replay.finish().expect("failed to finish replay");
3553
3554 assert_eq!(items.len(), 3, "Should have 3 items");
3555 assert_eq!(items[0], (1, offset1, item1));
3556 assert_eq!(items[1], (1, offset2, item2));
3557 assert_eq!(items[2], (1, offset3, item3));
3558 }
3559
3560 journal.destroy().await.unwrap();
3561 });
3562 }
3563
3564 #[test_traced]
3565 fn test_journal_clear() {
3566 let executor = deterministic::Runner::default();
3567 executor.start(|context| async move {
3568 let cfg = Config {
3569 partition: "clear-test".into(),
3570 compression: None,
3571 codec_config: (),
3572 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3573 write_buffer: NZUsize!(1024),
3574 };
3575
3576 let mut journal: Journal<_, u64> = Journal::init(context.child("journal"), cfg.clone())
3577 .await
3578 .expect("Failed to initialize journal");
3579
3580 for section in 0..5u64 {
3582 for i in 0..10u64 {
3583 (journal, _, _) = journal
3584 .append(section, &(section * 1000 + i))
3585 .await
3586 .expect("Failed to append");
3587 }
3588 journal = journal.sync(section).await.expect("Failed to sync");
3589 }
3590
3591 assert_eq!(journal.get(0, 0).await.unwrap(), 0);
3593 assert_eq!(journal.get(4, 0).await.unwrap(), 4000);
3594
3595 journal = journal.clear().await.expect("Failed to clear");
3597
3598 for section in 0..5u64 {
3600 assert!(matches!(
3601 journal.get(section, 0).await,
3602 Err(Error::SectionOutOfRange(s)) if s == section
3603 ));
3604 }
3605
3606 for i in 0..5u64 {
3608 (journal, _, _) = journal
3609 .append(10, &(i * 100))
3610 .await
3611 .expect("Failed to append after clear");
3612 }
3613 journal = journal.sync(10).await.expect("Failed to sync after clear");
3614
3615 assert_eq!(journal.get(10, 0).await.unwrap(), 0);
3617
3618 assert!(matches!(
3620 journal.get(0, 0).await,
3621 Err(Error::SectionOutOfRange(0))
3622 ));
3623
3624 journal.destroy().await.unwrap();
3625 });
3626 }
3627}