1use super::manager::{AppendFactory, Config as ManagerConfig, Manager};
83use crate::journal::{
84 Error,
85 frame::{
86 FrameInfo, Limited, UncompressedFrame, decode_item, decode_length_prefix,
87 encode_compressed_frame_into, find_frame, read_frame_at,
88 },
89};
90use bytes::Bytes;
91use commonware_codec::{Codec, CodecShared, Copying, Encode as _, varint::MAX_U32_VARINT_SIZE};
92use commonware_runtime::{
93 Blob, Buf, Error as RError, Handle, IoBuf, Metrics, ReadOptions, Storage,
94 buffer::paged::{CacheRef, Recovery as PagedRecovery, Replay as BlobReplay},
95};
96use std::{
97 collections::{BTreeSet, VecDeque},
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>,
139
140 compression: Option<u8>,
142
143 codec_config: V::Cfg,
145}
146
147impl<E: Storage + Metrics, V: Codec> Inner<E, V> {
148 fn writer(&self, section: u64) -> Result<&PagedRecovery<E::Blob>, Error> {
151 Ok(self
152 .manager
153 .get(section)?
154 .expect("replayed section is present"))
155 }
156}
157
158impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
159 async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
161 let manager_cfg = ManagerConfig {
162 partition: cfg.partition,
163 factory: AppendFactory {
164 write_buffer: cfg.write_buffer,
165 page_cache_ref: cfg.page_cache,
166 },
167 };
168 let manager = Manager::init(context, manager_cfg).await?;
169 let mut unrecovered = BTreeSet::new();
170 for section in manager.sections() {
171 if manager.size(section)? != 0 {
172 unrecovered.insert(section);
173 }
174 }
175
176 Ok(Self {
177 manager,
178 unrecovered,
179 compression: cfg.compression,
180 codec_config: cfg.codec_config,
181 })
182 }
183
184 async fn read(
186 compressed: bool,
187 cfg: &V::Cfg,
188 blob: &PagedRecovery<E::Blob>,
189 offset: u64,
190 ) -> Result<(u64, u32, V), Error> {
191 read_frame_at(blob, offset, cfg, compressed).await
192 }
193
194 async fn append(&mut self, section: u64, item: &V) -> Result<(u64, u32), Error> {
196 assert!(
197 !self.unrecovered.contains(§ion),
198 "section {section} must be replayed before append"
199 );
200
201 let (offset, item_len) = if let Some(level) = self.compression {
204 let mut buf = Vec::new();
206 let item_len = encode_compressed_frame_into(level, item, &mut buf)?;
207 let blob = self.manager.get_or_create(section).await?;
208 let offset = match blob.try_append(&buf) {
209 Some(offset) => offset,
210 None => {
211 let blob = self.manager.take(section).await?;
213 let (blob, offset) = blob.append_owned(IoBuf::from(buf)).await?;
214 self.manager.put(section, blob);
215 offset
216 }
217 };
218 (offset, item_len)
219 } else {
220 let frame = UncompressedFrame::new(item)?;
222 let blob = self.manager.get_or_create(section).await?;
223 let offset = match blob.try_append_value(&frame) {
224 Some(offset) => offset,
225 None => {
226 let blob = self.manager.take(section).await?;
228 let (blob, offset) = blob.append_owned(frame.encode_mut().into()).await?;
229 self.manager.put(section, blob);
230 offset
231 }
232 };
233 (offset, frame.item_len)
234 };
235 trace!(blob = section, offset, "appended item");
236 Ok((offset, item_len))
237 }
238
239 async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
241 let blob = self
242 .manager
243 .get(section)?
244 .ok_or(Error::SectionOutOfRange(section))?;
245
246 let (_, _, item) =
248 Self::read(self.compression.is_some(), &self.codec_config, blob, offset).await?;
249 Ok(item)
250 }
251
252 async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
254 if offsets.is_empty() {
255 return Ok(Vec::new());
256 }
257 let blob = self
258 .manager
259 .get(section)?
260 .ok_or(Error::SectionOutOfRange(section))?;
261
262 let compressed = self.compression.is_some();
263 let cfg = &self.codec_config;
264 let mut items = Vec::with_capacity(offsets.len());
265 for &offset in offsets {
266 let (_, _, item) = Self::read(compressed, cfg, blob, offset).await?;
267 items.push(item);
268 }
269 Ok(items)
270 }
271
272 fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
274 let blob = self.manager.get(section).ok()??;
275 let remaining = blob.size().checked_sub(offset)?;
276 let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
277 if header_len == 0 {
278 return None;
279 }
280
281 let mut header = [0u8; MAX_U32_VARINT_SIZE];
283 if !blob.try_read_sync_into(&mut header[..header_len], offset) {
284 return None;
285 }
286 let mut cursor = Copying(&header[..header_len]);
287 let (_, frame_info) = find_frame(&mut cursor, offset).ok()?;
288 let (varint_len, data_len) = match frame_info {
289 FrameInfo::Complete {
290 varint_len,
291 data_len,
292 } => (varint_len, data_len),
293 FrameInfo::Incomplete {
294 varint_len,
295 total_len,
296 ..
297 } => (varint_len, total_len),
298 };
299 let item_len = varint_len.checked_add(data_len)?;
300 if item_len > usize::try_from(remaining).ok()? {
301 return None;
302 }
303
304 let compressed = self.compression.is_some();
306 if item_len <= header_len {
307 return decode_item::<V>(
308 Copying(&header[varint_len..varint_len + data_len]),
309 &self.codec_config,
310 compressed,
311 )
312 .ok();
313 }
314
315 let mut buf = vec![0u8; item_len];
318 if !blob.try_read_sync_into(&mut buf, offset) {
319 return None;
320 }
321 let mut buf = Bytes::from(buf);
322 buf.advance(varint_len);
323 decode_item::<V>(buf, &self.codec_config, compressed).ok()
324 }
325
326 fn size(&self, section: u64) -> Result<u64, Error> {
328 self.manager.size(section)
329 }
330
331 async fn sync(mut self: Box<Self>, sections: impl crate::Sections) -> Result<Box<Self>, Error> {
333 self.manager = self.manager.sync(sections).await?;
334 Ok(self)
335 }
336
337 async fn start_sync(
339 mut self: Box<Self>,
340 sections: impl crate::Sections,
341 ) -> Result<(Box<Self>, Handle<()>), Error> {
342 let (manager, handle) = self.manager.start_sync(sections).await?;
343 self.manager = manager;
344 Ok((self, handle))
345 }
346
347 async fn sync_all(mut self: Box<Self>) -> Result<Box<Self>, Error> {
349 self.manager = self.manager.sync_all().await?;
350 Ok(self)
351 }
352
353 async fn prune(mut self: Box<Self>, min: u64) -> Result<(Box<Self>, bool), Error> {
355 let (manager, pruned) = self.manager.prune(min).await?;
356 self.manager = manager;
357 if pruned {
358 self.unrecovered.retain(|section| *section >= min);
359 }
360 Ok((self, pruned))
361 }
362
363 const fn pruned(&self, section: u64) -> bool {
365 self.manager.pruned(section)
366 }
367
368 fn oldest_section(&self) -> Option<u64> {
370 self.manager.oldest_section()
371 }
372
373 fn newest_section(&self) -> Option<u64> {
375 self.manager.newest_section()
376 }
377
378 fn is_empty(&self) -> bool {
380 self.manager.is_empty()
381 }
382
383 fn num_sections(&self) -> usize {
385 self.manager.num_sections()
386 }
387
388 async fn destroy(self) -> Result<(), Error> {
390 self.manager.destroy().await
391 }
392
393 async fn clear(mut self: Box<Self>) -> Result<Box<Self>, Error> {
395 self.manager = self.manager.clear().await?;
396 self.unrecovered.clear();
397 Ok(self)
398 }
399}
400
401pub struct Journal<E: Storage + Metrics, V: Codec>(Box<Inner<E, V>>);
423
424impl<E: Storage + Metrics, V: CodecShared> std::fmt::Debug for Journal<E, V> {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 f.debug_struct("Journal")
427 .field("oldest_section", &self.oldest_section())
428 .field("newest_section", &self.newest_section())
429 .finish_non_exhaustive()
430 }
431}
432
433impl<E: Storage + Metrics, V: CodecShared> Journal<E, V> {
434 pub async fn init_at_most(
439 context: E,
440 cfg: Config<V::Cfg>,
441 section: u64,
442 end: u64,
443 ) -> Result<Self, Error> {
444 let replay_buffer = cfg.write_buffer;
445 let mut end = end;
446 super::manager::truncate_paged_tail(
447 &context,
448 &cfg.partition,
449 cfg.page_cache.page_size(),
450 section,
451 end,
452 )
453 .await?;
454 let mut journal = Self::init(context, cfg).await?;
455
456 if let Some(blob) = journal.0.manager.get(section)?
458 && end > 0
459 && end < blob.size()
460 {
461 end = end.min(
462 blob.recoverable_prefix_len_at_most(0, end, replay_buffer, ReadOptions::default())
463 .await?,
464 );
465 }
466 journal.0.manager = journal.0.manager.truncate_pending(section, end).await?;
467 let mut replay = journal
468 .replay(0, 0, replay_buffer, ReadOptions::default())
469 .await?;
470 while let Some(item) = replay.next().await {
471 item?;
472 }
473 replay.finish()
474 }
475
476 pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
482 Ok(Self(Box::new(Inner::init(context, cfg).await?)))
483 }
484
485 pub async fn replay(
497 mut self,
498 start_section: u64,
499 start_offset: u64,
500 buffer: NonZeroUsize,
501 read_options: ReadOptions,
502 ) -> Result<Replay<E, V>, Error> {
503 let mut sections = VecDeque::new();
505 let replayed: Vec<_> = self.0.manager.sections_from(start_section).collect();
506 for section in replayed {
507 let blob = self.0.manager.take(section).await?;
508 let (blob, reader) = blob.replay(buffer, read_options).await?;
509 self.0.manager.put(section, blob);
510
511 let skip_bytes = if section == start_section {
513 start_offset
514 } else {
515 0
516 };
517 sections.push_back(SectionReplay {
518 section,
519 reader,
520 skip_bytes,
521 offset: 0,
522 valid_offset: skip_bytes,
523 pending: None,
524 });
525 }
526 let finished = sections.is_empty();
527 let replay = Replay {
528 journal: self,
529 sections,
530 recovered_from: if start_offset == 0 {
531 Some(start_section)
532 } else {
533 start_section.checked_add(1)
534 },
535 buffer,
536 read_options,
537 finished,
538 errored: false,
539 repairing: false,
540 };
541
542 if let Some(current) = replay.sections.front()
546 && current.section == start_section
547 && start_offset > current.reader.blob_size()
548 {
549 return Err(Error::ItemOutOfRange(start_offset));
550 }
551 Ok(replay)
552 }
553
554 pub async fn append(mut self, section: u64, item: &V) -> Result<(Self, u64, u32), Error> {
563 let (offset, item_len) = self.0.append(section, item).await?;
564 Ok((self, offset, item_len))
565 }
566
567 pub async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
578 self.0.get(section, offset).await
579 }
580
581 pub async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
585 self.0.get_many(section, offsets).await
586 }
587
588 pub fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
590 self.0.try_get_sync(section, offset)
591 }
592
593 pub fn size(&self, section: u64) -> Result<u64, Error> {
597 self.0.size(section)
598 }
599
600 pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
605 self.0 = self.0.sync(sections).await?;
606 Ok(self)
607 }
608
609 pub async fn start_sync(
614 mut self,
615 sections: impl crate::Sections,
616 ) -> Result<(Self, Handle<()>), Error> {
617 let (inner, handle) = self.0.start_sync(sections).await?;
618 self.0 = inner;
619 Ok((self, handle))
620 }
621
622 pub async fn sync_all(mut self) -> Result<Self, Error> {
624 self.0 = self.0.sync_all().await?;
625 Ok(self)
626 }
627
628 pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
630 let (inner, pruned) = self.0.prune(min).await?;
631 self.0 = inner;
632 Ok((self, pruned))
633 }
634
635 pub fn pruned(&self, section: u64) -> bool {
640 self.0.pruned(section)
641 }
642
643 pub fn oldest_section(&self) -> Option<u64> {
645 self.0.oldest_section()
646 }
647
648 pub fn newest_section(&self) -> Option<u64> {
650 self.0.newest_section()
651 }
652
653 pub fn is_empty(&self) -> bool {
655 self.0.is_empty()
656 }
657
658 pub fn num_sections(&self) -> usize {
660 self.0.num_sections()
661 }
662
663 pub async fn destroy(self) -> Result<(), Error> {
665 self.0.destroy().await
666 }
667
668 pub async fn clear(mut self) -> Result<Self, Error> {
672 self.0 = self.0.clear().await?;
673 Ok(self)
674 }
675}
676
677pub struct Replay<E: Storage + Metrics, V: Codec> {
684 journal: Journal<E, V>,
685 sections: VecDeque<SectionReplay<E::Blob>>,
686 recovered_from: Option<u64>,
689 buffer: NonZeroUsize,
690 read_options: ReadOptions,
691 finished: bool,
692 errored: bool,
693 repairing: bool,
694}
695
696impl<E: Storage + Metrics, V: CodecShared> Replay<E, V> {
697 async fn plan_repair(&mut self, source: RError) -> Result<u64, Error> {
700 if !matches!(source, RError::InvalidChecksum) {
703 return Err(source.into());
704 }
705
706 let current = self.sections.front().expect("replayed section is present");
708 let section = current.section;
709 if !self.journal.0.unrecovered.contains(§ion) {
710 return Err(source.into());
711 }
712 let size = current.reader.blob_size();
713 let valid_offset = current.valid_offset;
714
715 let recoverable = self
717 .journal
718 .0
719 .writer(section)?
720 .recoverable_prefix_len(valid_offset, self.buffer, self.read_options)
721 .await?;
722
723 if recoverable >= size {
726 return Err(source.into());
727 }
728
729 if recoverable < valid_offset {
732 return Err(Error::ItemOutOfRange(valid_offset));
733 }
734
735 Ok(recoverable)
736 }
737
738 async fn repair(&mut self, source: RError) -> Result<(), Error> {
740 let recoverable = match self.plan_repair(source).await {
742 Ok(target) => target,
743 Err(err) => {
744 self.sections.pop_front();
745 return Err(err);
746 }
747 };
748
749 let current = self.sections.front().expect("replayed section is present");
750 let (section, valid_offset) = (current.section, current.valid_offset);
751 warn!(
752 section,
753 invalid_size = current.reader.blob_size(),
754 new_size = recoverable,
755 "torn page detected: truncating"
756 );
757
758 self.repairing = true;
761 let current = self
762 .sections
763 .pop_front()
764 .expect("repaired section is present");
765 drop(current.reader);
766 let writer = self.journal.0.manager.take(section).await?;
767 let writer = writer.truncate(recoverable).await?;
768 let (writer, mut reader) = writer.replay(self.buffer, self.read_options).await?;
769 self.journal.0.manager.put(section, writer);
770 reader.seek_to(valid_offset)?;
771 self.sections.push_front(SectionReplay {
772 section,
773 reader,
774 skip_bytes: 0,
775 offset: valid_offset,
776 valid_offset,
777 pending: None,
778 });
779 self.repairing = false;
780 Ok(())
781 }
782
783 async fn repair_tail(&mut self, message: &'static str) -> Result<(), Error> {
785 let current = self.sections.front().expect("replayed section is present");
786 let (section, offset, valid_offset) =
787 (current.section, current.offset, current.valid_offset);
788 if !self.journal.0.unrecovered.contains(§ion) {
789 return Err(Error::ItemOutOfRange(offset));
790 }
791 warn!(
792 blob = section,
793 bad_offset = offset,
794 new_size = valid_offset,
795 "{message}"
796 );
797
798 self.repairing = true;
802 let writer = self.journal.0.manager.take(section).await?;
803 let writer = writer.truncate(valid_offset).await?;
804 self.journal.0.manager.put(section, writer);
805 self.repairing = false;
806 Ok(())
807 }
808
809 pub async fn next(&mut self) -> Option<Result<(u64, u64, u32, V), Error>> {
816 if self.repairing {
819 self.repairing = false;
820 self.sections.clear();
821 if !self.errored {
822 return self.fail(Error::ReplayInterrupted);
823 }
824 }
825 while let Some(current) = self.sections.front_mut() {
826 let blob_size = current.reader.blob_size();
827
828 let (item_size, varint_len) = match current.pending {
830 Some(header) => header,
831 None => {
832 match current.reader.ensure(MAX_U32_VARINT_SIZE).await {
836 Ok(true) => {}
837 Ok(false) => {
838 if current.reader.remaining() == 0 {
840 self.sections.pop_front();
841 continue;
842 }
843 }
845 Err(err) => {
846 if let Err(err) = self.repair(err).await {
847 return self.fail(err);
848 }
849 continue;
850 }
851 }
852
853 if current.skip_bytes > 0 {
855 let to_skip =
856 current.skip_bytes.min(current.reader.remaining() as u64) as usize;
857 current.reader.advance(to_skip);
858 current.skip_bytes -= to_skip as u64;
859 current.offset += to_skip as u64;
860 continue;
861 }
862
863 let before_remaining = current.reader.remaining();
865 match decode_length_prefix(&mut current.reader) {
866 Ok(header) => {
867 current.pending = Some(header);
870 header
871 }
872 Err(err) => {
873 if current.reader.is_exhausted()
875 || before_remaining < MAX_U32_VARINT_SIZE
876 {
877 if current.valid_offset < blob_size
879 && current.offset < blob_size
880 && let Err(err) = self
881 .repair_tail("trailing bytes detected: truncating")
882 .await
883 {
884 self.sections.pop_front();
885 return self.fail(err);
886 }
887 self.sections.pop_front();
888 continue;
889 }
890 self.sections.pop_front();
891 return self.fail(err);
892 }
893 }
894 }
895 };
896
897 match current.reader.ensure(item_size).await {
899 Ok(true) => {}
900 Ok(false) => {
901 if let Err(err) = self.repair_tail("incomplete item at end: truncating").await {
903 self.sections.pop_front();
904 return self.fail(err);
905 }
906 self.sections.pop_front();
907 continue;
908 }
909 Err(err) => {
910 if let Err(err) = self.repair(err).await {
911 return self.fail(err);
912 }
913 continue;
914 }
915 }
916
917 let item_offset = current.offset;
919 let next_offset = match current
920 .offset
921 .checked_add(varint_len as u64)
922 .and_then(|o| o.checked_add(item_size as u64))
923 {
924 Some(o) => o,
925 None => {
926 self.sections.pop_front();
927 return self.fail(Error::OffsetOverflow);
928 }
929 };
930 match decode_item::<V>(
931 Limited::new(&mut current.reader, item_size),
932 &self.journal.0.codec_config,
933 self.journal.0.compression.is_some(),
934 ) {
935 Ok(decoded) => {
936 current.pending = None;
937 current.valid_offset = next_offset;
938 current.offset = next_offset;
939 return Some(Ok((
940 current.section,
941 item_offset,
942 item_size as u32,
943 decoded,
944 )));
945 }
946 Err(err) => {
947 self.sections.pop_front();
948 return self.fail(err);
949 }
950 }
951 }
952 self.finished = true;
953 None
954 }
955
956 const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, u32, V), Error>> {
958 self.errored = true;
959 Some(Err(err))
960 }
961
962 pub fn finish(mut self) -> Result<Journal<E, V>, Error> {
967 if self.errored || !self.finished {
968 return Err(Error::ReplayFailed);
969 }
970 if let Some(start) = self.recovered_from {
971 self.journal
972 .0
973 .unrecovered
974 .retain(|section| *section < start);
975 }
976 Ok(self.journal)
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use crate::journal::frame::encode_frame_into;
984 use commonware_codec::{EncodeSize, Write as _, varint::UInt};
985 use commonware_macros::test_traced;
986 use commonware_runtime::{
987 Blob, BufMut, Runner, Storage, Supervisor as _, WriteOptions,
988 buffer::paged::corrupt_page,
989 deterministic,
990 mocks::{DelayedSyncContext, PendingSyncs, RecordingContext, release_pending_syncs},
991 };
992 use commonware_utils::{NZU16, NZUsize, probability};
993 use std::num::NonZeroU16;
994
995 impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
996 async fn append_raw(&mut self, section: u64, buf: IoBuf) -> Result<u64, Error> {
998 let blob = self.manager.take(section).await?;
999 let (blob, offset) = blob.append_owned(buf).await?;
1000 self.manager.put(section, blob);
1001 Ok(offset)
1002 }
1003 }
1004
1005 impl<E: Storage + Metrics, V: CodecShared> Journal<E, V> {
1006 async fn test_reopen_at_most(self, section: u64, end: u64) -> Result<Self, Error> {
1007 let (context, partition, factory) = self.0.manager.test_configuration();
1008 let cfg = Config {
1009 partition,
1010 write_buffer: factory.write_buffer,
1011 page_cache: factory.page_cache_ref,
1012 compression: self.0.compression,
1013 codec_config: self.0.codec_config.clone(),
1014 };
1015 _ = self.sync_all().await?;
1016 Self::init_at_most(context, cfg, section, end).await
1017 }
1018
1019 async fn test_reopen_section(self, section: u64, end: u64) -> Result<Self, Error> {
1020 let (context, partition, factory) = self.0.manager.test_configuration();
1021 let replay_buffer = factory.write_buffer;
1022 let cfg = Config {
1023 partition,
1024 write_buffer: replay_buffer,
1025 page_cache: factory.page_cache_ref,
1026 compression: self.0.compression,
1027 codec_config: self.0.codec_config.clone(),
1028 };
1029 _ = self.sync_all().await?;
1030 let mut journal = Self::init(context, cfg).await?;
1031 journal.0.manager = journal
1032 .0
1033 .manager
1034 .truncate_pending_section(section, end)
1035 .await?;
1036 let mut replay = journal
1037 .replay(0, 0, replay_buffer, ReadOptions::default())
1038 .await?;
1039 while let Some(item) = replay.next().await {
1040 item?;
1041 }
1042 replay.finish()
1043 }
1044 }
1045
1046 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
1047 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
1048
1049 async fn journal_with_torn_interior_page(
1050 context: &deterministic::Context,
1051 partition: &str,
1052 later_section: bool,
1053 ) -> Journal<deterministic::Context, u64> {
1054 const LOGICAL_PAGE_SIZE: u64 = 64;
1055 const FIRST_SECTION: u64 = 0;
1056 const TORN_SECTION: u64 = 1;
1057
1058 let cfg = Config {
1059 partition: partition.into(),
1060 compression: None,
1061 codec_config: (),
1062 page_cache: CacheRef::new(NZU16!(LOGICAL_PAGE_SIZE as u16), NZUsize!(4)),
1063 write_buffer: NZUsize!(256),
1064 };
1065 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1066 .await
1067 .unwrap();
1068 (journal, _, _) = journal.append(FIRST_SECTION, &u64::MAX).await.unwrap();
1069 for value in 0..15u64 {
1070 let offset;
1071 (journal, offset, _) = journal.append(TORN_SECTION, &value).await.unwrap();
1072 assert_eq!(offset, value * 9);
1073 }
1074 if later_section {
1075 (journal, _, _) = journal.append(2, &u64::MIN).await.unwrap();
1076 }
1077 journal = journal.sync_all().await.unwrap();
1078 drop(journal);
1079
1080 corrupt_page(
1081 context,
1082 &cfg.partition,
1083 &TORN_SECTION.to_be_bytes(),
1084 1,
1085 LOGICAL_PAGE_SIZE,
1086 )
1087 .await;
1088
1089 Journal::<_, u64>::init(context.child("recover"), cfg)
1090 .await
1091 .unwrap()
1092 }
1093
1094 #[test]
1095 fn test_segmented_capped_init_bounds_torn_suffix_reads() {
1096 deterministic::Runner::default().start(|context| async move {
1097 for source_section in [0, 1] {
1098 for cap in [0, 9] {
1099 let mut counts = Vec::new();
1101 for pages in [16, 4096] {
1102 let partition = format!("torn-cap-{source_section}-{cap}-{pages}");
1103 let mut page = Vec::new();
1104 for value in 0..8u64 {
1105 encode_frame_into(None, &value, &mut page).unwrap();
1106 }
1107 assert_eq!(page.len(), 72);
1108 super::super::manager::tests::seed_torn_suffix(
1109 &context,
1110 &partition,
1111 source_section,
1112 &page,
1113 pages,
1114 )
1115 .await;
1116
1117 let cfg = Config {
1119 partition,
1120 page_cache: CacheRef::new(NZU16!(72), NZUsize!(4)),
1121 write_buffer: NZUsize!(144),
1122 compression: None,
1123 codec_config: (),
1124 };
1125 let (recorded, recordings) = RecordingContext::new(context.child("cap"));
1126 let mut journal =
1127 Journal::<_, u64>::init_at_most(recorded, cfg.clone(), 0, cap)
1128 .await
1129 .unwrap();
1130 let retained = if source_section == 0 { cap } else { 0 };
1131 assert_eq!(journal.size(0).unwrap(), retained);
1132 counts.push(recordings.snapshot().reads.len());
1133
1134 (journal, _, _) = journal.append(0, &999).await.unwrap();
1136 drop(journal.sync_all().await.unwrap());
1137 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1138 .await
1139 .unwrap();
1140 let mut replay = journal
1141 .replay(0, 0, NZUsize!(144), ReadOptions::default())
1142 .await
1143 .unwrap();
1144 let mut actual = Vec::new();
1145 while let Some(item) = replay.next().await {
1146 actual.push(item.unwrap().3);
1147 }
1148 replay.finish().unwrap();
1149 let expected = if retained == 0 {
1150 vec![999]
1151 } else {
1152 vec![0, 999]
1153 };
1154 assert_eq!(actual, expected);
1155 }
1156
1157 assert_eq!(
1159 counts[0], counts[1],
1160 "discarded suffix reads grew: {counts:?}"
1161 );
1162 }
1163 }
1164 });
1165 }
1166
1167 #[test_traced]
1168 fn test_bounded_initialization_bounds_scan() {
1169 deterministic::Runner::default().start(|context| async move {
1170 let mut counts = Vec::new();
1172 for count in [128, 65536] {
1173 let cfg = Config {
1174 partition: format!("variable-cap-scan-{count}"),
1175 page_cache: CacheRef::new(NZU16!(64), NZUsize!(4)),
1176 write_buffer: NZUsize!(65536),
1177 compression: None,
1178 codec_config: (),
1179 };
1180 let mut journal = Journal::<_, u64>::init(
1181 context.child(if count == 128 {
1182 "seed_small"
1183 } else {
1184 "seed_large"
1185 }),
1186 cfg.clone(),
1187 )
1188 .await
1189 .unwrap();
1190 for value in 0..count {
1191 (journal, _, _) = journal.append(0, &value).await.unwrap();
1192 }
1193 _ = journal.sync_all().await.unwrap();
1194
1195 let (recorded, recordings) =
1197 RecordingContext::new(context.child(if count == 128 {
1198 "cap_small"
1199 } else {
1200 "cap_large"
1201 }));
1202 let journal = Journal::<_, u64>::init_at_most(recorded, cfg, 0, 9)
1203 .await
1204 .unwrap();
1205 assert_eq!(journal.size(0).unwrap(), 9);
1206 counts.push(recordings.snapshot().reads.len());
1207 }
1208
1209 assert_eq!(
1211 counts[0], counts[1],
1212 "same one-item cap: small/large suffix read counts {counts:?}"
1213 );
1214 });
1215 }
1216
1217 #[test_traced]
1218 fn test_bounded_initialization_recovers_torn_page() {
1219 deterministic::Runner::default().start(|context| async move {
1220 for cap in [0, 9, 60, 63, 64, 65, 90, 128, 135, u64::MAX] {
1221 let cfg = Config {
1223 partition: format!("capped-torn-{cap}"),
1224 page_cache: CacheRef::new(NZU16!(64), NZUsize!(4)),
1225 write_buffer: NZUsize!(256),
1226 compression: None,
1227 codec_config: (),
1228 };
1229 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1230 .await
1231 .unwrap();
1232 for value in 0..15 {
1233 (journal, _, _) = journal.append(0, &value).await.unwrap();
1234 }
1235 (journal, _, _) = journal.append(1, &100).await.unwrap();
1236 _ = journal.sync_all().await.unwrap();
1237
1238 corrupt_page(&context, &cfg.partition, &0u64.to_be_bytes(), 1, 64).await;
1240 let journal =
1241 Journal::<_, u64>::init_at_most(context.child("cap"), cfg.clone(), 0, cap)
1242 .await
1243 .unwrap();
1244 let expected_bytes = cap.min(63) / 9 * 9;
1245 assert_eq!(journal.size(0).unwrap(), expected_bytes, "cap {cap}");
1246 assert_eq!(journal.newest_section(), Some(0));
1247 drop(journal);
1248
1249 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1251 .await
1252 .unwrap();
1253 let mut replay = journal
1254 .replay(0, 0, NZUsize!(256), ReadOptions::default())
1255 .await
1256 .unwrap();
1257 let mut count = 0;
1258 while let Some(item) = replay.next().await {
1259 let item = item.unwrap();
1260 assert_eq!(item.0, 0);
1261 assert_eq!(item.3, count);
1262 count += 1;
1263 }
1264 assert_eq!(count, expected_bytes / 9);
1265 let mut journal = replay.finish().unwrap();
1266 let position;
1267 (journal, position, _) = journal.append(0, &99).await.unwrap();
1268 assert_eq!(position, expected_bytes);
1269 journal.destroy().await.unwrap();
1270 }
1271 });
1272 }
1273
1274 #[test_traced]
1275 #[should_panic(expected = "must be replayed before append")]
1276 fn test_segmented_variable_rejects_append_before_replay() {
1277 let executor = deterministic::Runner::default();
1278 executor.start(|context| async move {
1279 const PARTITION: &str = "segmented-variable-append-before-replay";
1280 const NEW_SECTION: u64 = 2;
1281 const TORN_SECTION: u64 = 1;
1282
1283 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
1284 let (journal, offset, _) = journal.append(NEW_SECTION, &15).await.unwrap();
1285 assert_eq!(offset, 0);
1286 let (journal, offset, _) = journal.append(NEW_SECTION, &16).await.unwrap();
1287 assert_eq!(offset, 9);
1288 let _ = journal.append(TORN_SECTION, &15).await;
1289 });
1290 }
1291
1292 #[test_traced]
1293 #[should_panic(expected = "must be replayed before append")]
1294 fn test_segmented_variable_partial_replay_keeps_append_guard() {
1295 let executor = deterministic::Runner::default();
1296 executor.start(|context| async move {
1297 const PARTITION: &str = "segmented-variable-partial-replay-append";
1298 const SECTION: u64 = 1;
1299
1300 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
1301 let mut replay = journal
1302 .replay(SECTION, 9, NZUsize!(1024), ReadOptions::default())
1303 .await
1304 .unwrap();
1305 while let Some(item) = replay.next().await {
1306 item.unwrap();
1307 }
1308 let journal = replay.finish().unwrap();
1309 let _ = journal.append(SECTION, &7).await;
1310 });
1311 }
1312
1313 #[test_traced]
1314 #[should_panic(expected = "must be replayed before append")]
1315 fn test_segmented_variable_gates_clean_older_section() {
1316 let executor = deterministic::Runner::default();
1317 executor.start(|context| async move {
1318 const PARTITION: &str = "segmented-variable-gate-clean-older-section";
1319
1320 let journal = journal_with_torn_interior_page(&context, PARTITION, true).await;
1323 let _ = journal.append(0, &7).await;
1324 });
1325 }
1326
1327 #[test_traced]
1328 fn test_segmented_variable_replay_propagates_read_options() {
1329 let executor = deterministic::Runner::default();
1330 executor.start(|context| async move {
1331 let (context, recordings) = RecordingContext::new(context);
1332 let cfg = Config {
1333 partition: "test-partition".into(),
1334 compression: None,
1335 codec_config: (),
1336 page_cache: CacheRef::new(NZU16!(8), PAGE_CACHE_SIZE),
1337 write_buffer: NZUsize!(1024),
1338 };
1339 let mut journal = Journal::init(context.child("storage"), cfg)
1340 .await
1341 .expect("failed to init");
1342
1343 for section in 1..=2 {
1344 (journal, _, _) = journal
1345 .append(section, §ion)
1346 .await
1347 .expect("failed to append");
1348 }
1349
1350 let mut replay = journal
1351 .replay(1, 0, NZUsize!(1036), ReadOptions::DONT_CACHE)
1352 .await
1353 .expect("failed to replay");
1354 recordings.clear();
1355
1356 let (section, offset, _, item) = replay
1358 .next()
1359 .await
1360 .expect("missing first replay item")
1361 .expect("failed to read first replay item");
1362 assert_eq!((section, offset, item), (1, 0, 1));
1363 let reads = recordings.snapshot().reads;
1364 assert!(!reads.is_empty());
1365 assert!(
1366 reads
1367 .iter()
1368 .all(|options| *options == ReadOptions::DONT_CACHE)
1369 );
1370
1371 recordings.clear();
1373 let (section, offset, _, item) = replay
1374 .next()
1375 .await
1376 .expect("missing second replay item")
1377 .expect("failed to read second replay item");
1378 assert_eq!((section, offset, item), (2, 0, 2));
1379 let reads = recordings.snapshot().reads;
1380 assert!(!reads.is_empty());
1381 assert!(
1382 reads
1383 .iter()
1384 .all(|options| *options == ReadOptions::DONT_CACHE)
1385 );
1386 assert!(replay.next().await.is_none());
1387
1388 let journal = replay.finish().expect("failed to finish replay");
1389 journal.destroy().await.expect("failed to destroy");
1390 });
1391 }
1392
1393 #[test_traced]
1394 fn test_journal_append_and_read() {
1395 let executor = deterministic::Runner::default();
1397
1398 executor.start(|context| async move {
1400 let cfg = Config {
1402 partition: "test-partition".into(),
1403 compression: None,
1404 codec_config: (),
1405 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1406 write_buffer: NZUsize!(1024),
1407 };
1408 let index = 1u64;
1409 let data = 10;
1410 let mut journal = Journal::init(context.child("first"), cfg.clone())
1411 .await
1412 .expect("Failed to initialize journal");
1413
1414 (journal, _, _) = journal
1416 .append(index, &data)
1417 .await
1418 .expect("Failed to append data");
1419
1420 let buffer = context.encode();
1422 assert!(buffer.contains("first_tracked 1"));
1423
1424 journal = journal.sync(index).await.expect("Failed to sync journal");
1426 drop(journal);
1427 let journal = Journal::<_, i32>::init(context.child("second"), cfg)
1428 .await
1429 .expect("Failed to re-initialize journal");
1430
1431 let mut items = Vec::new();
1433 let mut replay = journal
1434 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1435 .await
1436 .expect("unable to setup replay");
1437 while let Some(result) = replay.next().await {
1438 match result {
1439 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1440 Err(err) => panic!("Failed to read item: {err}"),
1441 }
1442 }
1443
1444 assert_eq!(items.len(), 1);
1446 assert_eq!(items[0].0, index);
1447 assert_eq!(items[0].1, data);
1448
1449 let buffer = context.encode();
1451 assert!(buffer.contains("second_tracked 1"));
1452 });
1453 }
1454
1455 #[test_traced]
1456 fn test_journal_multiple_appends_and_reads() {
1457 let executor = deterministic::Runner::default();
1459
1460 executor.start(|context| async move {
1462 let cfg = Config {
1464 partition: "test-partition".into(),
1465 compression: None,
1466 codec_config: (),
1467 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1468 write_buffer: NZUsize!(1024),
1469 };
1470
1471 let mut journal = Journal::init(context.child("first"), cfg.clone())
1473 .await
1474 .expect("Failed to initialize journal");
1475
1476 let data_items = vec![(1u64, 1), (1u64, 2), (2u64, 3), (3u64, 4)];
1478 for (index, data) in &data_items {
1479 (journal, _, _) = journal
1480 .append(*index, data)
1481 .await
1482 .expect("Failed to append data");
1483 journal = journal.sync(*index).await.expect("Failed to sync blob");
1484 }
1485
1486 let buffer = context.encode();
1488 assert!(buffer.contains("first_tracked 3"));
1489 assert!(buffer.contains("first_synced_total 4"));
1490
1491 drop(journal);
1493 let mut journal = Journal::init(context.child("second"), cfg)
1494 .await
1495 .expect("Failed to re-initialize journal");
1496
1497 let mut items = Vec::<(u64, u32)>::new();
1499 {
1500 let mut replay = journal
1501 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1502 .await
1503 .expect("unable to setup replay");
1504 while let Some(result) = replay.next().await {
1505 match result {
1506 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1507 Err(err) => panic!("Failed to read item: {err}"),
1508 }
1509 }
1510 journal = replay.finish().expect("failed to finish replay");
1511 }
1512
1513 assert_eq!(items.len(), data_items.len());
1515 for ((expected_index, expected_data), (actual_index, actual_data)) in
1516 data_items.iter().zip(items.iter())
1517 {
1518 assert_eq!(actual_index, expected_index);
1519 assert_eq!(actual_data, expected_data);
1520 }
1521
1522 journal.destroy().await.expect("Failed to destroy journal");
1524 });
1525 }
1526
1527 #[test_traced]
1528 fn test_journal_prune_blobs() {
1529 let executor = deterministic::Runner::default();
1531
1532 executor.start(|context| async move {
1534 let cfg = Config {
1536 partition: "test-partition".into(),
1537 compression: None,
1538 codec_config: (),
1539 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1540 write_buffer: NZUsize!(1024),
1541 };
1542
1543 let mut journal = Journal::init(context.child("first"), cfg.clone())
1545 .await
1546 .expect("Failed to initialize journal");
1547
1548 for index in 1u64..=5u64 {
1550 (journal, _, _) = journal
1551 .append(index, &index)
1552 .await
1553 .expect("Failed to append data");
1554 journal = journal.sync(index).await.expect("Failed to sync blob");
1555 }
1556
1557 let data = 99;
1559 (journal, _, _) = journal
1560 .append(2u64, &data)
1561 .await
1562 .expect("Failed to append data");
1563 journal = journal.sync(2u64).await.expect("Failed to sync blob");
1564
1565 (journal, _) = journal.prune(3).await.expect("Failed to prune blobs");
1567
1568 let buffer = context.encode();
1570 assert!(buffer.contains("first_pruned_total 2"));
1571
1572 (journal, _) = journal.prune(2).await.expect("Failed to no-op prune");
1574 let buffer = context.encode();
1575 assert!(buffer.contains("first_pruned_total 2"));
1576
1577 drop(journal);
1579 let mut journal = Journal::init(context.child("second"), cfg.clone())
1580 .await
1581 .expect("Failed to re-initialize journal");
1582
1583 let mut items = Vec::<(u64, u64)>::new();
1585 {
1586 let mut replay = journal
1587 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1588 .await
1589 .expect("unable to setup replay");
1590 while let Some(result) = replay.next().await {
1591 match result {
1592 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1593 Err(err) => panic!("Failed to read item: {err}"),
1594 }
1595 }
1596 journal = replay.finish().expect("failed to finish replay");
1597 }
1598
1599 assert_eq!(items.len(), 3);
1601 let expected_indices = [3u64, 4u64, 5u64];
1602 for (item, expected_index) in items.iter().zip(expected_indices.iter()) {
1603 assert_eq!(item.0, *expected_index);
1604 }
1605
1606 (journal, _) = journal.prune(6).await.expect("Failed to prune blobs");
1608
1609 drop(journal);
1611
1612 assert!(
1617 context
1618 .scan(&cfg.partition)
1619 .await
1620 .expect("Failed to list blobs")
1621 .is_empty()
1622 );
1623 });
1624 }
1625
1626 #[test_traced]
1627 fn test_journal_prune_guard() {
1628 let executor = deterministic::Runner::default();
1629 executor.start(|context| async move {
1630 let cfg = Config {
1631 partition: "test-partition".into(),
1632 compression: None,
1633 codec_config: (),
1634 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1635 write_buffer: NZUsize!(1024),
1636 };
1637
1638 let mut journal = Journal::init(context.child("storage"), cfg.clone())
1639 .await
1640 .expect("Failed to initialize journal");
1641
1642 for section in 1u64..=5u64 {
1644 (journal, _, _) = journal
1645 .append(section, &(section as i32))
1646 .await
1647 .expect("Failed to append data");
1648 journal = journal.sync(section).await.expect("Failed to sync");
1649 }
1650
1651 (journal, _) = journal.prune(3).await.expect("Failed to prune");
1653
1654 assert!(journal.pruned(1));
1656 assert!(journal.pruned(2));
1657 assert!(!journal.pruned(3));
1658
1659 match journal.0.append(1, &100).await {
1663 Err(Error::AlreadyPrunedToSection(3)) => {}
1664 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1665 }
1666
1667 match journal.0.append(2, &100).await {
1668 Err(Error::AlreadyPrunedToSection(3)) => {}
1669 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1670 }
1671
1672 match journal.get(1, 0).await {
1674 Err(Error::AlreadyPrunedToSection(3)) => {}
1675 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1676 }
1677
1678 match journal.size(1) {
1680 Err(Error::AlreadyPrunedToSection(3)) => {}
1681 other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1682 }
1683
1684 assert!(journal.get(3, 0).await.is_ok());
1686 assert!(journal.get(4, 0).await.is_ok());
1687 assert!(journal.get(5, 0).await.is_ok());
1688 assert!(journal.size(3).is_ok());
1689 journal = journal
1690 .sync(4)
1691 .await
1692 .expect("Should be able to sync section 4");
1693
1694 (journal, _, _) = journal
1696 .append(3, &999)
1697 .await
1698 .expect("Should be able to append to section 3");
1699
1700 (journal, _) = journal.prune(5).await.expect("Failed to prune");
1702
1703 assert!(journal.pruned(4));
1705 assert!(!journal.pruned(5));
1706 match journal.get(3, 0).await {
1707 Err(Error::AlreadyPrunedToSection(5)) => {}
1708 other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1709 }
1710
1711 match journal.get(4, 0).await {
1712 Err(Error::AlreadyPrunedToSection(5)) => {}
1713 other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1714 }
1715
1716 assert!(journal.get(5, 0).await.is_ok());
1718
1719 match journal.sync(4).await {
1721 Err(Error::AlreadyPrunedToSection(5)) => {}
1722 other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1723 }
1724 });
1725 }
1726
1727 #[test_traced]
1728 fn test_journal_prune_guard_across_restart() {
1729 let executor = deterministic::Runner::default();
1730 executor.start(|context| async move {
1731 let cfg = Config {
1732 partition: "test-partition".into(),
1733 compression: None,
1734 codec_config: (),
1735 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1736 write_buffer: NZUsize!(1024),
1737 };
1738
1739 {
1741 let mut journal = Journal::init(context.child("first"), cfg.clone())
1742 .await
1743 .expect("Failed to initialize journal");
1744
1745 for section in 1u64..=5u64 {
1746 (journal, _, _) = journal
1747 .append(section, &(section as i32))
1748 .await
1749 .expect("Failed to append data");
1750 journal = journal.sync(section).await.expect("Failed to sync");
1751 }
1752
1753 journal.prune(3).await.expect("Failed to prune");
1754 }
1755
1756 {
1758 let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
1759 .await
1760 .expect("Failed to re-initialize journal");
1761
1762 assert!(!journal.pruned(1));
1764 assert!(!journal.pruned(2));
1765
1766 match journal.get(1, 0).await {
1769 Err(Error::SectionOutOfRange(1)) => {}
1770 other => panic!("Expected SectionOutOfRange(1), got {other:?}"),
1771 }
1772
1773 match journal.get(2, 0).await {
1774 Err(Error::SectionOutOfRange(2)) => {}
1775 other => panic!("Expected SectionOutOfRange(2), got {other:?}"),
1776 }
1777
1778 assert!(journal.get(3, 0).await.is_ok());
1780 assert!(journal.get(4, 0).await.is_ok());
1781 assert!(journal.get(5, 0).await.is_ok());
1782 }
1783 });
1784 }
1785
1786 #[test_traced]
1787 fn test_journal_with_invalid_blob_name() {
1788 let executor = deterministic::Runner::default();
1790
1791 executor.start(|context| async move {
1793 let cfg = Config {
1795 partition: "test-partition".into(),
1796 compression: None,
1797 codec_config: (),
1798 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1799 write_buffer: NZUsize!(1024),
1800 };
1801
1802 let invalid_blob_name = b"invalid"; let (blob, _) = context
1805 .open(&cfg.partition, invalid_blob_name)
1806 .await
1807 .expect("Failed to create blob with invalid name");
1808 blob.sync().await.expect("Failed to sync blob");
1809
1810 drop(blob);
1812 let result = Journal::<_, u64>::init(context, cfg).await;
1813
1814 assert!(matches!(result, Err(Error::InvalidBlobName(_))));
1816 });
1817 }
1818
1819 #[test_traced]
1820 fn test_journal_read_size_missing() {
1821 let executor = deterministic::Runner::default();
1823
1824 executor.start(|context| async move {
1826 let cfg = Config {
1828 partition: "test-partition".into(),
1829 compression: None,
1830 codec_config: (),
1831 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1832 write_buffer: NZUsize!(1024),
1833 };
1834
1835 let section = 1u64;
1837 let blob_name = section.to_be_bytes();
1838 let (blob, _) = context
1839 .open(&cfg.partition, &blob_name)
1840 .await
1841 .expect("Failed to create blob");
1842
1843 let mut incomplete_data = Vec::new();
1845 UInt(u32::MAX).write(&mut incomplete_data);
1846 incomplete_data.truncate(1);
1847 blob.write_at(0, incomplete_data, WriteOptions::SYNC)
1848 .await
1849 .expect("Failed to write incomplete data");
1850
1851 drop(blob);
1853 let journal = Journal::init(context, cfg)
1854 .await
1855 .expect("Failed to initialize journal");
1856
1857 let mut replay = journal
1859 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1860 .await
1861 .expect("unable to setup replay");
1862 let mut items = Vec::<(u64, u64)>::new();
1863 while let Some(result) = replay.next().await {
1864 match result {
1865 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1866 Err(err) => panic!("Failed to read item: {err}"),
1867 }
1868 }
1869 assert!(items.is_empty());
1870 });
1871 }
1872
1873 #[test_traced]
1874 fn test_journal_replay_empty_finishes_immediately() {
1875 let executor = deterministic::Runner::default();
1876 executor.start(|context| async move {
1877 let cfg = Config {
1878 partition: "test-partition".into(),
1879 compression: None,
1880 codec_config: (),
1881 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1882 write_buffer: NZUsize!(1024),
1883 };
1884 let journal = Journal::<_, i32>::init(context.child("storage"), cfg)
1885 .await
1886 .expect("Failed to initialize journal");
1887
1888 let replay = journal
1890 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1891 .await
1892 .expect("Failed to replay");
1893 let journal = replay.finish().expect("failed to finish replay");
1894 journal.destroy().await.expect("Failed to destroy");
1895 });
1896 }
1897
1898 #[test_traced]
1899 fn test_journal_replay_finish_before_drain_fails() {
1900 let executor = deterministic::Runner::default();
1901 executor.start(|context| async move {
1902 let cfg = Config {
1903 partition: "test-partition".into(),
1904 compression: None,
1905 codec_config: (),
1906 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1907 write_buffer: NZUsize!(1024),
1908 };
1909 let mut journal = Journal::init(context.child("storage"), cfg)
1910 .await
1911 .expect("Failed to initialize journal");
1912 (journal, _, _) = journal.append(1, &7i32).await.expect("Failed to append");
1913 journal = journal.sync(1).await.expect("Failed to sync");
1914
1915 let replay = journal
1916 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1917 .await
1918 .expect("Failed to replay");
1919 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
1920 });
1921 }
1922
1923 #[test_traced]
1924 fn test_journal_replay_reports_resize_error_on_trailing_bytes() {
1925 let executor = deterministic::Runner::default();
1926 executor.start(|context| async move {
1927 let cfg = Config {
1928 partition: "test-partition".into(),
1929 compression: None,
1930 codec_config: (),
1931 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1932 write_buffer: NZUsize!(1024),
1933 };
1934
1935 let section = 1u64;
1938 let item = [10u8; 1021];
1939 let item_record_size =
1940 UInt(item.encode_size() as u32).encode_size() + item.encode_size();
1941 assert_eq!(item_record_size, PAGE_SIZE.get() as usize - 1);
1942
1943 let mut journal = Journal::init(context.child("first"), cfg.clone())
1944 .await
1945 .expect("Failed to initialize journal");
1946 (journal, _, _) = journal
1947 .append(section, &item)
1948 .await
1949 .expect("Failed to append item");
1950 journal
1951 .0
1952 .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
1953 .await
1954 .expect("Failed to append trailing bytes");
1955 journal = journal.sync(section).await.expect("Failed to sync journal");
1956 drop(journal);
1957
1958 let journal = Journal::init(context.child("second"), cfg)
1959 .await
1960 .expect("Failed to re-initialize journal");
1961 *context.storage_fault_config().write() = deterministic::FaultConfig {
1962 resize_rate: Some(deterministic::ResizeConfig {
1963 failure_rate: probability!(1.0),
1964 partial_rate: probability!(0.0),
1965 }),
1966 ..Default::default()
1967 };
1968
1969 let mut replay = journal
1970 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1971 .await
1972 .expect("unable to setup replay");
1973
1974 let first = replay
1975 .next()
1976 .await
1977 .expect("expected item before trailing bytes")
1978 .expect("failed to replay valid item");
1979 assert_eq!(first, (section, 0, item.encode_size() as u32, item));
1980
1981 match replay.next().await {
1983 Some(Err(_)) => {}
1984 other => {
1985 panic!("expected resize error while repairing trailing bytes, got {other:?}")
1986 }
1987 }
1988 assert!(replay.next().await.is_none());
1989 });
1990 }
1991
1992 #[test_traced]
1993 fn test_journal_replay_finish_after_error_fails() {
1994 let executor = deterministic::Runner::default();
1995 executor.start(|context| async move {
1996 let cfg = Config {
1997 partition: "test-partition".into(),
1998 compression: None,
1999 codec_config: (),
2000 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2001 write_buffer: NZUsize!(1024),
2002 };
2003
2004 let section = 1u64;
2007 let item = [10u8; 1021];
2008 let mut journal = Journal::init(context.child("first"), cfg.clone())
2009 .await
2010 .expect("Failed to initialize journal");
2011 (journal, _, _) = journal
2012 .append(section, &item)
2013 .await
2014 .expect("Failed to append item");
2015 journal
2016 .0
2017 .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
2018 .await
2019 .expect("Failed to append trailing bytes");
2020 journal = journal.sync(section).await.expect("Failed to sync journal");
2021 drop(journal);
2022
2023 let journal = Journal::<_, [u8; 1021]>::init(context.child("second"), cfg)
2024 .await
2025 .expect("Failed to re-initialize journal");
2026 *context.storage_fault_config().write() = deterministic::FaultConfig {
2027 resize_rate: Some(deterministic::ResizeConfig {
2028 failure_rate: probability!(1.0),
2029 partial_rate: probability!(0.0),
2030 }),
2031 ..Default::default()
2032 };
2033
2034 let mut replay = journal
2035 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2036 .await
2037 .expect("unable to setup replay");
2038 let _ = replay
2039 .next()
2040 .await
2041 .expect("expected item before trailing bytes")
2042 .expect("failed to replay valid item");
2043 assert!(matches!(replay.next().await, Some(Err(_))));
2044 assert!(replay.next().await.is_none());
2045
2046 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2048 });
2049 }
2050
2051 #[test_traced]
2052 fn test_journal_replay_dropped_during_repair_fails_replay() {
2053 let executor = deterministic::Runner::default();
2054 executor.start(|context| async move {
2055 let cfg = Config {
2056 partition: "test-partition".into(),
2057 compression: None,
2058 codec_config: (),
2059 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2060 write_buffer: NZUsize!(1024),
2061 };
2062
2063 let section = 1u64;
2066 let item = [10u8; 1021];
2067 let mut journal = Journal::init(context.child("first"), cfg.clone())
2068 .await
2069 .expect("Failed to initialize journal");
2070 (journal, _, _) = journal
2071 .append(section, &item)
2072 .await
2073 .expect("Failed to append item");
2074 journal
2075 .0
2076 .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
2077 .await
2078 .expect("Failed to append trailing bytes");
2079 journal = journal.sync(section).await.expect("Failed to sync journal");
2080 drop(journal);
2081
2082 let pending = PendingSyncs::default();
2084 let gated = DelayedSyncContext {
2085 inner: context.child("second"),
2086 pending: pending.clone(),
2087 };
2088 let journal = Journal::<_, [u8; 1021]>::init(gated, cfg.clone())
2089 .await
2090 .expect("Failed to re-initialize journal");
2091 let mut replay = journal
2092 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2093 .await
2094 .expect("unable to setup replay");
2095 let _ = replay
2096 .next()
2097 .await
2098 .expect("expected item before trailing bytes")
2099 .expect("failed to replay valid item");
2100 pending.arm();
2101 {
2102 let fut = replay.next();
2103 futures::pin_mut!(fut);
2104 assert!(
2105 futures::poll!(fut.as_mut()).is_pending(),
2106 "repair must suspend on the gated sync"
2107 );
2108 }
2109 release_pending_syncs(&pending);
2110
2111 assert!(matches!(
2113 replay.next().await,
2114 Some(Err(Error::ReplayInterrupted))
2115 ));
2116 assert!(replay.next().await.is_none());
2117 drop(replay);
2118
2119 let journal = Journal::<_, [u8; 1021]>::init(context.child("third"), cfg)
2121 .await
2122 .expect("Failed to re-initialize journal");
2123 let mut replay = journal
2124 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2125 .await
2126 .expect("unable to setup replay");
2127 let first = replay
2128 .next()
2129 .await
2130 .expect("expected item after recovery")
2131 .expect("failed to replay valid item");
2132 assert_eq!(first, (section, 0, item.encode_size() as u32, item));
2133 assert!(replay.next().await.is_none());
2134 let journal = replay.finish().expect("failed to finish replay");
2135 journal.destroy().await.expect("Failed to destroy");
2136 });
2137 }
2138
2139 #[test_traced]
2140 fn test_journal_read_item_missing() {
2141 let executor = deterministic::Runner::default();
2143
2144 executor.start(|context| async move {
2146 let cfg = Config {
2148 partition: "test-partition".into(),
2149 compression: None,
2150 codec_config: (),
2151 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2152 write_buffer: NZUsize!(1024),
2153 };
2154
2155 let section = 1u64;
2157 let blob_name = section.to_be_bytes();
2158 let (blob, _) = context
2159 .open(&cfg.partition, &blob_name)
2160 .await
2161 .expect("Failed to create blob");
2162
2163 let item_size: u32 = 10; let mut buf = Vec::new();
2166 UInt(item_size).write(&mut buf); let data = [2u8; 5];
2168 BufMut::put_slice(&mut buf, &data);
2169 blob.write_at(0, buf, WriteOptions::SYNC)
2170 .await
2171 .expect("Failed to write incomplete item");
2172
2173 drop(blob);
2175 let journal = Journal::init(context, cfg)
2176 .await
2177 .expect("Failed to initialize journal");
2178
2179 let mut replay = journal
2181 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2182 .await
2183 .expect("unable to setup replay");
2184 let mut items = Vec::<(u64, u64)>::new();
2185 while let Some(result) = replay.next().await {
2186 match result {
2187 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2188 Err(err) => panic!("Failed to read item: {err}"),
2189 }
2190 }
2191 assert!(items.is_empty());
2192 });
2193 }
2194
2195 #[test_traced]
2196 fn test_journal_read_checksum_missing() {
2197 let executor = deterministic::Runner::default();
2199
2200 executor.start(|context| async move {
2202 let cfg = Config {
2204 partition: "test-partition".into(),
2205 compression: None,
2206 codec_config: (),
2207 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2208 write_buffer: NZUsize!(1024),
2209 };
2210
2211 let section = 1u64;
2213 let blob_name = section.to_be_bytes();
2214 let (blob, _) = context
2215 .open(&cfg.partition, &blob_name)
2216 .await
2217 .expect("Failed to create blob");
2218
2219 let item_data = b"Test data";
2221 let item_size = item_data.len() as u32;
2222
2223 let mut buf = Vec::new();
2225 UInt(item_size).write(&mut buf);
2226 BufMut::put_slice(&mut buf, item_data);
2227 blob.write_at(0, buf, WriteOptions::SYNC)
2228 .await
2229 .expect("Failed to write item without checksum");
2230
2231 drop(blob);
2233 let journal = Journal::init(context, cfg)
2234 .await
2235 .expect("Failed to initialize journal");
2236
2237 let mut replay = journal
2241 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2242 .await
2243 .expect("unable to setup replay");
2244 let mut items = Vec::<(u64, u64)>::new();
2245 while let Some(result) = replay.next().await {
2246 match result {
2247 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2248 Err(err) => panic!("Failed to read item: {err}"),
2249 }
2250 }
2251 assert!(items.is_empty());
2252 });
2253 }
2254
2255 #[test_traced]
2256 fn test_journal_read_checksum_mismatch() {
2257 let executor = deterministic::Runner::default();
2259
2260 executor.start(|context| async move {
2262 let cfg = Config {
2264 partition: "test-partition".into(),
2265 compression: None,
2266 codec_config: (),
2267 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2268 write_buffer: NZUsize!(1024),
2269 };
2270
2271 let section = 1u64;
2273 let blob_name = section.to_be_bytes();
2274 let (blob, _) = context
2275 .open(&cfg.partition, &blob_name)
2276 .await
2277 .expect("Failed to create blob");
2278
2279 let item_data = b"Test data";
2281 let item_size = item_data.len() as u32;
2282 let incorrect_checksum: u32 = 0xDEADBEEF;
2283
2284 let mut buf = Vec::new();
2286 UInt(item_size).write(&mut buf);
2287 BufMut::put_slice(&mut buf, item_data);
2288 buf.put_u32(incorrect_checksum);
2289 blob.write_at(0, buf, WriteOptions::SYNC)
2290 .await
2291 .expect("Failed to write item with bad checksum");
2292
2293 drop(blob);
2295 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2296 .await
2297 .expect("Failed to initialize journal");
2298
2299 {
2301 let mut replay = journal
2302 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2303 .await
2304 .expect("unable to setup replay");
2305 let mut items = Vec::<(u64, u64)>::new();
2306 while let Some(result) = replay.next().await {
2307 match result {
2308 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2309 Err(err) => panic!("Failed to read item: {err}"),
2310 }
2311 }
2312 journal = replay.finish().expect("failed to finish replay");
2313 assert!(items.is_empty());
2314 }
2315 drop(journal);
2316
2317 let (_, blob_size) = context
2319 .open(&cfg.partition, §ion.to_be_bytes())
2320 .await
2321 .expect("Failed to open blob");
2322 assert_eq!(blob_size, 0);
2323 });
2324 }
2325
2326 #[test_traced]
2327 fn test_segmented_variable_replay_repairs_torn_interior_page_when_reached() {
2328 let executor = deterministic::Runner::default();
2329 executor.start(|context| async move {
2330 const FIRST_SECTION: u64 = 0;
2331 const PARTITION: &str = "segmented-variable-torn-interior";
2332 const TORN_SECTION: u64 = 1;
2333
2334 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
2343 let original_size = context
2344 .logical_blob(PARTITION, &TORN_SECTION.to_be_bytes())
2345 .unwrap()
2346 .len();
2347 let mut replay = journal
2348 .replay(FIRST_SECTION, 0, NZUsize!(1024), ReadOptions::default())
2349 .await
2350 .unwrap();
2351
2352 let size = context
2353 .logical_blob(PARTITION, &TORN_SECTION.to_be_bytes())
2354 .unwrap()
2355 .len();
2356 assert_eq!(
2357 size, original_size,
2358 "replay setup must not repair a later section"
2359 );
2360
2361 let (section, offset, _, value) = replay.next().await.unwrap().unwrap();
2362 assert_eq!((section, offset, value), (FIRST_SECTION, 0, u64::MAX));
2363 let size = context
2364 .logical_blob(PARTITION, &TORN_SECTION.to_be_bytes())
2365 .unwrap()
2366 .len();
2367 assert_eq!(
2368 size, original_size,
2369 "consuming an earlier section must not repair a later section"
2370 );
2371
2372 let mut values = Vec::new();
2373 while let Some(result) = replay.next().await {
2374 let (section, offset, _, value) = result.unwrap();
2375 assert_eq!(section, TORN_SECTION);
2376 assert_eq!(offset, value * 9);
2377 values.push(value);
2378 }
2379 assert_eq!(values, (0..7).collect::<Vec<_>>());
2380
2381 let journal = replay.finish().unwrap();
2382 assert_eq!(journal.size(TORN_SECTION).unwrap(), 63);
2383 let (journal, offset, _) = journal.append(TORN_SECTION, &7).await.unwrap();
2384 assert_eq!(offset, 63);
2385 journal.destroy().await.unwrap();
2386 });
2387 }
2388
2389 #[test_traced]
2390 fn test_segmented_variable_replay_rejects_start_beyond_torn_prefix_without_repair() {
2391 let executor = deterministic::Runner::default();
2392 executor.start(|context| async move {
2393 const PARTITION: &str = "segmented-variable-torn-start";
2394 const SECTION: u64 = 1;
2395 const START_OFFSET: u64 = 72;
2396
2397 let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
2398 let original_size = context
2399 .logical_blob(PARTITION, &SECTION.to_be_bytes())
2400 .unwrap()
2401 .len();
2402 let mut replay = journal
2403 .replay(
2404 SECTION,
2405 START_OFFSET,
2406 NZUsize!(1024),
2407 ReadOptions::default(),
2408 )
2409 .await
2410 .expect("apparent tail still covers the requested start");
2411
2412 assert!(matches!(
2413 replay.next().await,
2414 Some(Err(Error::ItemOutOfRange(START_OFFSET)))
2415 ));
2416 let size = context
2417 .logical_blob(PARTITION, &SECTION.to_be_bytes())
2418 .unwrap()
2419 .len();
2420 assert_eq!(
2421 size, original_size,
2422 "an unvalidated start offset must not become a repair boundary"
2423 );
2424 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2425 });
2426 }
2427
2428 #[test_traced]
2429 fn test_segmented_variable_replay_stops_after_failed_interior_repair() {
2430 let executor = deterministic::Runner::default();
2431 executor.start(|context| async move {
2432 let journal = journal_with_torn_interior_page(
2433 &context,
2434 "segmented-variable-failed-interior-repair",
2435 true,
2436 )
2437 .await;
2438 *context.storage_fault_config().write() = deterministic::FaultConfig {
2439 resize_rate: Some(deterministic::ResizeConfig {
2440 failure_rate: probability!(1.0),
2441 partial_rate: probability!(0.0),
2442 }),
2443 ..Default::default()
2444 };
2445
2446 let mut replay = journal
2449 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2450 .await
2451 .unwrap();
2452 assert!(matches!(replay.next().await, Some(Ok((0, 0, _, u64::MAX)))));
2453 assert!(matches!(replay.next().await, Some(Err(Error::Runtime(_)))));
2454 assert!(replay.next().await.is_none());
2455 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2456 });
2457 }
2458
2459 #[test_traced]
2460 fn test_segmented_variable_replay_stops_after_failed_tail_repair() {
2461 let executor = deterministic::Runner::default();
2462 executor.start(|context| async move {
2463 let journal = journal_with_torn_interior_page(
2464 &context,
2465 "segmented-variable-failed-tail-repair",
2466 true,
2467 )
2468 .await;
2469 let mut replay = journal
2470 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2471 .await
2472 .unwrap();
2473
2474 assert!(matches!(replay.next().await, Some(Ok((0, 0, _, u64::MAX)))));
2475 for expected in 0..7 {
2476 let (section, offset, _, value) = replay.next().await.unwrap().unwrap();
2477 assert_eq!((section, offset, value), (1, expected * 9, expected));
2478 }
2479
2480 *context.storage_fault_config().write() = deterministic::FaultConfig {
2481 write_rate: Some(deterministic::WriteConfig {
2482 failure_rate: probability!(1.0),
2483 retention_rate: probability!(1.0),
2484 mode: deterministic::PartialWriteMode::Prefix,
2485 }),
2486 ..Default::default()
2487 };
2488 assert!(matches!(replay.next().await, Some(Err(Error::Runtime(_)))));
2489 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
2490
2491 assert!(replay.next().await.is_none());
2492 assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2493 });
2494 }
2495
2496 #[test_traced]
2497 fn test_journal_truncation_recovery() {
2498 let executor = deterministic::Runner::default();
2500
2501 executor.start(|context| async move {
2503 let cfg = Config {
2505 partition: "test-partition".into(),
2506 compression: None,
2507 codec_config: (),
2508 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2509 write_buffer: NZUsize!(1024),
2510 };
2511
2512 let mut journal = Journal::init(context.child("first"), cfg.clone())
2514 .await
2515 .expect("Failed to initialize journal");
2516
2517 (journal, _, _) = journal.append(1, &1).await.expect("Failed to append data");
2519
2520 let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
2522 for (index, data) in &data_items {
2523 (journal, _, _) = journal
2524 .append(*index, data)
2525 .await
2526 .expect("Failed to append data");
2527 journal = journal.sync(*index).await.expect("Failed to sync blob");
2528 }
2529
2530 journal = journal.sync_all().await.expect("Failed to sync");
2532 drop(journal);
2533
2534 let (blob, blob_size) = context
2536 .open(&cfg.partition, &2u64.to_be_bytes())
2537 .await
2538 .expect("Failed to open blob");
2539 blob.resize(blob_size - 4)
2540 .await
2541 .expect("Failed to corrupt blob");
2542 blob.sync().await.expect("Failed to sync blob");
2543
2544 drop(blob);
2546 let mut journal = Journal::init(context.child("second"), cfg.clone())
2547 .await
2548 .expect("Failed to re-initialize journal");
2549
2550 let mut items = Vec::<(u64, u32)>::new();
2552 {
2553 let mut replay = journal
2554 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2555 .await
2556 .expect("unable to setup replay");
2557 while let Some(result) = replay.next().await {
2558 match result {
2559 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2560 Err(err) => panic!("Failed to read item: {err}"),
2561 }
2562 }
2563 journal = replay.finish().expect("failed to finish replay");
2564 }
2565 drop(journal);
2566
2567 assert_eq!(items.len(), 1);
2569 assert_eq!(items[0].0, 1);
2570 assert_eq!(items[0].1, 1);
2571
2572 let (_, blob_size) = context
2574 .open(&cfg.partition, &2u64.to_be_bytes())
2575 .await
2576 .expect("Failed to open blob");
2577 assert_eq!(blob_size, 0);
2578
2579 let mut journal = Journal::init(context.child("third"), cfg.clone())
2581 .await
2582 .expect("Failed to re-initialize journal");
2583
2584 let mut items = Vec::<(u64, u32)>::new();
2586 {
2587 let mut replay = journal
2588 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2589 .await
2590 .expect("unable to setup replay");
2591 while let Some(result) = replay.next().await {
2592 match result {
2593 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2594 Err(err) => panic!("Failed to read item: {err}"),
2595 }
2596 }
2597 journal = replay.finish().expect("failed to finish replay");
2598 }
2599
2600 assert_eq!(items.len(), 1);
2602 assert_eq!(items[0].0, 1);
2603 assert_eq!(items[0].1, 1);
2604
2605 (journal, _, _) = journal.append(2, &5).await.expect("Failed to append data");
2607 journal = journal.sync(2).await.expect("Failed to sync blob");
2608
2609 let item = journal.get(2, 0).await.expect("Failed to get item");
2611 assert_eq!(item, 5);
2612
2613 drop(journal);
2615
2616 let journal = Journal::init(context.child("storage"), cfg.clone())
2618 .await
2619 .expect("Failed to re-initialize journal");
2620
2621 let mut items = Vec::<(u64, u32)>::new();
2623 {
2624 let mut replay = journal
2625 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2626 .await
2627 .expect("unable to setup replay");
2628 while let Some(result) = replay.next().await {
2629 match result {
2630 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2631 Err(err) => panic!("Failed to read item: {err}"),
2632 }
2633 }
2634 }
2635
2636 assert_eq!(items.len(), 2);
2638 assert_eq!(items[0].0, 1);
2639 assert_eq!(items[0].1, 1);
2640 assert_eq!(items[1].0, 2);
2641 assert_eq!(items[1].1, 5);
2642 });
2643 }
2644
2645 #[test_traced]
2646 fn test_journal_handling_extra_data() {
2647 let executor = deterministic::Runner::default();
2649
2650 executor.start(|context| async move {
2652 let cfg = Config {
2654 partition: "test-partition".into(),
2655 compression: None,
2656 codec_config: (),
2657 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2658 write_buffer: NZUsize!(1024),
2659 };
2660
2661 let mut journal = Journal::init(context.child("first"), cfg.clone())
2663 .await
2664 .expect("Failed to initialize journal");
2665
2666 (journal, _, _) = journal.append(1, &1).await.expect("Failed to append data");
2668
2669 let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
2671 for (index, data) in &data_items {
2672 (journal, _, _) = journal
2673 .append(*index, data)
2674 .await
2675 .expect("Failed to append data");
2676 journal = journal.sync(*index).await.expect("Failed to sync blob");
2677 }
2678
2679 journal = journal.sync_all().await.expect("Failed to sync");
2681 drop(journal);
2682
2683 let (blob, blob_size) = context
2685 .open(&cfg.partition, &2u64.to_be_bytes())
2686 .await
2687 .expect("Failed to open blob");
2688 blob.write_at(blob_size, vec![0u8; 16], WriteOptions::SYNC)
2689 .await
2690 .expect("Failed to add extra data");
2691
2692 drop(blob);
2694 let journal = Journal::init(context.child("second"), cfg)
2695 .await
2696 .expect("Failed to re-initialize journal");
2697
2698 let mut items = Vec::<(u64, i32)>::new();
2700 let mut replay = journal
2701 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2702 .await
2703 .expect("unable to setup replay");
2704 while let Some(result) = replay.next().await {
2705 match result {
2706 Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2707 Err(err) => panic!("Failed to read item: {err}"),
2708 }
2709 }
2710 });
2711 }
2712
2713 #[test_traced]
2714 fn test_journal_truncate() {
2715 let executor = deterministic::Runner::default();
2717 executor.start(|context| async move {
2718 let cfg = Config {
2720 partition: "test-partition".into(),
2721 compression: None,
2722 codec_config: (),
2723 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2724 write_buffer: NZUsize!(1024),
2725 };
2726 let mut journal = Journal::init(context, cfg).await.unwrap();
2727
2728 let size = journal.size(1).unwrap();
2730 assert_eq!(size, 0);
2731
2732 (journal, _, _) = journal.append(1, &42i32).await.unwrap();
2734
2735 let size = journal.size(1).unwrap();
2737 assert!(size > 0);
2738
2739 (journal, _, _) = journal.append(1, &43i32).await.unwrap();
2741 let new_size = journal.size(1).unwrap();
2742 assert!(new_size > size);
2743
2744 let size = journal.size(2).unwrap();
2746 assert_eq!(size, 0);
2747
2748 (journal, _, _) = journal.append(2, &44i32).await.unwrap();
2750
2751 let size = journal.size(2).unwrap();
2753 assert!(size > 0);
2754
2755 journal = journal.test_reopen_at_most(1, 0).await.unwrap();
2757
2758 let size = journal.size(1).unwrap();
2760 assert_eq!(size, 0);
2761
2762 let size = journal.size(2).unwrap();
2764 assert_eq!(size, 0);
2765 });
2766 }
2767
2768 #[test_traced]
2769 fn test_journal_truncate_max_section() {
2770 let executor = deterministic::Runner::default();
2771 executor.start(|context| async move {
2772 let cfg = Config {
2773 partition: "test-partition".into(),
2774 compression: None,
2775 codec_config: (),
2776 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2777 write_buffer: NZUsize!(1024),
2778 };
2779 let mut journal = Journal::init(context, cfg).await.unwrap();
2780
2781 let offset;
2783 (journal, offset, _) = journal.append(u64::MAX, &42i32).await.unwrap();
2784 let size = journal.size(u64::MAX).unwrap();
2785 assert!(size > 0);
2786
2787 journal = journal.test_reopen_at_most(u64::MAX, size).await.unwrap();
2789
2790 assert_eq!(journal.size(u64::MAX).unwrap(), size);
2792 assert_eq!(journal.get(u64::MAX, offset).await.unwrap(), 42i32);
2793 });
2794 }
2795
2796 #[test_traced]
2797 fn test_journal_truncate_section() {
2798 let executor = deterministic::Runner::default();
2800 executor.start(|context| async move {
2801 let cfg = Config {
2803 partition: "test-partition".into(),
2804 compression: None,
2805 codec_config: (),
2806 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2807 write_buffer: NZUsize!(1024),
2808 };
2809 let mut journal = Journal::init(context, cfg).await.unwrap();
2810
2811 let size = journal.size(1).unwrap();
2813 assert_eq!(size, 0);
2814
2815 (journal, _, _) = journal.append(1, &42i32).await.unwrap();
2817
2818 let size = journal.size(1).unwrap();
2820 assert!(size > 0);
2821
2822 (journal, _, _) = journal.append(1, &43i32).await.unwrap();
2824 let new_size = journal.size(1).unwrap();
2825 assert!(new_size > size);
2826
2827 let size = journal.size(2).unwrap();
2829 assert_eq!(size, 0);
2830
2831 (journal, _, _) = journal.append(2, &44i32).await.unwrap();
2833
2834 let size = journal.size(2).unwrap();
2836 assert!(size > 0);
2837
2838 journal = journal.test_reopen_section(1, 0).await.unwrap();
2840
2841 let size = journal.size(1).unwrap();
2843 assert_eq!(size, 0);
2844
2845 let size = journal.size(2).unwrap();
2847 assert!(size > 0);
2848 });
2849 }
2850
2851 #[test_traced]
2852 fn test_journal_small_items() {
2853 let executor = deterministic::Runner::default();
2854 executor.start(|context| async move {
2855 let cfg = Config {
2856 partition: "test-partition".into(),
2857 compression: None,
2858 codec_config: (),
2859 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2860 write_buffer: NZUsize!(1024),
2861 };
2862
2863 let mut journal = Journal::init(context.child("first"), cfg.clone())
2864 .await
2865 .expect("Failed to initialize journal");
2866
2867 let num_items = 100;
2869 let mut offsets = Vec::new();
2870 for i in 0..num_items {
2871 let offset;
2872 let size;
2873 (journal, offset, size) = journal
2874 .append(1, &(i as u8))
2875 .await
2876 .expect("Failed to append data");
2877 assert_eq!(size, 1, "u8 should encode to 1 byte");
2878 offsets.push(offset);
2879 }
2880 journal = journal.sync(1).await.expect("Failed to sync");
2881
2882 for (i, &offset) in offsets.iter().enumerate() {
2884 let item: u8 = journal.get(1, offset).await.expect("Failed to get item");
2885 assert_eq!(item, i as u8, "Item mismatch at offset {offset}");
2886 }
2887
2888 drop(journal);
2890 let journal = Journal::<_, u8>::init(context.child("second"), cfg)
2891 .await
2892 .expect("Failed to re-initialize journal");
2893
2894 let mut replay = journal
2896 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2897 .await
2898 .expect("Failed to setup replay");
2899
2900 let mut count = 0;
2901 while let Some(result) = replay.next().await {
2902 let (section, offset, size, item) = result.expect("Failed to replay item");
2903 assert_eq!(section, 1);
2904 assert_eq!(offset, offsets[count]);
2905 assert_eq!(size, 1);
2906 assert_eq!(item, count as u8);
2907 count += 1;
2908 }
2909 assert_eq!(count, num_items, "Should replay all items");
2910 });
2911 }
2912
2913 #[test_traced]
2914 fn test_journal_truncate_many_sections() {
2915 let executor = deterministic::Runner::default();
2916 executor.start(|context| async move {
2917 let cfg = Config {
2918 partition: "test-partition".into(),
2919 compression: None,
2920 codec_config: (),
2921 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2922 write_buffer: NZUsize!(1024),
2923 };
2924 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2925 .await
2926 .unwrap();
2927
2928 for section in 1u64..=10 {
2930 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
2931 }
2932 journal = journal.sync_all().await.unwrap();
2933
2934 for section in 1u64..=10 {
2936 let size = journal.size(section).unwrap();
2937 assert!(size > 0, "section {section} should have data");
2938 }
2939
2940 let size = journal.size(5).unwrap();
2942 journal = journal.test_reopen_at_most(5, size).await.unwrap();
2943
2944 for section in 1u64..=5 {
2946 let size = journal.size(section).unwrap();
2947 assert!(size > 0, "section {section} should still have data");
2948 }
2949
2950 for section in 6u64..=10 {
2952 let size = journal.size(section).unwrap();
2953 assert_eq!(size, 0, "section {section} should be removed");
2954 }
2955
2956 {
2958 let mut replay = journal
2959 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2960 .await
2961 .unwrap();
2962 let mut items = Vec::new();
2963 while let Some(result) = replay.next().await {
2964 let (section, _, _, item) = result.unwrap();
2965 items.push((section, item));
2966 }
2967 journal = replay.finish().expect("failed to finish replay");
2968 assert_eq!(items.len(), 5);
2969 for (i, (section, item)) in items.iter().enumerate() {
2970 assert_eq!(*section, (i + 1) as u64);
2971 assert_eq!(*item, (i + 1) as i32);
2972 }
2973 }
2974
2975 journal.destroy().await.unwrap();
2976 });
2977 }
2978
2979 #[test_traced]
2980 fn test_journal_truncate_partial_truncation() {
2981 let executor = deterministic::Runner::default();
2982 executor.start(|context| async move {
2983 let cfg = Config {
2984 partition: "test-partition".into(),
2985 compression: None,
2986 codec_config: (),
2987 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2988 write_buffer: NZUsize!(1024),
2989 };
2990 let mut journal = Journal::init(context.child("storage"), cfg.clone())
2991 .await
2992 .unwrap();
2993
2994 let mut sizes = Vec::new();
2996 for i in 0..5 {
2997 (journal, _, _) = journal.append(1, &i).await.unwrap();
2998 journal = journal.sync(1).await.unwrap();
2999 sizes.push(journal.size(1).unwrap());
3000 }
3001
3002 let target_size = sizes[2];
3004 journal = journal.test_reopen_at_most(1, target_size).await.unwrap();
3005
3006 let new_size = journal.size(1).unwrap();
3008 assert_eq!(new_size, target_size);
3009
3010 {
3012 let mut replay = journal
3013 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3014 .await
3015 .unwrap();
3016 let mut items = Vec::new();
3017 while let Some(result) = replay.next().await {
3018 let (_, _, _, item) = result.unwrap();
3019 items.push(item);
3020 }
3021 journal = replay.finish().expect("failed to finish replay");
3022 assert_eq!(items.len(), 3);
3023 for (i, item) in items.iter().enumerate() {
3024 assert_eq!(*item, i as i32);
3025 }
3026 }
3027
3028 journal.destroy().await.unwrap();
3029 });
3030 }
3031
3032 #[test_traced]
3033 fn test_journal_truncate_nonexistent_target() {
3034 let executor = deterministic::Runner::default();
3035 executor.start(|context| async move {
3036 let cfg = Config {
3037 partition: "test-partition".into(),
3038 compression: None,
3039 codec_config: (),
3040 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3041 write_buffer: NZUsize!(1024),
3042 };
3043 let mut journal = Journal::init(context.child("storage"), cfg.clone())
3044 .await
3045 .unwrap();
3046
3047 for section in 5u64..=7 {
3049 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
3050 }
3051 journal = journal.sync_all().await.unwrap();
3052
3053 journal = journal.test_reopen_at_most(3, 0).await.unwrap();
3055
3056 for section in 5u64..=7 {
3058 let size = journal.size(section).unwrap();
3059 assert_eq!(size, 0, "section {section} should be removed");
3060 }
3061
3062 {
3064 let mut replay = journal
3065 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3066 .await
3067 .unwrap();
3068 assert!(replay.next().await.is_none());
3069 journal = replay.finish().expect("failed to finish replay");
3070 }
3071
3072 journal.destroy().await.unwrap();
3073 });
3074 }
3075
3076 #[test_traced]
3077 fn test_journal_truncate_persistence() {
3078 let executor = deterministic::Runner::default();
3079 executor.start(|context| async move {
3080 let cfg = Config {
3081 partition: "test-partition".into(),
3082 compression: None,
3083 codec_config: (),
3084 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3085 write_buffer: NZUsize!(1024),
3086 };
3087
3088 let mut journal = Journal::init(context.child("first"), cfg.clone())
3090 .await
3091 .unwrap();
3092 for section in 1u64..=5 {
3093 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
3094 }
3095 journal = journal.sync_all().await.unwrap();
3096
3097 let size = journal.size(2).unwrap();
3099 journal = journal.test_reopen_at_most(2, size).await.unwrap();
3100 journal = journal.sync_all().await.unwrap();
3101 drop(journal);
3102
3103 let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3105 .await
3106 .unwrap();
3107
3108 for section in 1u64..=2 {
3110 let size = journal.size(section).unwrap();
3111 assert!(size > 0, "section {section} should have data after restart");
3112 }
3113
3114 for section in 3u64..=5 {
3116 let size = journal.size(section).unwrap();
3117 assert_eq!(size, 0, "section {section} should be gone after restart");
3118 }
3119
3120 {
3122 let mut replay = journal
3123 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3124 .await
3125 .unwrap();
3126 let mut items = Vec::new();
3127 while let Some(result) = replay.next().await {
3128 let (section, _, _, item) = result.unwrap();
3129 items.push((section, item));
3130 }
3131 journal = replay.finish().expect("failed to finish replay");
3132 assert_eq!(items.len(), 2);
3133 assert_eq!(items[0], (1, 1));
3134 assert_eq!(items[1], (2, 2));
3135 }
3136
3137 journal.destroy().await.unwrap();
3138 });
3139 }
3140
3141 #[test_traced]
3142 fn test_journal_truncate_to_zero_removes_all_newer() {
3143 let executor = deterministic::Runner::default();
3144 executor.start(|context| async move {
3145 let cfg = Config {
3146 partition: "test-partition".into(),
3147 compression: None,
3148 codec_config: (),
3149 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3150 write_buffer: NZUsize!(1024),
3151 };
3152 let mut journal = Journal::init(context.child("storage"), cfg.clone())
3153 .await
3154 .unwrap();
3155
3156 for section in 1u64..=3 {
3158 (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
3159 }
3160 journal = journal.sync_all().await.unwrap();
3161
3162 journal = journal.test_reopen_at_most(1, 0).await.unwrap();
3164
3165 let size = journal.size(1).unwrap();
3167 assert_eq!(size, 0, "section 1 should be empty");
3168
3169 for section in 2u64..=3 {
3171 let size = journal.size(section).unwrap();
3172 assert_eq!(size, 0, "section {section} should be removed");
3173 }
3174
3175 {
3177 let mut replay = journal
3178 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3179 .await
3180 .unwrap();
3181 assert!(replay.next().await.is_none());
3182 journal = replay.finish().expect("failed to finish replay");
3183 }
3184
3185 journal.destroy().await.unwrap();
3186 });
3187 }
3188
3189 #[test_traced]
3190 fn test_journal_replay_start_offset_with_trailing_bytes() {
3191 let executor = deterministic::Runner::default();
3193 executor.start(|context| async move {
3194 let cfg = Config {
3195 partition: "test-partition".into(),
3196 compression: None,
3197 codec_config: (),
3198 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3199 write_buffer: NZUsize!(1024),
3200 };
3201 let mut journal = Journal::init(context.child("first"), cfg.clone())
3202 .await
3203 .expect("Failed to initialize journal");
3204
3205 for i in 0..5i32 {
3207 (journal, _, _) = journal.append(1, &i).await.unwrap();
3208 }
3209 journal = journal.sync(1).await.unwrap();
3210 let valid_logical_size = journal.size(1).unwrap();
3211 drop(journal);
3212
3213 let (blob, physical_size_before) = context
3215 .open(&cfg.partition, &1u64.to_be_bytes())
3216 .await
3217 .unwrap();
3218
3219 blob.write_at(physical_size_before, vec![0xFF, 0xFF], WriteOptions::SYNC)
3222 .await
3223 .unwrap();
3224
3225 let start_offset = valid_logical_size;
3229 {
3230 drop(blob);
3231 let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3232 .await
3233 .unwrap();
3234
3235 let mut replay = journal
3236 .replay(1, start_offset, NZUsize!(1024), ReadOptions::default())
3237 .await
3238 .unwrap();
3239
3240 while let Some(_result) = replay.next().await {}
3242 }
3243
3244 let physical_size_after = context
3246 .logical_blob(&cfg.partition, &1u64.to_be_bytes())
3247 .unwrap()
3248 .len() as u64;
3249
3250 assert!(
3253 physical_size_after >= physical_size_before,
3254 "Valid data was lost! Physical blob truncated from {physical_size_before} to \
3255 {physical_size_after}. Logical valid size was {valid_logical_size}. \
3256 This indicates valid_offset was incorrectly initialized to 0 instead of start_offset."
3257 );
3258 });
3259 }
3260
3261 #[test_traced]
3262 fn test_journal_replay_rejects_mid_frame_offset() {
3263 deterministic::Runner::default().start(|context| async move {
3264 let cfg = Config {
3265 partition: "mid-frame".into(),
3266 compression: None,
3267 codec_config: (),
3268 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3269 write_buffer: NZUsize!(1024),
3270 };
3271 let journal = Journal::<_, [u8; 16]>::init(context.child("storage"), cfg.clone())
3272 .await
3273 .unwrap();
3274 let (journal, _, _) = journal.append(1, &[127; 16]).await.unwrap();
3275 let journal = journal.sync_all().await.unwrap();
3276
3277 let mut replay = journal
3279 .replay(1, 1, NZUsize!(1024), ReadOptions::default())
3280 .await
3281 .unwrap();
3282 assert!(matches!(
3283 replay.next().await,
3284 Some(Err(Error::ItemOutOfRange(1)))
3285 ));
3286 drop(replay);
3287
3288 let journal = Journal::<_, [u8; 16]>::init(context.child("reopen"), cfg)
3289 .await
3290 .unwrap();
3291 let mut replay = journal
3292 .replay(1, 0, NZUsize!(1024), ReadOptions::default())
3293 .await
3294 .unwrap();
3295 assert_eq!(replay.next().await.unwrap().unwrap().3, [127; 16]);
3296 assert!(replay.next().await.is_none());
3297 replay.finish().unwrap().destroy().await.unwrap();
3298 });
3299 }
3300
3301 #[test_traced]
3302 fn test_journal_replay_rejects_start_offset_past_section() {
3303 let executor = deterministic::Runner::default();
3304 executor.start(|context| async move {
3305 let cfg = Config {
3306 partition: "test-partition".into(),
3307 compression: None,
3308 codec_config: (),
3309 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3310 write_buffer: NZUsize!(1024),
3311 };
3312 let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3313 (journal, _, _) = journal.append(1, &7i32).await.unwrap();
3314
3315 let result = journal
3317 .replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
3318 .await;
3319 assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
3320 });
3321 }
3322
3323 #[test_traced]
3324 fn test_journal_large_item_spanning_pages() {
3325 const LARGE_SIZE: usize = 2048;
3327 type LargeItem = [u8; LARGE_SIZE];
3328
3329 let executor = deterministic::Runner::default();
3330 executor.start(|context| async move {
3331 let cfg = Config {
3332 partition: "test-partition".into(),
3333 compression: None,
3334 codec_config: (),
3335 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3336 write_buffer: NZUsize!(4096),
3337 };
3338 let mut journal = Journal::init(context.child("first"), cfg.clone())
3339 .await
3340 .expect("Failed to initialize journal");
3341
3342 let mut large_data: LargeItem = [0u8; LARGE_SIZE];
3344 for (i, byte) in large_data.iter_mut().enumerate() {
3345 *byte = (i % 256) as u8;
3346 }
3347 assert!(
3348 LARGE_SIZE > PAGE_SIZE.get() as usize,
3349 "Item must be larger than page size"
3350 );
3351
3352 let offset;
3354 let size;
3355 (journal, offset, size) = journal
3356 .append(1, &large_data)
3357 .await
3358 .expect("Failed to append large item");
3359 assert_eq!(size as usize, LARGE_SIZE);
3360 journal = journal.sync(1).await.expect("Failed to sync");
3361
3362 let retrieved: LargeItem = journal
3364 .get(1, offset)
3365 .await
3366 .expect("Failed to get large item");
3367 assert_eq!(retrieved, large_data, "Random access read mismatch");
3368
3369 drop(journal);
3371 let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
3372 .await
3373 .expect("Failed to re-initialize journal");
3374
3375 {
3377 let mut replay = journal
3378 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3379 .await
3380 .expect("Failed to setup replay");
3381
3382 let mut items = Vec::new();
3383 while let Some(result) = replay.next().await {
3384 let (section, off, sz, item) = result.expect("Failed to replay item");
3385 items.push((section, off, sz, item));
3386 }
3387 journal = replay.finish().expect("failed to finish replay");
3388
3389 assert_eq!(items.len(), 1, "Should have exactly one item");
3390 let (section, off, sz, item) = &items[0];
3391 assert_eq!(*section, 1);
3392 assert_eq!(*off, offset);
3393 assert_eq!(*sz as usize, LARGE_SIZE);
3394 assert_eq!(*item, large_data, "Replay read mismatch");
3395 }
3396
3397 journal.destroy().await.unwrap();
3398 });
3399 }
3400
3401 #[test_traced]
3402 fn test_journal_large_item_direct_path() {
3403 const LARGE_SIZE: usize = 2048;
3408 type LargeItem = [u8; LARGE_SIZE];
3409
3410 let executor = deterministic::Runner::default();
3411 executor.start(|context| async move {
3412 let cfg = Config {
3413 partition: "test-partition".into(),
3414 compression: None,
3415 codec_config: (),
3416 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3417 write_buffer: NZUsize!(1024),
3418 };
3419 let mut journal = Journal::init(context.child("first"), cfg.clone())
3420 .await
3421 .expect("Failed to initialize journal");
3422
3423 let mut first: LargeItem = [0u8; LARGE_SIZE];
3424 for (i, byte) in first.iter_mut().enumerate() {
3425 *byte = (i % 256) as u8;
3426 }
3427 let mut second: LargeItem = [0u8; LARGE_SIZE];
3428 for (i, byte) in second.iter_mut().enumerate() {
3429 *byte = ((i + 7) % 251) as u8;
3430 }
3431
3432 let first_offset;
3433 (journal, first_offset, _) = journal
3434 .append(1, &first)
3435 .await
3436 .expect("Failed to append first item");
3437 let second_offset;
3438 (journal, second_offset, _) = journal
3439 .append(1, &second)
3440 .await
3441 .expect("Failed to append second item");
3442
3443 let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
3445 assert_eq!(retrieved, first);
3446 let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
3447 assert_eq!(retrieved, second);
3448
3449 journal = journal.sync(1).await.expect("Failed to sync");
3451 drop(journal);
3452 let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
3453 .await
3454 .expect("Failed to re-initialize journal");
3455
3456 let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
3457 assert_eq!(retrieved, first);
3458 let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
3459 assert_eq!(retrieved, second);
3460
3461 {
3462 let mut replay = journal
3463 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3464 .await
3465 .expect("Failed to setup replay");
3466
3467 let mut items = Vec::new();
3468 while let Some(result) = replay.next().await {
3469 let (section, off, _, item) = result.expect("Failed to replay item");
3470 items.push((section, off, item));
3471 }
3472 journal = replay.finish().expect("failed to finish replay");
3473 assert_eq!(items.len(), 2);
3474 assert_eq!(items[0], (1, first_offset, first));
3475 assert_eq!(items[1], (1, second_offset, second));
3476 }
3477
3478 journal.destroy().await.unwrap();
3479 });
3480 }
3481
3482 #[test_traced]
3483 fn test_journal_non_contiguous_sections() {
3484 let executor = deterministic::Runner::default();
3487 executor.start(|context| async move {
3488 let cfg = Config {
3489 partition: "test-partition".into(),
3490 compression: None,
3491 codec_config: (),
3492 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3493 write_buffer: NZUsize!(1024),
3494 };
3495 let mut journal = Journal::init(context.child("first"), cfg.clone())
3496 .await
3497 .expect("Failed to initialize journal");
3498
3499 let sections_and_data = [(1u64, 100i32), (5u64, 500i32), (10u64, 1000i32)];
3501 let mut offsets = Vec::new();
3502
3503 for (section, data) in §ions_and_data {
3504 let offset;
3505 (journal, offset, _) = journal
3506 .append(*section, data)
3507 .await
3508 .expect("Failed to append");
3509 offsets.push(offset);
3510 }
3511 journal = journal.sync_all().await.expect("Failed to sync");
3512
3513 for (i, (section, expected_data)) in sections_and_data.iter().enumerate() {
3515 let retrieved: i32 = journal
3516 .get(*section, offsets[i])
3517 .await
3518 .expect("Failed to get item");
3519 assert_eq!(retrieved, *expected_data);
3520 }
3521
3522 for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
3524 let result = journal.get(missing_section, 0).await;
3525 assert!(
3526 matches!(result, Err(Error::SectionOutOfRange(_))),
3527 "Expected SectionOutOfRange for section {}, got {:?}",
3528 missing_section,
3529 result
3530 );
3531 }
3532
3533 drop(journal);
3535 let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3536 .await
3537 .expect("Failed to re-initialize journal");
3538
3539 {
3541 let mut replay = journal
3542 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3543 .await
3544 .expect("Failed to setup replay");
3545
3546 let mut items = Vec::new();
3547 while let Some(result) = replay.next().await {
3548 let (section, _, _, item) = result.expect("Failed to replay item");
3549 items.push((section, item));
3550 }
3551 journal = replay.finish().expect("failed to finish replay");
3552
3553 assert_eq!(items.len(), 3, "Should have 3 items");
3554 assert_eq!(items[0], (1, 100));
3555 assert_eq!(items[1], (5, 500));
3556 assert_eq!(items[2], (10, 1000));
3557 }
3558
3559 {
3561 let mut replay = journal
3562 .replay(5, 0, NZUsize!(1024), ReadOptions::default())
3563 .await
3564 .expect("Failed to setup replay from section 5");
3565
3566 let mut items = Vec::new();
3567 while let Some(result) = replay.next().await {
3568 let (section, _, _, item) = result.expect("Failed to replay item");
3569 items.push((section, item));
3570 }
3571 journal = replay.finish().expect("failed to finish replay");
3572
3573 assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
3574 assert_eq!(items[0], (5, 500));
3575 assert_eq!(items[1], (10, 1000));
3576 }
3577
3578 {
3580 let mut replay = journal
3581 .replay(3, 0, NZUsize!(1024), ReadOptions::default())
3582 .await
3583 .expect("Failed to setup replay from section 3");
3584
3585 let mut items = Vec::new();
3586 while let Some(result) = replay.next().await {
3587 let (section, _, _, item) = result.expect("Failed to replay item");
3588 items.push((section, item));
3589 }
3590 journal = replay.finish().expect("failed to finish replay");
3591
3592 assert_eq!(items.len(), 2);
3594 assert_eq!(items[0], (5, 500));
3595 assert_eq!(items[1], (10, 1000));
3596 }
3597
3598 journal.destroy().await.unwrap();
3599 });
3600 }
3601
3602 #[test_traced]
3603 fn test_journal_empty_section_in_middle() {
3604 let executor = deterministic::Runner::default();
3607 executor.start(|context| async move {
3608 let cfg = Config {
3609 partition: "test-partition".into(),
3610 compression: None,
3611 codec_config: (),
3612 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3613 write_buffer: NZUsize!(1024),
3614 };
3615 let mut journal = Journal::init(context.child("first"), cfg.clone())
3616 .await
3617 .expect("Failed to initialize journal");
3618
3619 (journal, _, _) = journal.append(1, &100i32).await.expect("Failed to append");
3621
3622 (journal, _, _) = journal.append(2, &200i32).await.expect("Failed to append");
3624 journal = journal.sync(2).await.expect("Failed to sync");
3625 journal = journal
3626 .test_reopen_section(2, 0)
3627 .await
3628 .expect("Failed to truncate");
3629
3630 (journal, _, _) = journal.append(3, &300i32).await.expect("Failed to append");
3632
3633 journal = journal.sync_all().await.expect("Failed to sync");
3634
3635 assert!(journal.size(1).unwrap() > 0);
3637 assert_eq!(journal.size(2).unwrap(), 0);
3638 assert!(journal.size(3).unwrap() > 0);
3639
3640 drop(journal);
3642 let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3643 .await
3644 .expect("Failed to re-initialize journal");
3645
3646 {
3648 let mut replay = journal
3649 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3650 .await
3651 .expect("Failed to setup replay");
3652
3653 let mut items = Vec::new();
3654 while let Some(result) = replay.next().await {
3655 let (section, _, _, item) = result.expect("Failed to replay item");
3656 items.push((section, item));
3657 }
3658 journal = replay.finish().expect("failed to finish replay");
3659
3660 assert_eq!(
3661 items.len(),
3662 2,
3663 "Should have 2 items (skipping empty section)"
3664 );
3665 assert_eq!(items[0], (1, 100));
3666 assert_eq!(items[1], (3, 300));
3667 }
3668
3669 {
3671 let mut replay = journal
3672 .replay(2, 0, NZUsize!(1024), ReadOptions::default())
3673 .await
3674 .expect("Failed to setup replay from section 2");
3675
3676 let mut items = Vec::new();
3677 while let Some(result) = replay.next().await {
3678 let (section, _, _, item) = result.expect("Failed to replay item");
3679 items.push((section, item));
3680 }
3681 journal = replay.finish().expect("failed to finish replay");
3682
3683 assert_eq!(items.len(), 1, "Should have 1 item from section 3");
3684 assert_eq!(items[0], (3, 300));
3685 }
3686
3687 journal.destroy().await.unwrap();
3688 });
3689 }
3690
3691 #[test_traced]
3692 fn test_journal_item_exactly_page_size() {
3693 const ITEM_SIZE: usize = PAGE_SIZE.get() as usize;
3696 type ExactItem = [u8; ITEM_SIZE];
3697
3698 let executor = deterministic::Runner::default();
3699 executor.start(|context| async move {
3700 let cfg = Config {
3701 partition: "test-partition".into(),
3702 compression: None,
3703 codec_config: (),
3704 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3705 write_buffer: NZUsize!(4096),
3706 };
3707 let mut journal = Journal::init(context.child("first"), cfg.clone())
3708 .await
3709 .expect("Failed to initialize journal");
3710
3711 let mut exact_data: ExactItem = [0u8; ITEM_SIZE];
3713 for (i, byte) in exact_data.iter_mut().enumerate() {
3714 *byte = (i % 256) as u8;
3715 }
3716
3717 let offset;
3719 let size;
3720 (journal, offset, size) = journal
3721 .append(1, &exact_data)
3722 .await
3723 .expect("Failed to append exact item");
3724 assert_eq!(size as usize, ITEM_SIZE);
3725 journal = journal.sync(1).await.expect("Failed to sync");
3726
3727 let retrieved: ExactItem = journal
3729 .get(1, offset)
3730 .await
3731 .expect("Failed to get exact item");
3732 assert_eq!(retrieved, exact_data, "Random access read mismatch");
3733
3734 drop(journal);
3736 let mut journal = Journal::<_, ExactItem>::init(context.child("second"), cfg.clone())
3737 .await
3738 .expect("Failed to re-initialize journal");
3739
3740 {
3742 let mut replay = journal
3743 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3744 .await
3745 .expect("Failed to setup replay");
3746
3747 let mut items = Vec::new();
3748 while let Some(result) = replay.next().await {
3749 let (section, off, sz, item) = result.expect("Failed to replay item");
3750 items.push((section, off, sz, item));
3751 }
3752 journal = replay.finish().expect("failed to finish replay");
3753
3754 assert_eq!(items.len(), 1, "Should have exactly one item");
3755 let (section, off, sz, item) = &items[0];
3756 assert_eq!(*section, 1);
3757 assert_eq!(*off, offset);
3758 assert_eq!(*sz as usize, ITEM_SIZE);
3759 assert_eq!(*item, exact_data, "Replay read mismatch");
3760 }
3761
3762 journal.destroy().await.unwrap();
3763 });
3764 }
3765
3766 #[test_traced]
3767 fn test_journal_uncompressed_frames_fill_write_buffer() {
3768 deterministic::Runner::default().start(|context| async move {
3769 let cfg = Config {
3770 partition: "test-partition".into(),
3771 compression: None,
3772 codec_config: ((..).into(), ()),
3773 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3774 write_buffer: NZUsize!(2048),
3775 };
3776 let direct = vec![7u8; 126];
3777 let fallback = vec![9u8; 127];
3778 assert_eq!(direct.encode_size(), 127);
3779 assert_eq!(fallback.encode_size(), 128);
3780
3781 let mut journal = Journal::init(context.child("first"), cfg.clone())
3782 .await
3783 .unwrap();
3784
3785 for i in 0..16 {
3788 let offset;
3789 let item_len;
3790 (journal, offset, item_len) = journal.append(1, &direct).await.unwrap();
3791 assert_eq!(offset, i * 128);
3792 assert_eq!(item_len, 127);
3793 }
3794 let fallback_offset;
3795 let fallback_len;
3796 (journal, fallback_offset, fallback_len) = journal.append(1, &fallback).await.unwrap();
3797 assert_eq!(fallback_offset, 2048);
3798 assert_eq!(fallback_len, 128);
3799
3800 journal = journal.sync(1).await.unwrap();
3802 drop(journal);
3803 let mut journal = Journal::<_, Vec<u8>>::init(context.child("second"), cfg)
3804 .await
3805 .unwrap();
3806 assert_eq!(journal.get(1, fallback_offset).await.unwrap(), fallback);
3807
3808 let mut replay = journal
3810 .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3811 .await
3812 .unwrap();
3813 for i in 0..16 {
3814 let (section, offset, item_len, item) = replay.next().await.unwrap().unwrap();
3815 assert_eq!((section, offset, item_len), (1, i * 128, 127));
3816 assert_eq!(item, direct);
3817 }
3818 let (section, offset, item_len, item) = replay.next().await.unwrap().unwrap();
3819 assert_eq!((section, offset, item_len), (1, 2048, 128));
3820 assert_eq!(item, fallback);
3821 assert!(replay.next().await.is_none());
3822 journal = replay.finish().unwrap();
3823 journal.destroy().await.unwrap();
3824 });
3825 }
3826
3827 #[test_traced]
3828 fn test_journal_varint_spanning_page_boundary() {
3829 const SMALL_PAGE: NonZeroU16 = NZU16!(16);
3837
3838 let executor = deterministic::Runner::default();
3839 executor.start(|context| async move {
3840 let cfg = Config {
3841 partition: "test-partition".into(),
3842 compression: None,
3843 codec_config: (),
3844 page_cache: CacheRef::new(SMALL_PAGE, PAGE_CACHE_SIZE),
3845 write_buffer: NZUsize!(1024),
3846 };
3847 let mut journal: Journal<_, [u8; 128]> =
3848 Journal::init(context.child("first"), cfg.clone())
3849 .await
3850 .expect("Failed to initialize journal");
3851
3852 let item1: [u8; 128] = [1u8; 128];
3854 let item2: [u8; 128] = [2u8; 128];
3855 let item3: [u8; 128] = [3u8; 128];
3856
3857 let offset1;
3860 (journal, offset1, _) = journal.append(1, &item1).await.expect("Failed to append");
3861 let offset2;
3862 (journal, offset2, _) = journal.append(1, &item2).await.expect("Failed to append");
3863 let offset3;
3864 (journal, offset3, _) = journal.append(1, &item3).await.expect("Failed to append");
3865
3866 journal = journal.sync(1).await.expect("Failed to sync");
3867
3868 let retrieved1: [u8; 128] = journal.get(1, offset1).await.expect("Failed to get");
3870 let retrieved2: [u8; 128] = journal.get(1, offset2).await.expect("Failed to get");
3871 let retrieved3: [u8; 128] = journal.get(1, offset3).await.expect("Failed to get");
3872 assert_eq!(retrieved1, item1);
3873 assert_eq!(retrieved2, item2);
3874 assert_eq!(retrieved3, item3);
3875
3876 drop(journal);
3878 let mut journal: Journal<_, [u8; 128]> =
3879 Journal::init(context.child("second"), cfg.clone())
3880 .await
3881 .expect("Failed to re-initialize journal");
3882
3883 {
3885 let mut replay = journal
3886 .replay(0, 0, NZUsize!(64), ReadOptions::default())
3887 .await
3888 .expect("Failed to setup replay");
3889
3890 let mut items = Vec::new();
3891 while let Some(result) = replay.next().await {
3892 let (section, off, _, item) = result.expect("Failed to replay item");
3893 items.push((section, off, item));
3894 }
3895 journal = replay.finish().expect("failed to finish replay");
3896
3897 assert_eq!(items.len(), 3, "Should have 3 items");
3898 assert_eq!(items[0], (1, offset1, item1));
3899 assert_eq!(items[1], (1, offset2, item2));
3900 assert_eq!(items[2], (1, offset3, item3));
3901 }
3902
3903 journal.destroy().await.unwrap();
3904 });
3905 }
3906
3907 #[test_traced]
3908 fn test_journal_clear() {
3909 let executor = deterministic::Runner::default();
3910 executor.start(|context| async move {
3911 let cfg = Config {
3912 partition: "clear-test".into(),
3913 compression: None,
3914 codec_config: (),
3915 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3916 write_buffer: NZUsize!(1024),
3917 };
3918
3919 let mut journal: Journal<_, u64> = Journal::init(context.child("journal"), cfg.clone())
3920 .await
3921 .expect("Failed to initialize journal");
3922
3923 for section in 0..5u64 {
3925 for i in 0..10u64 {
3926 (journal, _, _) = journal
3927 .append(section, &(section * 1000 + i))
3928 .await
3929 .expect("Failed to append");
3930 }
3931 journal = journal.sync(section).await.expect("Failed to sync");
3932 }
3933
3934 assert_eq!(journal.get(0, 0).await.unwrap(), 0);
3936 assert_eq!(journal.get(4, 0).await.unwrap(), 4000);
3937
3938 journal = journal.clear().await.expect("Failed to clear");
3940
3941 for section in 0..5u64 {
3943 assert!(matches!(
3944 journal.get(section, 0).await,
3945 Err(Error::SectionOutOfRange(s)) if s == section
3946 ));
3947 }
3948
3949 for i in 0..5u64 {
3951 (journal, _, _) = journal
3952 .append(10, &(i * 100))
3953 .await
3954 .expect("Failed to append after clear");
3955 }
3956 journal = journal.sync(10).await.expect("Failed to sync after clear");
3957
3958 assert_eq!(journal.get(10, 0).await.unwrap(), 0);
3960
3961 assert!(matches!(
3963 journal.get(0, 0).await,
3964 Err(Error::SectionOutOfRange(0))
3965 ));
3966
3967 journal.destroy().await.unwrap();
3968 });
3969 }
3970
3971 #[test_traced]
3972 fn test_journal_try_get_sync() {
3973 let executor = deterministic::Runner::default();
3974 executor.start(|context| async move {
3975 for (compression, partition) in [(None, "plain"), (Some(3), "compressed")] {
3976 let cfg = Config {
3977 partition: partition.into(),
3978 compression,
3979 codec_config: (..).into(),
3980 page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3981 write_buffer: NZUsize!(1024),
3982 };
3983 let journal = Journal::<_, Bytes>::init(context.child(partition), cfg)
3984 .await
3985 .expect("Failed to initialize journal");
3986
3987 let short = Bytes::from(vec![1u8; 3]);
3990 let (journal, short_offset, _) =
3991 journal.append(1, &short).await.expect("Failed to append");
3992 let long = Bytes::from(vec![7u8; 32]);
3993 let (journal, long_offset, _) =
3994 journal.append(1, &long).await.expect("Failed to append");
3995 assert_eq!(journal.try_get_sync(1, short_offset), Some(short.clone()));
3996 assert_eq!(journal.try_get_sync(1, long_offset), Some(long.clone()));
3997
3998 let journal = journal.sync(1).await.expect("Failed to sync");
4000 journal.get(1, long_offset).await.expect("Failed to get");
4001 assert_eq!(journal.try_get_sync(1, long_offset), Some(long.clone()));
4002
4003 let size = journal.size(1).expect("Failed to size");
4005 assert!(journal.try_get_sync(1, size).is_none());
4006
4007 journal.destroy().await.expect("Failed to destroy");
4008 }
4009 });
4010 }
4011}