1use super::{
7 blob_first_position,
8 blobs::{Blob, Blobs, Partition, Replay as BlobReplay, Writable},
9 fixed,
10 metrics::Metrics,
11 position_to_blob, Contiguous, Many, Mutable,
12};
13#[commonware_macros::stability(ALPHA)]
14use crate::{journal::authenticated, merkle};
15use crate::{
16 journal::{
17 frame::{
18 decode_item, decode_length_prefix, encode_frame_into, find_frame, read_frame_at,
19 FrameInfo,
20 },
21 Error,
22 },
23 Context,
24};
25use commonware_codec::{varint::MAX_U32_VARINT_SIZE, Codec, CodecShared};
26use commonware_macros::boxed;
27use commonware_runtime::{
28 buffer::paged::{CacheRef, Replay, Writer},
29 Blob as RBlob, Buf, IoBuf,
30};
31use commonware_utils::NZUsize;
32use futures::{future::try_join_all, Stream};
33use std::{
34 collections::BTreeMap,
35 io::Cursor,
36 marker::PhantomData,
37 num::{NonZeroU64, NonZeroUsize},
38 ops::Range,
39 sync::Arc,
40};
41#[commonware_macros::stability(ALPHA)]
42use tracing::debug;
43use tracing::warn;
44
45pub struct PreparedAppend<V> {
48 encoded: Vec<u8>,
49 item_starts: Vec<usize>,
50 compressed: bool,
51 _marker: PhantomData<V>,
52}
53
54const REPLAY_BUFFER_SIZE: NonZeroUsize = NZUsize!(1024);
56
57const DATA_SUFFIX: &str = "_data";
59
60const OFFSETS_SUFFIX: &str = "_offsets";
62
63fn decode_frame_from_span<V: CodecShared>(
67 bytes: &[u8],
68 frame_len: usize,
69 codec_config: &V::Cfg,
70 compressed: bool,
71) -> Option<V> {
72 let mut cursor = Cursor::new(bytes);
73 let (size, varint_len) = decode_length_prefix(&mut cursor).ok()?;
74 let actual_len = size.checked_add(varint_len)?;
75 if actual_len != frame_len || frame_len > bytes.len() {
76 return None;
77 }
78 decode_item::<V>(&bytes[varint_len..frame_len], codec_config, compressed).ok()
79}
80
81enum Frame {
83 Item { offset: u64 },
85 End { valid_size: u64, torn: bool },
88}
89
90struct FrameScanner<'a, B: RBlob, V: Codec> {
92 replay: Replay<B>,
93 offset: u64,
95 codec_config: &'a V::Cfg,
96 compressed: bool,
97}
98
99impl<'a, B: RBlob, V: CodecShared> FrameScanner<'a, B, V> {
100 const fn new(replay: Replay<B>, codec_config: &'a V::Cfg, compressed: bool) -> Self {
101 Self {
102 replay,
103 offset: 0,
104 codec_config,
105 compressed,
106 }
107 }
108
109 async fn next(&mut self) -> Result<Frame, Error> {
114 match self.replay.ensure(MAX_U32_VARINT_SIZE).await {
115 Ok(true) => {}
116 Ok(false) if self.replay.remaining() == 0 => {
117 return Ok(Frame::End {
118 valid_size: self.offset,
119 torn: false,
120 });
121 }
122 Ok(false) => {}
124 Err(err) => return Err(err.into()),
125 }
126
127 let before_remaining = self.replay.remaining();
128 let (item_size, varint_len) = match decode_length_prefix(&mut self.replay) {
129 Ok(result) => result,
130 Err(err) => {
131 if self.replay.is_exhausted() || before_remaining < MAX_U32_VARINT_SIZE {
134 return Ok(Frame::End {
135 valid_size: self.offset,
136 torn: true,
137 });
138 }
139 return Err(err);
140 }
141 };
142
143 match self.replay.ensure(item_size).await {
144 Ok(true) => {}
145 Ok(false) => {
146 return Ok(Frame::End {
147 valid_size: self.offset,
148 torn: true,
149 });
150 }
151 Err(err) => return Err(err.into()),
152 }
153
154 let item_offset = self.offset;
155 let next_offset = item_offset
156 .checked_add(varint_len as u64)
157 .and_then(|offset| offset.checked_add(item_size as u64))
158 .ok_or(Error::OffsetOverflow)?;
159 decode_item::<V>(
160 (&mut self.replay).take(item_size),
161 self.codec_config,
162 self.compressed,
163 )?;
164 self.offset = next_offset;
165 Ok(Frame::Item {
166 offset: item_offset,
167 })
168 }
169}
170
171struct BlobScan {
173 items: u64,
175 valid_size: u64,
177 torn: bool,
179}
180
181struct ReplayState<'a, B: RBlob, V: Codec> {
186 blob: u64,
188 replay: BlobReplay<'a, B>,
190 budget: u64,
192 pos: u64,
194 end_pos: u64,
196 offset: u64,
198 codec_config: V::Cfg,
200 compressed: bool,
202 _marker: PhantomData<V>,
203}
204
205impl<B: RBlob, V: CodecShared> super::ReplayBatchState for ReplayState<'_, B, V> {
206 type Item = V;
207
208 async fn next_batch(mut self) -> Option<(Vec<Result<(u64, V), Error>>, Self)> {
210 if self.pos == self.end_pos {
211 return None;
212 }
213
214 let mut batch = Vec::new();
215 let mut consumed = 0u64;
216 loop {
217 if self.pos == self.end_pos {
218 return (!batch.is_empty()).then_some((batch, self));
219 }
220
221 match self.replay.ensure(MAX_U32_VARINT_SIZE).await {
224 Ok(true) => {}
225 Ok(false) if self.replay.remaining() == 0 => {
226 batch.push(Err(Error::Corruption(format!(
227 "data blob {} ended before position {}",
228 self.blob, self.pos
229 ))));
230 self.pos = self.end_pos;
231 return Some((batch, self));
232 }
233 Ok(false) => {}
234 Err(err) => {
235 batch.push(Err(err));
236 self.pos = self.end_pos;
237 return Some((batch, self));
238 }
239 }
240
241 let before_remaining = self.replay.remaining();
242 let (item_size, varint_len) = match decode_length_prefix(&mut self.replay) {
243 Ok(result) => result,
244 Err(err) => {
245 if self.replay.is_exhausted() || before_remaining < MAX_U32_VARINT_SIZE {
246 batch.push(Err(Error::Corruption(format!(
247 "incomplete frame header in data blob {} at offset {}",
248 self.blob, self.offset
249 ))));
250 } else {
251 batch.push(Err(err));
252 }
253 self.pos = self.end_pos;
254 return Some((batch, self));
255 }
256 };
257
258 match self.replay.ensure(item_size).await {
259 Ok(true) => {}
260 Ok(false) => {
261 batch.push(Err(Error::Corruption(format!(
262 "incomplete frame in data blob {} at offset {}",
263 self.blob, self.offset
264 ))));
265 self.pos = self.end_pos;
266 return Some((batch, self));
267 }
268 Err(err) => {
269 batch.push(Err(err));
270 self.pos = self.end_pos;
271 return Some((batch, self));
272 }
273 }
274
275 let next_offset = self
276 .offset
277 .checked_add(varint_len as u64)
278 .and_then(|offset| offset.checked_add(item_size as u64));
279 let Some(next_offset) = next_offset else {
280 batch.push(Err(Error::OffsetOverflow));
281 self.pos = self.end_pos;
282 return Some((batch, self));
283 };
284 let item_len = next_offset - self.offset;
285
286 match decode_item::<V>(
289 (&mut self.replay).take(item_size),
290 &self.codec_config,
291 self.compressed,
292 ) {
293 Ok(item) => {
294 let pos = self.pos;
295 let Some(next_pos) = self.pos.checked_add(1) else {
296 batch.push(Err(Error::OffsetOverflow));
297 self.pos = self.end_pos;
298 return Some((batch, self));
299 };
300 self.pos = next_pos;
301 self.offset = next_offset;
302 consumed = match consumed.checked_add(item_len) {
303 Some(consumed) => consumed,
304 None => {
305 batch.push(Err(Error::OffsetOverflow));
306 self.pos = self.end_pos;
307 return Some((batch, self));
308 }
309 };
310 batch.push(Ok((pos, item)));
311 }
312 Err(err) => {
313 batch.push(Err(err));
314 self.pos = self.end_pos;
315 return Some((batch, self));
316 }
317 }
318
319 if consumed >= self.budget {
322 return Some((batch, self));
323 }
324 if self.replay.remaining() < MAX_U32_VARINT_SIZE {
325 return Some((batch, self));
326 }
327 }
328 }
329}
330
331#[derive(Clone)]
333pub struct Config<C> {
334 pub partition: String,
336
337 pub items_per_section: NonZeroU64,
342
343 pub compression: Option<u8>,
345
346 pub codec_config: C,
348
349 pub page_cache: CacheRef,
351
352 pub write_buffer: NonZeroUsize,
354}
355
356impl<C> Config<C> {
357 fn data_partition(&self) -> String {
359 format!("{}{}", self.partition, DATA_SUFFIX)
360 }
361
362 fn offsets_partition(&self) -> String {
364 format!("{}{}", self.partition, OFFSETS_SUFFIX)
365 }
366}
367
368pub struct Journal<E: Context, V: Codec> {
413 blobs: Writable<E>,
415
416 offsets: fixed::Journal<E, u64>,
419
420 bounds: Range<u64>,
422
423 dirty_from_blob: Option<u64>,
425
426 #[cfg(test)]
429 halt_before_offsets_prune: bool,
430
431 items_per_blob: NonZeroU64,
438
439 compression: Option<u8>,
441
442 codec_config: V::Cfg,
444
445 metrics: Arc<Metrics<E>>,
447}
448
449pub struct Reader<'a, E: Context, V: Codec> {
451 data: Blobs<'a, E::Blob>,
453
454 bounds: Range<u64>,
456
457 offsets: fixed::Reader<'a, E, u64>,
459
460 items_per_blob: NonZeroU64,
462
463 codec_config: V::Cfg,
465
466 compressed: bool,
468
469 metrics: Arc<Metrics<E>>,
471}
472
473impl<'a, E: Context, V: CodecShared> Reader<'a, E, V> {
474 const fn validate_readable(&self, position: u64) -> Result<(), Error> {
476 if position >= self.bounds.end {
477 return Err(Error::ItemOutOfRange(position));
478 }
479 if position < self.bounds.start {
480 return Err(Error::ItemPruned(position));
481 }
482 Ok(())
483 }
484
485 async fn read_at_offset(&self, blob: &Blob<'_, E::Blob>, offset: u64) -> Result<V, Error> {
487 read_frame_at(blob, offset, &self.codec_config, self.compressed)
488 .await
489 .map(|(_, _, item)| item)
490 }
491
492 async fn read_consecutive(
499 &self,
500 blob_handle: &Blob<'_, E::Blob>,
501 blob: u64,
502 offsets: &[u64],
503 ) -> Result<Vec<V>, Error> {
504 if offsets.len() <= 1 {
506 let mut items = Vec::with_capacity(offsets.len());
507 for &offset in offsets {
508 items.push(self.read_at_offset(blob_handle, offset).await?);
509 }
510 return Ok(items);
511 }
512
513 for window in offsets.windows(2) {
514 if window[1] <= window[0] {
515 return Err(Error::Corruption(format!(
516 "non-increasing offsets in blob {blob}: {} >= {}",
517 window[0], window[1]
518 )));
519 }
520 }
521
522 let start = offsets[0];
525 let end = offsets[offsets.len() - 1];
526 let range_len = usize::try_from(end - start).map_err(|_| Error::OffsetOverflow)?;
527 let bytes = blob_handle.read_at(start, range_len).await?.coalesce();
528 let bytes = bytes.as_ref();
529
530 let mut items = Vec::with_capacity(offsets.len());
531 let mut local_offset = 0usize;
532 for window in offsets.windows(2) {
533 let offset = window[0];
534 let next_offset = window[1];
535 let item_len =
536 usize::try_from(next_offset - offset).map_err(|_| Error::OffsetOverflow)?;
537
538 let mut cursor = Cursor::new(&bytes[local_offset..]);
539 let (size, varint_len) = decode_length_prefix(&mut cursor)?;
540 let actual_len = size.checked_add(varint_len).ok_or(Error::OffsetOverflow)?;
541 if actual_len != item_len {
542 return Err(Error::OffsetDataMismatch {
543 section: blob,
544 offset,
545 expected_len: item_len,
546 actual_len,
547 });
548 }
549
550 let data_start = local_offset
553 .checked_add(varint_len)
554 .ok_or(Error::OffsetOverflow)?;
555 let data_end = local_offset
556 .checked_add(item_len)
557 .ok_or(Error::OffsetOverflow)?;
558 items.push(decode_item::<V>(
559 &bytes[data_start..data_end],
560 &self.codec_config,
561 self.compressed,
562 )?);
563
564 local_offset = data_end;
565 }
566
567 items.push(self.read_at_offset(blob_handle, end).await?);
568 Ok(items)
569 }
570
571 fn try_read_frame_sync(&self, position: u64, offset: u64, buf: &mut Vec<u8>) -> Option<V> {
574 let blob = self
575 .data
576 .get(position_to_blob(position, self.items_per_blob.get()))?;
577 let remaining = blob.size().checked_sub(offset)?;
578 let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
579 if header_len == 0 {
580 return None;
581 }
582
583 let mut header = [0u8; MAX_U32_VARINT_SIZE];
585 if !blob.try_read_sync_into(&mut header[..header_len], offset) {
586 return None;
587 }
588 let mut cursor = Cursor::new(&header[..header_len]);
589 let (_, item_info) = find_frame(&mut cursor, offset).ok()?;
590
591 let (varint_len, data_len) = match item_info {
592 FrameInfo::Complete {
593 varint_len,
594 data_len,
595 } => (varint_len, data_len),
596 FrameInfo::Incomplete {
597 varint_len,
598 total_len,
599 ..
600 } => (varint_len, total_len),
601 };
602 let item_len = varint_len.checked_add(data_len)?;
603 if item_len > usize::try_from(remaining).ok()? {
604 return None;
605 }
606
607 if item_len <= header_len {
609 return decode_item::<V>(
610 &header[varint_len..varint_len + data_len],
611 &self.codec_config,
612 self.compressed,
613 )
614 .ok();
615 }
616
617 buf.resize(item_len, 0);
619 if !blob.try_read_sync_into(buf, offset) {
620 return None;
621 }
622 decode_item::<V>(
623 &buf[varint_len..varint_len + data_len],
624 &self.codec_config,
625 self.compressed,
626 )
627 .ok()
628 }
629
630 async fn replay_states(
632 &self,
633 start_pos: u64,
634 buffer: NonZeroUsize,
635 ) -> Result<Vec<ReplayState<'a, E::Blob, V>>, Error> {
636 let bounds = self.bounds();
637 if start_pos > bounds.end {
638 return Err(Error::ItemOutOfRange(start_pos));
639 }
640 if start_pos < bounds.start {
641 return Err(Error::ItemPruned(start_pos));
642 }
643
644 let mut states = Vec::new();
645 if start_pos < bounds.end {
646 let items_per_blob = self.items_per_blob.get();
649 let start_blob = position_to_blob(start_pos, items_per_blob);
650 let end_blob = position_to_blob(bounds.end - 1, items_per_blob);
651 let start_offset = self.offsets.read(start_pos).await?;
652
653 for blob in start_blob..=end_blob {
654 let blob_handle = self
655 .data
656 .get(blob)
657 .expect("positions in bounds map to a retained blob");
658 let offset = if blob == start_blob { start_offset } else { 0 };
659
660 let first_pos = if blob == start_blob {
661 start_pos
662 } else {
663 blob_first_position(blob, items_per_blob)?
664 };
665 let end_pos = super::blob_end_position(blob, items_per_blob, bounds.end);
666
667 states.push(ReplayState::<E::Blob, V> {
670 blob,
671 replay: blob_handle.replay_from(offset, buffer)?,
672 budget: buffer.get() as u64,
673 pos: first_pos,
674 end_pos,
675 offset,
676 codec_config: self.codec_config.clone(),
677 compressed: self.compressed,
678 _marker: PhantomData,
679 });
680 }
681 }
682
683 Ok(states)
684 }
685
686 fn validate_read_many(&self, positions: &[u64]) -> Result<(), Error> {
689 if positions[0] < self.bounds.start {
690 return Err(Error::ItemPruned(positions[0]));
691 }
692 let last_position = *positions.last().expect("positions is not empty");
693 if last_position >= self.bounds.end {
694 return Err(Error::ItemOutOfRange(last_position));
695 }
696 assert!(
697 positions.is_sorted_by(|a, b| a < b),
698 "positions must be strictly increasing"
699 );
700 Ok(())
701 }
702
703 async fn read_misses(
707 &self,
708 result: &mut [Option<V>],
709 miss_indices: Option<&[usize]>,
710 miss_positions: &[u64],
711 miss_offsets: &[u64],
712 ) -> Result<(), Error> {
713 let items_per_blob = self.items_per_blob.get();
716 let mut runs = Vec::new();
717 let mut group_start = 0;
718 while group_start < miss_positions.len() {
719 let blob = position_to_blob(miss_positions[group_start], items_per_blob);
720 let mut group_end = group_start + 1;
721 while group_end < miss_positions.len()
722 && position_to_blob(miss_positions[group_end], items_per_blob) == blob
723 {
724 group_end += 1;
725 }
726
727 let blob_handle = self
728 .data
729 .get(blob)
730 .expect("positions in bounds map to a retained blob");
731 let mut run_start = group_start;
734 while run_start < group_end {
735 let mut run_end = run_start + 1;
736 while run_end < group_end
737 && miss_positions[run_end - 1].checked_add(1) == Some(miss_positions[run_end])
738 {
739 run_end += 1;
740 }
741 runs.push((run_start, run_end, blob, blob_handle.clone()));
742 run_start = run_end;
743 }
744 group_start = group_end;
745 }
746
747 let run_items = try_join_all(runs.iter().map(|(run_start, run_end, blob, handle)| {
748 self.read_consecutive(handle, *blob, &miss_offsets[*run_start..*run_end])
749 }))
750 .await?;
751 for ((run_start, _, _, _), items) in runs.iter().zip(run_items) {
752 for (k, item) in items.into_iter().enumerate() {
753 let slot = miss_indices.map_or(run_start + k, |indices| indices[run_start + k]);
754 result[slot] = Some(item);
755 }
756 }
757
758 Ok(())
759 }
760
761 fn read_many_sync_pass(&self, positions: &[u64], out: &mut [Option<V>]) -> Vec<Option<u64>> {
767 let mut resolved: Vec<Option<u64>> = vec![None; positions.len()];
768 if positions.is_empty() {
769 return resolved;
770 }
771
772 let mut lookups: Vec<u64> = Vec::with_capacity(positions.len() * 2);
777 for &position in positions {
778 if lookups.last() != Some(&position) {
779 lookups.push(position);
780 }
781 match position.checked_add(1) {
782 Some(next) if next < self.bounds.end => lookups.push(next),
783 _ => {}
784 }
785 }
786 let offsets = self.offsets.probe_items(&lookups);
787
788 let items_per_blob = self.items_per_blob.get();
792 let mut extents: Vec<(usize, u64, usize)> = Vec::with_capacity(positions.len());
793 let mut singles: Vec<(usize, u64)> = Vec::new();
794 let mut lookup_idx = 0;
795 for (idx, &position) in positions.iter().enumerate() {
796 while lookups[lookup_idx] != position {
797 lookup_idx += 1;
798 }
799 if self.validate_readable(position).is_err() {
800 continue;
801 }
802 let Some(offset) = offsets[lookup_idx] else {
803 continue;
804 };
805 resolved[idx] = Some(offset);
806
807 let next = position + 1;
811 let next_offset = if next < self.bounds.end
812 && position_to_blob(position, items_per_blob)
813 == position_to_blob(next, items_per_blob)
814 {
815 offsets[lookup_idx + 1]
816 } else {
817 None
818 };
819 match next_offset {
820 Some(next) if next > offset => {
821 extents.push((idx, offset, (next - offset) as usize))
822 }
823 _ => singles.push((idx, offset)),
824 }
825 }
826
827 let mut buf = Vec::new();
828 let mut hits = 0u64;
829
830 let mut group_start = 0;
832 while group_start < extents.len() {
833 let blob_num = position_to_blob(positions[extents[group_start].0], items_per_blob);
834 let mut group_end = group_start + 1;
835 while group_end < extents.len()
836 && position_to_blob(positions[extents[group_end].0], items_per_blob) == blob_num
837 {
838 group_end += 1;
839 }
840 let group = &extents[group_start..group_end];
841 group_start = group_end;
842
843 let Some(blob) = self.data.get(blob_num) else {
844 continue;
845 };
846 let ranges: Vec<(u64, usize)> = group
847 .iter()
848 .map(|&(_, offset, len)| (offset, len))
849 .collect();
850 let total: usize = ranges.iter().map(|&(_, len)| len).sum();
851 buf.resize(total, 0);
852 let missed = blob.try_read_ranges_sync_into(&mut buf, &ranges);
853 let mut missed = missed.into_iter().peekable();
854 let mut local = 0usize;
855 for (range_idx, &(idx, _, len)) in group.iter().enumerate() {
856 let slot = &buf[local..local + len];
857 local += len;
858 if missed.peek() == Some(&range_idx) {
859 missed.next();
860 continue;
861 }
862 if let Some(item) =
863 decode_frame_from_span(slot, len, &self.codec_config, self.compressed)
864 {
865 out[idx] = Some(item);
866 hits += 1;
867 }
868 }
869 }
870
871 let mut frame_buf = Vec::new();
873 for (idx, offset) in singles {
874 if let Some(item) = self.try_read_frame_sync(positions[idx], offset, &mut frame_buf) {
875 out[idx] = Some(item);
876 hits += 1;
877 }
878 }
879 self.metrics.cache_hits.inc_by(hits);
880 self.metrics.items_read.inc_by(hits);
881 resolved
882 }
883}
884
885#[derive(Clone, Copy)]
888struct Miss {
889 position: u64,
890 offset: Option<u64>,
891}
892
893async fn complete<E: Context, V: CodecShared>(
896 reader: &Reader<'_, E, V>,
897 items: Vec<Option<V>>,
898 misses: Vec<Miss>,
899) -> Result<Vec<V>, Error> {
900 if misses.is_empty() {
901 return Ok(items
902 .into_iter()
903 .map(|item| item.expect("complete probe has no misses"))
904 .collect());
905 }
906
907 let fetched = reader.fetch_misses(&misses).await?;
908 let mut fetched = fetched.into_iter();
909 Ok(items
910 .into_iter()
911 .map(|item| item.unwrap_or_else(|| fetched.next().expect("one fetched item per miss")))
912 .collect())
913}
914
915impl<E: Context, V: CodecShared> Reader<'_, E, V> {
916 fn probe_parts(&self, positions: &[u64]) -> (Vec<Option<V>>, Vec<Miss>) {
919 let mut items: Vec<Option<V>> = (0..positions.len()).map(|_| None).collect();
920 let resolved = self.read_many_sync_pass(positions, &mut items);
921 let misses = positions
922 .iter()
923 .zip(&items)
924 .zip(resolved)
925 .filter_map(|((&position, item), offset)| {
926 item.is_none().then_some(Miss { position, offset })
927 })
928 .collect();
929 (items, misses)
930 }
931
932 async fn fetch_misses(&self, misses: &[Miss]) -> Result<Vec<V>, Error> {
936 if misses.is_empty() {
937 return Ok(Vec::new());
938 }
939
940 for miss in misses {
943 self.validate_readable(miss.position)?;
944 }
945
946 let unresolved: Vec<u64> = misses
947 .iter()
948 .filter(|miss| miss.offset.is_none())
949 .map(|miss| miss.position)
950 .collect();
951
952 let fetched = self
955 .offsets
956 .read_many_inner(&unresolved)
957 .await
958 .map_err(|e| match e {
959 Error::ItemOutOfRange(e) | Error::ItemPruned(e) => {
960 Error::Corruption(format!("blob/item should be found, but got: {e}"))
961 }
962 other => other,
963 })?;
964 let mut fetched = fetched.into_iter();
965 let offsets: Vec<u64> = misses
966 .iter()
967 .map(|miss| {
968 miss.offset.unwrap_or_else(|| {
969 fetched
970 .next()
971 .expect("one fetched offset per unresolved miss")
972 })
973 })
974 .collect();
975 let positions: Vec<u64> = misses.iter().map(|miss| miss.position).collect();
976
977 let mut result: Vec<Option<V>> = (0..misses.len()).map(|_| None).collect();
978 self.read_misses(&mut result, None, &positions, &offsets)
979 .await?;
980 self.metrics.cache_misses.inc_by(positions.len() as u64);
981 self.metrics.items_read.inc_by(positions.len() as u64);
982 Ok(result
983 .into_iter()
984 .map(|item| item.expect("read_misses fills every slot"))
985 .collect())
986 }
987}
988
989impl<E: Context, V: CodecShared> super::Contiguous for Reader<'_, E, V> {
990 type Item = V;
991
992 fn bounds(&self) -> Range<u64> {
993 self.bounds.clone()
994 }
995
996 async fn read(&self, position: u64) -> Result<V, Error> {
997 self.metrics.read_calls.inc();
998 self.validate_readable(position)?;
999
1000 let cached_offset = self.offsets.try_read_sync(position);
1004 if let Some(offset) = cached_offset {
1005 let mut buf = Vec::new();
1006 if let Some(item) = self.try_read_frame_sync(position, offset, &mut buf) {
1007 self.metrics.cache_hits.inc();
1008 self.metrics.items_read.inc();
1009 return Ok(item);
1010 }
1011 }
1012
1013 let _timer = self.metrics.read_timer();
1014 let offset = match cached_offset {
1015 Some(offset) => offset,
1016 None => self.offsets.read(position).await?,
1017 };
1018 let blob = self
1019 .data
1020 .get(position_to_blob(position, self.items_per_blob.get()))
1021 .expect("position in bounds maps to a retained blob");
1022 self.metrics.cache_misses.inc();
1023 let item = self.read_at_offset(&blob, offset).await?;
1024 self.metrics.items_read.inc();
1025 Ok(item)
1026 }
1027
1028 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
1029 if positions.is_empty() {
1030 return Ok(Vec::new());
1031 }
1032 let _timer = self.metrics.read_many_timer();
1033 self.metrics.read_many_calls.inc();
1034 self.validate_read_many(positions)?;
1035 let (items, misses) = self.probe_parts(positions);
1036 complete(self, items, misses).await
1037 }
1038
1039 fn try_read_sync(&self, position: u64) -> Option<V> {
1040 self.validate_readable(position).ok()?;
1041 let offset = self.offsets.try_read_sync(position)?;
1042 let mut buf = Vec::new();
1043 let item = self.try_read_frame_sync(position, offset, &mut buf)?;
1044 self.metrics.cache_hits.inc();
1045 self.metrics.items_read.inc();
1046 Some(item)
1047 }
1048
1049 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
1050 assert!(
1051 positions.is_sorted_by(|a, b| a < b),
1052 "positions must be strictly increasing"
1053 );
1054 let mut items: Vec<Option<V>> = (0..positions.len()).map(|_| None).collect();
1055 self.read_many_sync_pass(positions, &mut items);
1056 items
1057 }
1058
1059 async fn replay(
1060 &self,
1061 start_pos: u64,
1062 buffer: NonZeroUsize,
1063 ) -> Result<impl Stream<Item = Result<(u64, V), Error>> + Send, Error> {
1064 let states = self.replay_states(start_pos, buffer).await?;
1065
1066 Ok(super::replay_stream_from_states(states))
1067 }
1068}
1069
1070impl<E: Context, V: CodecShared> Journal<E, V> {
1071 fn mark_dirty_from(&mut self, blob: u64) {
1073 self.dirty_from_blob = Some(
1074 self.dirty_from_blob
1075 .map_or(blob, |existing| existing.min(blob)),
1076 );
1077 }
1078
1079 #[boxed]
1086 pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
1087 let items_per_blob = cfg.items_per_section.get();
1088 let data_partition = cfg.data_partition();
1089 let data_context = context.child("data");
1090
1091 let mut offsets = fixed::Journal::<E, u64>::init_cleared(
1095 context.child("offsets"),
1096 fixed::Config {
1097 partition: cfg.offsets_partition(),
1098 items_per_blob: cfg.items_per_section,
1099 page_cache: cfg.page_cache.clone(),
1100 write_buffer: cfg.write_buffer,
1101 },
1102 || Partition::<E>::remove_all(&data_context, &data_partition),
1103 )
1104 .await?;
1105
1106 let partition = Partition::new(
1107 data_context,
1108 data_partition,
1109 cfg.page_cache,
1110 cfg.write_buffer,
1111 );
1112 let mut pending = partition.open_all().await?;
1113
1114 let bounds = Self::align(
1116 &partition,
1117 &mut pending,
1118 &mut offsets,
1119 items_per_blob,
1120 &cfg.codec_config,
1121 cfg.compression.is_some(),
1122 )
1123 .await?;
1124
1125 let tail_blob = position_to_blob(bounds.end, items_per_blob);
1127 let blobs = Writable::recover(partition, pending, tail_blob).await?;
1128
1129 let metrics = Metrics::new(context);
1134 metrics.update(bounds.end, bounds.start, items_per_blob);
1135
1136 Ok(Self {
1137 blobs,
1138 offsets,
1139 bounds,
1140 dirty_from_blob: None,
1141 #[cfg(test)]
1142 halt_before_offsets_prune: false,
1143 items_per_blob: cfg.items_per_section,
1144 compression: cfg.compression,
1145 codec_config: cfg.codec_config,
1146 metrics: Arc::new(metrics),
1147 })
1148 }
1149
1150 #[commonware_macros::stability(ALPHA)]
1159 pub async fn init_at_size(context: E, cfg: Config<V::Cfg>, size: u64) -> Result<Self, Error> {
1160 let items_per_blob = cfg.items_per_section.get();
1161 let data_partition = cfg.data_partition();
1162 let data_context = context.child("data");
1163 let offsets_partition = cfg.offsets_partition();
1164 let offsets_context = context.child("offsets");
1165
1166 Partition::select(&offsets_context, &offsets_partition).await?;
1168
1169 let offsets = fixed::Journal::<E, u64>::init_at_size_cleared(
1173 offsets_context,
1174 fixed::Config {
1175 partition: offsets_partition,
1176 items_per_blob: cfg.items_per_section,
1177 page_cache: cfg.page_cache.clone(),
1178 write_buffer: cfg.write_buffer,
1179 },
1180 size,
1181 || Partition::<E>::remove_all(&data_context, &data_partition),
1182 )
1183 .await?;
1184
1185 let partition = Partition::new(
1186 data_context,
1187 data_partition,
1188 cfg.page_cache,
1189 cfg.write_buffer,
1190 );
1191 let blobs = Writable::recover(
1192 partition,
1193 BTreeMap::new(),
1194 position_to_blob(size, items_per_blob),
1195 )
1196 .await?;
1197
1198 let metrics = Metrics::new(context);
1199 metrics.update(size, size, items_per_blob);
1200
1201 Ok(Self {
1202 blobs,
1203 offsets,
1204 bounds: size..size,
1205 dirty_from_blob: None,
1206 #[cfg(test)]
1207 halt_before_offsets_prune: false,
1208 items_per_blob: cfg.items_per_section,
1209 compression: cfg.compression,
1210 codec_config: cfg.codec_config,
1211 metrics: Arc::new(metrics),
1212 })
1213 }
1214
1215 #[commonware_macros::stability(ALPHA)]
1241 pub(crate) async fn init_sync(
1242 context: E,
1243 cfg: Config<V::Cfg>,
1244 range: Range<u64>,
1245 ) -> Result<Self, Error> {
1246 assert!(!range.is_empty(), "range must not be empty");
1247
1248 debug!(
1249 range.start,
1250 range.end,
1251 items_per_blob = cfg.items_per_section.get(),
1252 "initializing contiguous variable journal for sync"
1253 );
1254
1255 let mut journal = Self::init(context.child("journal"), cfg.clone()).await?;
1257
1258 let size = journal.size();
1259
1260 if size == 0 {
1262 if range.start == 0 {
1263 debug!("no existing journal data, returning empty journal");
1264 return Ok(journal);
1265 } else {
1266 debug!(
1267 range.start,
1268 "no existing journal data, resetting to sync range start"
1269 );
1270 journal.clear_to_size(range.start).await?;
1271 return Ok(journal);
1272 }
1273 }
1274
1275 let bounds = journal.bounds.clone();
1278 if bounds.is_empty() && bounds.start > range.start {
1279 journal.clear_to_size(range.start).await?;
1280 return Ok(journal);
1281 }
1282
1283 if size > range.end {
1285 return Err(Error::ItemOutOfRange(size));
1286 }
1287
1288 if size <= range.start {
1290 debug!(
1291 size,
1292 range.start, "existing journal data is stale, resetting to start position"
1293 );
1294 journal.clear_to_size(range.start).await?;
1295 return Ok(journal);
1296 }
1297
1298 if !bounds.is_empty() && bounds.start < range.start {
1300 debug!(
1301 oldest_pos = bounds.start,
1302 range.start, "pruning journal to sync range start"
1303 );
1304 journal.prune(range.start).await?;
1305 }
1306
1307 Ok(journal)
1308 }
1309
1310 pub async fn rewind(&mut self, size: u64) -> Result<(), Error> {
1325 match size.cmp(&self.bounds.end) {
1326 std::cmp::Ordering::Greater => return Err(Error::InvalidRewind(size)),
1327 std::cmp::Ordering::Equal => return Ok(()),
1328 std::cmp::Ordering::Less => {}
1329 }
1330
1331 if size < self.bounds.start {
1333 return Err(Error::ItemPruned(size));
1334 }
1335
1336 let discard_blob = position_to_blob(size, self.items_per_blob.get());
1337
1338 let discard_offset = self.offsets.read(size).await?;
1340
1341 self.offsets.rewind(size).await?;
1347
1348 if discard_blob == self.blobs.tail_blob_index() {
1349 self.blobs.rewind_tail(discard_offset).await?;
1350 } else {
1351 self.blobs
1352 .rewind_into_sealed(discard_blob, discard_offset)
1353 .await?;
1354 }
1355
1356 self.bounds.end = size;
1357 self.mark_dirty_from(discard_blob);
1358 self.metrics.update(
1359 self.bounds.end,
1360 self.bounds.start,
1361 self.items_per_blob.get(),
1362 );
1363
1364 Ok(())
1365 }
1366
1367 pub async fn append(&mut self, item: &V) -> Result<u64, Error> {
1377 let _timer = self.metrics.append_timer();
1378 self.metrics.append_calls.inc();
1379 self.append_many_inner(Many::Flat(std::slice::from_ref(item)))
1380 .await
1381 }
1382
1383 pub async fn append_many<'a>(&'a mut self, items: Many<'a, V>) -> Result<u64, Error> {
1387 let _timer = self.metrics.append_many_timer();
1388 self.metrics.append_many_calls.inc();
1389 self.append_many_inner(items).await
1390 }
1391
1392 async fn append_many_inner<'a>(&'a mut self, items: Many<'a, V>) -> Result<u64, Error> {
1393 self.write_encoded(self.prepare_append(items)?).await
1394 }
1395
1396 pub fn prepare_append(&self, items: Many<'_, V>) -> Result<PreparedAppend<V>, Error> {
1401 let mut encoded = Vec::new();
1402 let mut item_starts = Vec::with_capacity(items.len());
1403 let mut encode = |item: &V| {
1404 item_starts.push(encoded.len());
1405 encode_frame_into(self.compression, item, &mut encoded)
1406 };
1407 match items {
1408 Many::Flat(items) => {
1409 for item in items {
1410 encode(item)?;
1411 }
1412 }
1413 Many::Nested(nested_items) => {
1414 for items in nested_items {
1415 for item in *items {
1416 encode(item)?;
1417 }
1418 }
1419 }
1420 }
1421 Ok(PreparedAppend {
1422 encoded,
1423 item_starts,
1424 compressed: self.compression.is_some(),
1425 _marker: PhantomData,
1426 })
1427 }
1428
1429 pub async fn append_prepared(&mut self, prepared: PreparedAppend<V>) -> Result<u64, Error> {
1436 let _timer = self.metrics.append_prepared_timer();
1437 self.metrics.append_prepared_calls.inc();
1438 self.write_encoded(prepared).await
1439 }
1440
1441 async fn write_encoded(&mut self, prepared: PreparedAppend<V>) -> Result<u64, Error> {
1443 let PreparedAppend {
1444 encoded,
1445 item_starts,
1446 compressed,
1447 ..
1448 } = prepared;
1449 let items_count = item_starts.len();
1450 if items_count == 0 {
1451 return Err(Error::EmptyAppend);
1452 }
1453 if compressed != self.compression.is_some() {
1454 return Err(Error::InvalidConfiguration(
1455 "prepared append compression setting does not match journal".into(),
1456 ));
1457 }
1458 let encoded = IoBuf::from(encoded);
1459
1460 self.bounds
1463 .end
1464 .checked_add(items_count as u64)
1465 .ok_or(Error::SizeOverflow)?;
1466
1467 let items_per_blob = self.items_per_blob.get();
1468 self.mark_dirty_from(position_to_blob(self.bounds.end, items_per_blob));
1469 let mut written = 0;
1470 while written < items_count {
1471 let batch_count = super::batch_count_to_blob_boundary(
1472 self.bounds.end,
1473 items_count - written,
1474 items_per_blob,
1475 );
1476 let batch_start = item_starts[written];
1477 let batch_end = item_starts
1478 .get(written + batch_count)
1479 .copied()
1480 .unwrap_or(encoded.len());
1481
1482 let base_offset = self
1486 .blobs
1487 .tail_writer()
1488 .append_owned(encoded.slice(batch_start..batch_end))
1489 .await
1490 .map_err(Error::Runtime)?;
1491
1492 let absolute_offsets = item_starts[written..written + batch_count]
1493 .iter()
1494 .map(|&start| {
1495 base_offset
1496 .checked_add((start - batch_start) as u64)
1497 .ok_or(Error::OffsetOverflow)
1498 })
1499 .collect::<Result<Vec<u64>, _>>()?;
1500
1501 let last_offsets_pos = self
1503 .offsets
1504 .append_many(Many::Flat(&absolute_offsets))
1505 .await?;
1506 assert_eq!(last_offsets_pos, self.bounds.end + batch_count as u64 - 1);
1507
1508 self.bounds.end += batch_count as u64;
1509 written += batch_count;
1510
1511 if self.bounds.end.is_multiple_of(items_per_blob) {
1514 self.blobs.seal_tail().await?;
1515 }
1516 }
1517
1518 self.metrics.update(
1519 self.bounds.end,
1520 self.bounds.start,
1521 self.items_per_blob.get(),
1522 );
1523 Ok(self.bounds.end - 1)
1524 }
1525
1526 pub async fn snapshot(&mut self) -> Result<Reader<'static, E, V>, Error> {
1532 Ok(Reader {
1533 data: self.blobs.snapshot().await?,
1534 bounds: self.bounds.clone(),
1535 offsets: self.offsets.snapshot().await?,
1536 items_per_blob: self.items_per_blob,
1537 codec_config: self.codec_config.clone(),
1538 compressed: self.compression.is_some(),
1539 metrics: self.metrics.clone(),
1540 })
1541 }
1542
1543 fn reader(&self) -> Reader<'_, E, V> {
1545 Reader {
1546 data: self.blobs.reader(),
1547 bounds: self.bounds.clone(),
1548 offsets: self.offsets.reader(),
1549 items_per_blob: self.items_per_blob,
1550 codec_config: self.codec_config.clone(),
1551 compressed: self.compression.is_some(),
1552 metrics: self.metrics.clone(),
1553 }
1554 }
1555
1556 pub const fn size(&self) -> u64 {
1559 self.bounds.end
1560 }
1561
1562 pub async fn prune(&mut self, min_position: u64) -> Result<bool, Error> {
1570 let items_per_blob = self.items_per_blob.get();
1571
1572 let target_blob = position_to_blob(min_position, items_per_blob);
1575 let tail_blob = position_to_blob(self.bounds.end, items_per_blob);
1576 let min_blob = target_blob.min(tail_blob);
1577
1578 if min_blob <= self.blobs.oldest_blob_index() {
1579 return Ok(false);
1580 }
1581
1582 let new_boundary = blob_first_position(min_blob, items_per_blob)?;
1583
1584 self.flush_dirty_data().await?;
1596 self.dirty_from_blob = None;
1597 self.offsets.commit().await?;
1598
1599 self.blobs.prune(min_blob).await?;
1600 self.bounds.start = new_boundary;
1601
1602 #[cfg(test)]
1603 if self.halt_before_offsets_prune {
1604 std::future::pending::<()>().await;
1605 }
1606
1607 self.offsets.prune(new_boundary).await?;
1610 self.metrics.update(
1611 self.bounds.end,
1612 self.bounds.start,
1613 self.items_per_blob.get(),
1614 );
1615
1616 Ok(true)
1617 }
1618
1619 async fn flush_dirty_data(&mut self) -> Result<(), Error> {
1621 let Some(start_blob) = self.dirty_from_blob else {
1622 return Ok(());
1623 };
1624 self.blobs.sync_from(start_blob).await
1625 }
1626
1627 pub async fn commit(&mut self) -> Result<(), Error> {
1632 let _timer = self.metrics.commit_timer();
1633 self.metrics.commit_calls.inc();
1634 self.flush_dirty_data().await?;
1635 self.dirty_from_blob = None;
1636 Ok(())
1637 }
1638
1639 pub async fn sync(&mut self) -> Result<(), Error> {
1641 let _timer = self.metrics.sync_timer();
1642 self.metrics.sync_calls.inc();
1643 self.flush_dirty_data().await?;
1644 self.offsets.sync().await?;
1645 self.dirty_from_blob = None;
1646 Ok(())
1647 }
1648
1649 pub async fn destroy(self) -> Result<(), Error> {
1659 self.blobs.destroy().await?;
1660 self.offsets.destroy().await
1661 }
1662
1663 #[commonware_macros::stability(ALPHA)]
1670 pub(crate) async fn clear_to_size(&mut self, new_size: u64) -> Result<(), Error> {
1671 self.offsets.stage_clear_intent(new_size).await?;
1674 self.blobs
1675 .clear(position_to_blob(new_size, self.items_per_blob.get()))
1676 .await?;
1677 self.offsets.clear_to_size(new_size).await?;
1678
1679 self.bounds = new_size..new_size;
1680 self.dirty_from_blob = None;
1681 self.metrics.update(
1682 self.bounds.end,
1683 self.bounds.start,
1684 self.items_per_blob.get(),
1685 );
1686 Ok(())
1687 }
1688
1689 async fn scan_blob(
1691 writer: &mut Writer<E::Blob>,
1692 codec_config: &V::Cfg,
1693 compressed: bool,
1694 ) -> Result<BlobScan, Error> {
1695 let replay = writer
1696 .replay(REPLAY_BUFFER_SIZE)
1697 .await
1698 .map_err(Error::Runtime)?;
1699 let mut scanner = FrameScanner::<E::Blob, V>::new(replay, codec_config, compressed);
1700 let mut items = 0u64;
1701 loop {
1702 match scanner.next().await? {
1703 Frame::Item { .. } => items += 1,
1704 Frame::End { valid_size, torn } => {
1705 return Ok(BlobScan {
1706 items,
1707 valid_size,
1708 torn,
1709 })
1710 }
1711 }
1712 }
1713 }
1714
1715 async fn align(
1724 partition: &Partition<E>,
1725 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1726 offsets: &mut fixed::Journal<E, u64>,
1727 items_per_blob: u64,
1728 codec_config: &V::Cfg,
1729 compressed: bool,
1730 ) -> Result<Range<u64>, Error> {
1731 let scanned: Vec<u64> = pending.keys().rev().copied().collect();
1734 let mut items_in_newest = 0;
1735 let mut newest_blob = None;
1736 for &blob in &scanned {
1737 let writer = pending.get_mut(&blob).expect("blob came from pending");
1738 let scan = Self::scan_blob(writer, codec_config, compressed).await?;
1739 if scan.items > items_per_blob {
1740 return Err(Error::Corruption(format!(
1741 "blob {blob} has too many items: expected at most {items_per_blob}, got {}",
1742 scan.items
1743 )));
1744 }
1745 if scan.torn {
1746 warn!(
1747 blob,
1748 new_size = scan.valid_size,
1749 "crash repair: truncating trailing bytes"
1750 );
1751 writer
1752 .resize(scan.valid_size)
1753 .await
1754 .map_err(Error::Runtime)?;
1755 writer.sync().await.map_err(Error::Runtime)?;
1756 }
1757 if scan.items > 0 {
1758 items_in_newest = scan.items;
1759 newest_blob = Some(blob);
1760 break;
1761 }
1762 }
1763
1764 let tail_blob = match newest_blob {
1768 Some(blob) if items_in_newest == items_per_blob => blob.saturating_add(1),
1769 Some(blob) => blob,
1770 None => pending.keys().next().copied().unwrap_or(0),
1771 };
1772 for &blob in &scanned {
1773 if blob <= tail_blob {
1774 break;
1775 }
1776 warn!(blob, "crash repair: removing empty trailing data blob");
1777 pending.remove(&blob);
1778 partition.remove(blob).await?;
1779 }
1780
1781 let Some(newest_blob) = newest_blob else {
1782 return Self::align_empty(partition, pending, offsets, items_per_blob).await;
1783 };
1784
1785 let oldest_blob = *pending.keys().next().expect("pending is non-empty");
1789 let data_oldest_pos = blob_first_position(oldest_blob, items_per_blob)?;
1790 {
1791 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1792
1793 if offsets_bounds.end < data_oldest_pos {
1796 return Err(Error::Corruption(format!(
1797 "offsets journal size {} is behind data oldest position {data_oldest_pos}",
1798 offsets_bounds.end
1799 )));
1800 }
1801 let offsets_start_blob = position_to_blob(offsets_bounds.start, items_per_blob);
1802 match offsets_start_blob.cmp(&oldest_blob) {
1803 std::cmp::Ordering::Less => {
1804 warn!("crash repair: pruning offsets journal to {data_oldest_pos}");
1805 offsets.prune(data_oldest_pos).await?;
1806 }
1807 std::cmp::Ordering::Equal => {}
1808 std::cmp::Ordering::Greater => {
1809 return Err(Error::Corruption(format!(
1812 "offsets start blob {offsets_start_blob} ahead of \
1813 oldest data blob {oldest_blob}"
1814 )));
1815 }
1816 }
1817 }
1818
1819 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1821
1822 let retained_data_end_bound = blob_first_position(newest_blob, items_per_blob)?
1825 .max(offsets_bounds.start)
1826 .checked_add(items_in_newest)
1827 .ok_or(Error::OffsetOverflow)?;
1828 let data_sync_start =
1829 Self::recovery_anchor(offsets, &offsets_bounds, retained_data_end_bound)?;
1830
1831 let data_size = Self::rebuild_offsets_from_anchor(
1833 partition,
1834 pending,
1835 offsets,
1836 items_per_blob,
1837 data_sync_start,
1838 codec_config,
1839 compressed,
1840 )
1841 .await?;
1842
1843 let pruning_boundary = {
1846 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1847 if offsets_bounds.end != data_size {
1848 return Err(Error::Corruption(format!(
1849 "recovered offsets end {} does not match data size {data_size}",
1850 offsets_bounds.end
1851 )));
1852 }
1853
1854 if !offsets_bounds.is_empty()
1859 && position_to_blob(offsets_bounds.start, items_per_blob) != oldest_blob
1860 {
1861 return Err(Error::Corruption(format!(
1862 "recovered offsets and data start in different blobs: {} != {oldest_blob}",
1863 position_to_blob(offsets_bounds.start, items_per_blob)
1864 )));
1865 }
1866
1867 offsets_bounds.start
1869 };
1870
1871 Self::sync_data_range(pending, data_sync_start, data_size, items_per_blob).await?;
1874 offsets.sync().await?;
1875 Ok(pruning_boundary..data_size)
1876 }
1877
1878 async fn align_empty(
1884 partition: &Partition<E>,
1885 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1886 offsets: &mut fixed::Journal<E, u64>,
1887 items_per_blob: u64,
1888 ) -> Result<Range<u64>, Error> {
1889 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1890
1891 let Some(&blob) = pending.keys().next() else {
1892 let size = offsets_bounds.end;
1896 if !offsets_bounds.is_empty() {
1897 warn!("crash repair: clearing offsets to {size} (prune-all crash)");
1898 offsets.clear_to_size(size).await?;
1899 }
1900 return Ok(size..size);
1901 };
1902
1903 let blob_start = blob_first_position(blob, items_per_blob)?;
1906 let target = blob_start.max(offsets_bounds.start);
1907
1908 let aligned = position_to_blob(target, items_per_blob) == blob;
1910 if aligned && offsets_bounds == (target..target) {
1911 return Ok(target..target);
1912 }
1913
1914 if !aligned {
1917 warn!(blob, "crash repair: removing empty data blob");
1918 pending.remove(&blob);
1919 partition.remove(blob).await?;
1920 }
1921 warn!("crash repair: clearing offsets to {target} (empty data)");
1922 offsets.clear_to_size(target).await?;
1923 Ok(target..target)
1924 }
1925
1926 fn recovery_anchor(
1929 offsets: &fixed::Journal<E, u64>,
1930 offsets_bounds: &Range<u64>,
1931 retained_data_end_bound: u64,
1932 ) -> Result<u64, Error> {
1933 let recovery_watermark = offsets.recovery_watermark();
1934 if recovery_watermark > offsets_bounds.end {
1935 return Err(Error::Corruption(format!(
1938 "offsets recovery watermark {recovery_watermark} exceeds offsets size {}",
1939 offsets_bounds.end
1940 )));
1941 }
1942 if recovery_watermark < offsets_bounds.start {
1943 warn!(
1944 recovery_watermark,
1945 start = offsets_bounds.start,
1946 end = offsets_bounds.end,
1947 retained_data_end_bound,
1948 "crash repair: offsets recovery watermark is unusable, rebuilding from offsets start"
1949 );
1950 return Ok(offsets_bounds.start);
1951 }
1952 if recovery_watermark > retained_data_end_bound {
1953 return Err(Error::Corruption(format!(
1954 "offsets recovery watermark {recovery_watermark} exceeds retained data end \
1955 {retained_data_end_bound} (offsets bounds {}..{})",
1956 offsets_bounds.start, offsets_bounds.end
1957 )));
1958 }
1959 Ok(recovery_watermark)
1960 }
1961
1962 async fn sync_data_range(
1964 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1965 start_position: u64,
1966 end_position: u64,
1967 items_per_blob: u64,
1968 ) -> Result<(), Error> {
1969 if start_position >= end_position {
1970 return Ok(());
1971 }
1972
1973 let start_blob = position_to_blob(start_position, items_per_blob);
1974 let end_blob = position_to_blob(end_position - 1, items_per_blob);
1975 futures::future::try_join_all(
1976 pending
1977 .range_mut(start_blob..=end_blob)
1978 .map(|(_, writer)| writer.sync()),
1979 )
1980 .await
1981 .map_err(Error::Runtime)?;
1982 Ok(())
1983 }
1984
1985 async fn rebuild_offsets_from_anchor(
1991 partition: &Partition<E>,
1992 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1993 offsets: &mut fixed::Journal<E, u64>,
1994 items_per_blob: u64,
1995 anchor: u64,
1996 codec_config: &V::Cfg,
1997 compressed: bool,
1998 ) -> Result<u64, Error> {
1999 assert!(
2000 !pending.is_empty(),
2001 "rebuild_offsets called with no data blobs"
2002 );
2003
2004 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
2005 let data_too_short = || {
2006 if anchor == offsets_bounds.start {
2007 Error::Corruption(format!(
2008 "data blobs shorter than pruning boundary {}",
2009 offsets_bounds.start
2010 ))
2011 } else {
2012 Error::Corruption(format!(
2013 "data blobs shorter than offsets recovery watermark {anchor}"
2014 ))
2015 }
2016 };
2017 if anchor < offsets_bounds.start || anchor > offsets_bounds.end {
2018 return Err(data_too_short());
2019 }
2020
2021 if offsets_bounds.end > anchor {
2022 offsets.rewind(anchor).await?;
2023 }
2024
2025 let start_blob = position_to_blob(anchor, items_per_blob);
2026 let first_position = offsets_bounds
2027 .start
2028 .max(blob_first_position(start_blob, items_per_blob)?);
2029
2030 let mut skip = anchor - first_position;
2033 let mut size = anchor;
2034 let mut blob = start_blob;
2035 loop {
2036 let Some(writer) = pending.get_mut(&blob) else {
2037 if skip > 0 {
2038 return Err(data_too_short());
2040 }
2041 if pending.keys().next_back().is_some_and(|&n| n > blob) {
2044 warn!(
2045 blob,
2046 size, "crash repair: truncating data after missing blob"
2047 );
2048 Self::remove_blobs_after(partition, pending, blob).await?;
2049 }
2050 return Ok(size);
2051 };
2052
2053 let replay = writer
2054 .replay(REPLAY_BUFFER_SIZE)
2055 .await
2056 .map_err(Error::Runtime)?;
2057 let mut scanner = FrameScanner::<E::Blob, V>::new(replay, codec_config, compressed);
2058 let blob_end_pos = super::blob_end_position(blob, items_per_blob, u64::MAX);
2059
2060 let end = loop {
2061 if size == blob_end_pos {
2062 match scanner.next().await? {
2065 Frame::Item { .. } => {
2066 return Err(Error::Corruption(format!(
2067 "blob {blob} over capacity at logical position {size}"
2068 )));
2069 }
2070 Frame::End {
2071 valid_size,
2072 torn: true,
2073 } => break Some((valid_size, true)),
2074 Frame::End { .. } => break None,
2075 }
2076 }
2077 match scanner.next().await? {
2078 Frame::Item { offset } => {
2079 if skip > 0 {
2080 skip -= 1;
2081 } else {
2082 offsets.append(&offset).await?;
2083 size += 1;
2084 }
2085 }
2086 Frame::End { valid_size, torn } => break Some((valid_size, torn)),
2087 }
2088 };
2089
2090 if let Some((valid_size, torn)) = end {
2091 if skip > 0 {
2093 return Err(data_too_short());
2095 }
2096 if torn {
2097 warn!(
2098 blob,
2099 new_size = valid_size,
2100 "crash repair: truncating trailing bytes"
2101 );
2102 writer.resize(valid_size).await.map_err(Error::Runtime)?;
2103 writer.sync().await.map_err(Error::Runtime)?;
2104 }
2105 if pending.keys().next_back().is_some_and(|&n| n > blob) {
2108 warn!(blob, size, "crash repair: truncating data after short blob");
2109 Self::remove_blobs_after(partition, pending, blob).await?;
2110 }
2111 return Ok(size);
2112 }
2113
2114 blob = blob.checked_add(1).ok_or(Error::OffsetOverflow)?;
2115 }
2116 }
2117
2118 async fn remove_blobs_after(
2120 partition: &Partition<E>,
2121 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
2122 blob: u64,
2123 ) -> Result<(), Error> {
2124 while let Some((&newest, _)) = pending.last_key_value() {
2125 if newest <= blob {
2126 break;
2127 }
2128 drop(pending.remove(&newest));
2129 partition.remove(newest).await?;
2130 }
2131 Ok(())
2132 }
2133}
2134
2135impl<E: Context, V: CodecShared> Contiguous for Journal<E, V> {
2136 type Item = V;
2137
2138 fn bounds(&self) -> Range<u64> {
2139 self.bounds.clone()
2140 }
2141
2142 async fn read(&self, position: u64) -> Result<V, Error> {
2143 self.reader().read(position).await
2144 }
2145
2146 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
2147 self.reader().read_many(positions).await
2148 }
2149
2150 fn try_read_sync(&self, position: u64) -> Option<V> {
2151 self.reader().try_read_sync(position)
2152 }
2153
2154 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
2155 self.reader().try_read_many_sync(positions)
2156 }
2157
2158 async fn replay(
2159 &self,
2160 start_pos: u64,
2161 buffer: NonZeroUsize,
2162 ) -> Result<impl Stream<Item = Result<(u64, V), Error>> + Send, Error> {
2163 let reader = self.reader();
2164 let states = reader.replay_states(start_pos, buffer).await?;
2165
2166 Ok(super::replay_stream_from_states(states))
2167 }
2168}
2169
2170impl<E: Context, V: CodecShared> Mutable for Journal<E, V> {
2171 async fn append(&mut self, item: &Self::Item) -> Result<u64, Error> {
2172 Self::append(self, item).await
2173 }
2174
2175 async fn append_many<'a>(&'a mut self, items: Many<'a, Self::Item>) -> Result<u64, Error> {
2176 Self::append_many(self, items).await
2177 }
2178
2179 async fn prune(&mut self, min_position: u64) -> Result<bool, Error> {
2180 Self::prune(self, min_position).await
2181 }
2182
2183 async fn rewind(&mut self, size: u64) -> Result<(), Error> {
2184 Self::rewind(self, size).await
2185 }
2186
2187 async fn commit(&mut self) -> Result<(), Error> {
2188 Self::commit(self).await
2189 }
2190
2191 async fn sync(&mut self) -> Result<(), Error> {
2192 Self::sync(self).await
2193 }
2194
2195 async fn destroy(self) -> Result<(), Error> {
2196 Self::destroy(self).await
2197 }
2198}
2199
2200#[commonware_macros::stability(ALPHA)]
2201impl<E: Context, V: CodecShared> authenticated::Inner<E> for Journal<E, V> {
2202 type Config = Config<V::Cfg>;
2203
2204 async fn init<
2205 F: merkle::Family,
2206 H: commonware_cryptography::Hasher,
2207 S: commonware_parallel::Strategy,
2208 >(
2209 context: E,
2210 merkle_cfg: merkle::full::Config<S>,
2211 journal_cfg: Self::Config,
2212 rewind_predicate: fn(&V) -> bool,
2213 bagging: merkle::Bagging,
2214 ) -> Result<authenticated::Journal<F, E, Self, H, S>, authenticated::Error<F>> {
2215 authenticated::Journal::<F, E, Self, H, S>::new(
2216 context,
2217 merkle_cfg,
2218 journal_cfg,
2219 rewind_predicate,
2220 bagging,
2221 )
2222 .await
2223 }
2224}
2225
2226#[cfg(test)]
2227impl<E: Context, V: CodecShared> Journal<E, V> {
2228 pub(crate) async fn test_prune_data(&mut self, min_blob: u64) -> Result<bool, Error> {
2230 let min_blob = min_blob.min(self.blobs.tail_blob_index());
2231 if min_blob <= self.blobs.oldest_blob_index() {
2232 return Ok(false);
2233 }
2234 self.blobs.prune(min_blob).await?;
2235 Ok(true)
2236 }
2237
2238 pub(crate) async fn test_prune_offsets(&mut self, position: u64) -> Result<bool, Error> {
2240 self.offsets.prune(position).await
2241 }
2242
2243 pub(crate) async fn test_rewind_offsets(&mut self, position: u64) -> Result<(), Error> {
2245 self.offsets.rewind(position).await
2246 }
2247
2248 pub(crate) async fn test_set_offsets_recovery_watermark(
2250 &mut self,
2251 watermark: u64,
2252 ) -> Result<(), Error> {
2253 self.offsets.test_set_recovery_watermark(watermark).await
2254 }
2255
2256 pub(crate) fn test_offsets_size(&self) -> u64 {
2258 self.offsets.size()
2259 }
2260
2261 pub(crate) async fn test_rewind_data_to_position(
2263 &mut self,
2264 position: u64,
2265 ) -> Result<(), Error> {
2266 let offset = self.offsets.read(position).await?;
2267 let blob = position_to_blob(position, self.items_per_blob.get());
2268 if blob == self.blobs.tail_blob_index() {
2269 self.blobs.rewind_tail(offset).await
2270 } else {
2271 self.blobs.rewind_into_sealed(blob, offset).await
2272 }
2273 }
2274
2275 pub(crate) async fn test_append_data(
2278 &mut self,
2279 blob: u64,
2280 item: V,
2281 ) -> Result<(u64, u32), Error> {
2282 let mut encoded = Vec::new();
2283 encode_frame_into(self.compression, &item, &mut encoded)?;
2284 let item_len = encoded.len() as u32;
2285
2286 let tail_blob = self.blobs.tail_blob_index();
2287 if blob == tail_blob {
2288 let writer = self.blobs.tail_writer();
2289 let offset = writer.size();
2290 writer.append(&encoded).await.map_err(Error::Runtime)?;
2291 return Ok((offset, item_len));
2292 }
2293 assert!(blob > tail_blob, "cannot append to a sealed blob");
2294 let mut writer = self.blobs.open_blob(blob).await?;
2295 let offset = writer.size();
2296 writer.append(&encoded).await.map_err(Error::Runtime)?;
2297 writer.sync().await.map_err(Error::Runtime)?;
2298 Ok((offset, item_len))
2299 }
2300
2301 pub(crate) async fn test_sync_data(&mut self) -> Result<(), Error> {
2303 self.blobs.sync_from(self.blobs.tail_blob_index()).await
2304 }
2305
2306 pub(crate) async fn test_sync_data_blob(&mut self, blob: u64) -> Result<(), Error> {
2308 self.blobs.sync_blob(blob).await
2309 }
2310}
2311
2312#[cfg(test)]
2313mod tests {
2314 use super::*;
2315 use crate::journal::contiguous::tests::run_contiguous_tests;
2316 use commonware_macros::test_traced;
2317 use commonware_runtime::{
2318 buffer::paged::{CacheRef, Writer},
2319 deterministic, Metrics as _, Runner, Spawner as _, Storage, Supervisor as _,
2320 };
2321 use commonware_utils::{sequence::FixedBytes, NZUsize, NZU16, NZU64};
2322 use futures::{FutureExt as _, StreamExt as _};
2323 use std::num::NonZeroU16;
2324
2325 const PAGE_SIZE: NonZeroU16 = NZU16!(101);
2327 const PAGE_CACHE_SIZE: usize = 2;
2328 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(1024);
2330 const SMALL_PAGE_SIZE: NonZeroU16 = NZU16!(512);
2331
2332 fn counter(buffer: &str, name: &str) -> u64 {
2334 buffer
2335 .lines()
2336 .find(|l| l.contains(name) && !l.starts_with('#'))
2337 .and_then(|l| l.split_whitespace().last())
2338 .and_then(|v| v.parse().ok())
2339 .expect("counter missing")
2340 }
2341
2342 #[test_traced]
2343 fn test_variable_init_syncs_adopted_data_before_offsets_watermark_advance() {
2344 let executor = deterministic::Runner::default();
2345 executor.start(|context| async move {
2346 let cfg = Config {
2347 partition: "init-adopted-variable".into(),
2348 items_per_section: NZU64!(10),
2349 compression: None,
2350 codec_config: (),
2351 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2352 write_buffer: NZUsize!(1024),
2353 };
2354
2355 let mut journal =
2356 Journal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone())
2357 .await
2358 .unwrap();
2359 journal.append(&FixedBytes::new([1; 32])).await.unwrap();
2360 journal.append(&FixedBytes::new([2; 32])).await.unwrap();
2361 journal.sync().await.unwrap();
2362 journal
2365 .test_set_offsets_recovery_watermark(1)
2366 .await
2367 .unwrap();
2368 drop(journal);
2369
2370 let data_partition = format!("{}{}", cfg.partition, DATA_SUFFIX);
2375 let context = commonware_runtime::mocks::SyncFaultContext {
2376 inner: context,
2377 fail_partition: data_partition,
2378 };
2379 assert!(
2380 Journal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone())
2381 .await
2382 .is_err(),
2383 "init must sync adopted data before advancing rebuilt offsets"
2384 );
2385 });
2386 }
2387
2388 #[test_traced]
2389 fn test_variable_append_many_compressed() {
2390 let executor = deterministic::Runner::default();
2391 executor.start(|context| async move {
2392 let cfg = Config {
2393 partition: "append-many-compressed".into(),
2394 items_per_section: NZU64!(3),
2395 compression: Some(1),
2396 codec_config: (),
2397 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2398 write_buffer: NZUsize!(1024),
2399 };
2400 let mut journal = Journal::<_, FixedBytes<32>>::init(context.child("journal"), cfg)
2401 .await
2402 .unwrap();
2403 let items = [
2404 FixedBytes::new([0; 32]),
2405 FixedBytes::new([1; 32]),
2406 FixedBytes::new([2; 32]),
2407 FixedBytes::new([3; 32]),
2408 FixedBytes::new([4; 32]),
2409 ];
2410
2411 let last = journal.append_many(Many::Flat(&items)).await.unwrap();
2412 assert_eq!(last, 4);
2413 for (pos, item) in items.iter().enumerate() {
2414 assert_eq!(journal.read(pos as u64).await.unwrap(), *item);
2415 }
2416
2417 journal.destroy().await.unwrap();
2418 });
2419 }
2420
2421 #[test_traced]
2422 fn test_variable_append_many_exceeding_write_buffer_reopens_across_sections() {
2423 let executor = deterministic::Runner::default();
2424 executor.start(|context| async move {
2425 let cfg = Config {
2426 partition: "append-many-exceeds-buffer".into(),
2427 items_per_section: NZU64!(5),
2428 compression: None,
2429 codec_config: (),
2430 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2431 write_buffer: NZUsize!(512),
2432 };
2433 let items = (0..13)
2434 .map(|i| FixedBytes::new([i as u8; 300]))
2435 .collect::<Vec<_>>();
2436
2437 let mut journal =
2438 Journal::<_, FixedBytes<300>>::init(context.child("first"), cfg.clone())
2439 .await
2440 .unwrap();
2441 assert_eq!(journal.append_many(Many::Flat(&items)).await.unwrap(), 12);
2442 journal.sync().await.unwrap();
2443 drop(journal);
2444
2445 let journal = Journal::<_, FixedBytes<300>>::init(context.child("second"), cfg)
2446 .await
2447 .unwrap();
2448 assert_eq!(journal.bounds(), 0..13);
2449 for (pos, item) in items.iter().enumerate() {
2450 assert_eq!(journal.read(pos as u64).await.unwrap(), *item);
2451 }
2452 assert_eq!(
2453 journal.read_many(&[0, 4, 5, 9, 10, 12]).await.unwrap(),
2454 vec![
2455 items[0].clone(),
2456 items[4].clone(),
2457 items[5].clone(),
2458 items[9].clone(),
2459 items[10].clone(),
2460 items[12].clone(),
2461 ]
2462 );
2463
2464 journal.destroy().await.unwrap();
2465 });
2466 }
2467
2468 #[test_traced]
2469 fn test_variable_init_at_max_size_rejected() {
2470 let executor = deterministic::Runner::default();
2471 executor.start(|context| async move {
2472 let cfg = Config {
2473 partition: "init-at-max".into(),
2474 items_per_section: NZU64!(5),
2475 compression: None,
2476 codec_config: (),
2477 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2478 write_buffer: NZUsize!(1024),
2479 };
2480
2481 assert!(matches!(
2483 Journal::<_, u64>::init_at_size(context.child("max"), cfg, u64::MAX).await,
2484 Err(Error::SizeOverflow)
2485 ));
2486 });
2487 }
2488
2489 #[test_traced]
2490 fn test_variable_append_size_overflow() {
2491 let executor = deterministic::Runner::default();
2492 executor.start(|context| async move {
2493 let cfg = Config {
2494 partition: "append-size-overflow".into(),
2495 items_per_section: NZU64!(5),
2496 compression: None,
2497 codec_config: (),
2498 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2499 write_buffer: NZUsize!(1024),
2500 };
2501
2502 let mut journal =
2504 Journal::<_, u64>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
2505 .await
2506 .unwrap();
2507
2508 assert_eq!(journal.append(&7).await.unwrap(), u64::MAX - 1);
2510 assert_eq!(journal.size(), u64::MAX);
2511
2512 assert!(matches!(journal.append(&8).await, Err(Error::SizeOverflow)));
2515
2516 journal.destroy().await.unwrap();
2517 });
2518 }
2519
2520 #[test_traced]
2521 fn test_variable_replay_near_max_size() {
2522 let executor = deterministic::Runner::default();
2523 executor.start(|context| async move {
2524 let cfg = Config {
2525 partition: "replay-near-max-size".into(),
2526 items_per_section: NZU64!(10),
2527 compression: None,
2528 codec_config: (),
2529 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2530 write_buffer: NZUsize!(1024),
2531 };
2532
2533 let mut journal =
2534 Journal::<_, u64>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
2535 .await
2536 .unwrap();
2537 assert_eq!(journal.append(&7).await.unwrap(), u64::MAX - 1);
2538
2539 {
2540 let reader = journal.snapshot().await.unwrap();
2541 let stream = reader.replay(u64::MAX - 1, NZUsize!(20)).await.unwrap();
2542 futures::pin_mut!(stream);
2543 let (pos, item) = stream.next().await.unwrap().unwrap();
2544 assert_eq!(pos, u64::MAX - 1);
2545 assert_eq!(item, 7);
2546 assert!(stream.next().await.is_none());
2547 }
2548
2549 journal.destroy().await.unwrap();
2550 });
2551 }
2552
2553 #[test_traced]
2554 fn test_variable_try_read_many_sync_matches_read_many() {
2555 let executor = deterministic::Runner::default();
2558 executor.start(|context| async move {
2559 let cfg = Config {
2560 partition: "read-many-sync".into(),
2561 items_per_section: NZU64!(5),
2562 compression: None,
2563 codec_config: (),
2564 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(64)),
2565 write_buffer: NZUsize!(1024),
2566 };
2567 let items = (0..13)
2568 .map(|i| FixedBytes::new([i as u8; 300]))
2569 .collect::<Vec<_>>();
2570 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
2571 .await
2572 .unwrap();
2573 journal.append_many(Many::Flat(&items)).await.unwrap();
2574 journal.sync().await.unwrap();
2575
2576 let positions: Vec<u64> = (0..items.len() as u64).collect();
2577 let reader = journal.snapshot().await.unwrap();
2578 let expected = reader.read_many(&positions).await.unwrap();
2581 let served = reader.try_read_many_sync(&positions);
2582 assert_eq!(served.len(), positions.len());
2583 for (item, expected) in served.iter().zip(&expected) {
2584 assert_eq!(item.as_ref().expect("cached position is served"), expected);
2585 }
2586
2587 let served = reader.try_read_many_sync(&[9, 13]);
2591 assert!(served[0].is_some());
2592 assert!(served[1].is_none());
2593 drop(served);
2594 drop(reader);
2595
2596 journal.destroy().await.unwrap();
2597 });
2598 }
2599
2600 #[test_traced]
2601 #[should_panic(expected = "positions must be strictly increasing")]
2602 fn test_variable_read_many_rejects_unsorted_positions() {
2603 let executor = deterministic::Runner::default();
2604 executor.start(|context| async move {
2605 let cfg = Config {
2606 partition: "read-many-unsorted".into(),
2607 items_per_section: NZU64!(5),
2608 compression: None,
2609 codec_config: (),
2610 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2611 write_buffer: NZUsize!(1024),
2612 };
2613 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
2614 .await
2615 .unwrap();
2616 for i in 0..5u64 {
2617 journal.append(&(i * 100)).await.unwrap();
2618 }
2619 journal.sync().await.unwrap();
2620
2621 let reader = journal.snapshot().await.unwrap();
2622 let _ = reader.read_many(&[2, 1]).await;
2623 });
2624 }
2625
2626 #[test_traced]
2627 #[should_panic(expected = "positions must be strictly increasing")]
2628 fn test_variable_read_many_rejects_duplicate_positions() {
2629 let executor = deterministic::Runner::default();
2631 executor.start(|context| async move {
2632 let cfg = Config {
2633 partition: "read-many-duplicate".into(),
2634 items_per_section: NZU64!(5),
2635 compression: None,
2636 codec_config: (),
2637 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2638 write_buffer: NZUsize!(1024),
2639 };
2640 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
2641 .await
2642 .unwrap();
2643 for i in 0..5u64 {
2644 journal.append(&(i * 100)).await.unwrap();
2645 }
2646 journal.sync().await.unwrap();
2647
2648 let reader = journal.snapshot().await.unwrap();
2649 let _ = reader.read_many(&[1, 1]).await;
2650 });
2651 }
2652
2653 #[test_traced]
2654 fn test_variable_probe_then_read_many_matches_read_many() {
2655 let executor = deterministic::Runner::default();
2658 executor.start(|context| async move {
2659 let cfg = Config {
2660 partition: "read-many-probe-complete".into(),
2661 items_per_section: NZU64!(5),
2662 compression: None,
2663 codec_config: (),
2664 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2665 write_buffer: NZUsize!(1024),
2666 };
2667 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
2668 .await
2669 .unwrap();
2670 for i in 0..12u64 {
2671 journal.append(&(i * 100)).await.unwrap();
2672 }
2673 journal.sync().await.unwrap();
2674
2675 let positions: Vec<u64> = (0..12).collect();
2676 let expected: Vec<u64> = (0..12).map(|i| i * 100).collect();
2677 let reader = journal.snapshot().await.unwrap();
2678 for _ in 0..2 {
2679 let mut served = reader.try_read_many_sync(&positions);
2680 let misses: Vec<u64> = positions
2681 .iter()
2682 .zip(&served)
2683 .filter_map(|(&pos, item)| item.is_none().then_some(pos))
2684 .collect();
2685 let mut fetched = reader.read_many(&misses).await.unwrap().into_iter();
2686 for item in served.iter_mut().filter(|item| item.is_none()) {
2687 *item = fetched.next();
2688 }
2689 let completed: Vec<_> = served.into_iter().map(Option::unwrap).collect();
2690 assert_eq!(completed, expected);
2691 }
2692 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
2693 drop(reader);
2694
2695 journal.destroy().await.unwrap();
2696 });
2697 }
2698
2699 #[test_traced]
2700 fn test_variable_read_many_reuses_probed_offset() {
2701 let executor = deterministic::Runner::default();
2704 executor.start(|context| async move {
2705 let cfg = Config {
2709 partition: "read-many-offset-reuse".into(),
2710 items_per_section: NZU64!(128),
2711 compression: None,
2712 codec_config: (),
2713 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(4)),
2714 write_buffer: NZUsize!(1024),
2715 };
2716 let appended = (0..140)
2717 .map(|i| FixedBytes::new([i as u8; 300]))
2718 .collect::<Vec<_>>();
2719 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
2720 .await
2721 .unwrap();
2722 journal.append_many(Many::Flat(&appended)).await.unwrap();
2723 journal.sync().await.unwrap();
2724 let reader = journal.snapshot().await.unwrap();
2725
2726 reader
2731 .read_many(&(128..136).collect::<Vec<u64>>())
2732 .await
2733 .unwrap();
2734 reader.read(4).await.unwrap();
2735
2736 let (items, misses) = reader.probe_parts(&[0]);
2738 assert!(items[0].is_none());
2739 assert_eq!(misses[0].offset, Some(0));
2740
2741 let before = context.encode();
2745 assert_eq!(
2746 reader.read_many(&[0]).await.unwrap(),
2747 vec![appended[0].clone()]
2748 );
2749 let after = context.encode();
2750 assert_eq!(
2751 counter(&after, "offsets_items_read_total"),
2752 counter(&before, "offsets_items_read_total") + 2
2753 );
2754 drop(reader);
2755
2756 journal.destroy().await.unwrap();
2757 });
2758 }
2759
2760 #[test_traced]
2761 fn test_variable_read_many_consecutive_after_reopen() {
2762 let executor = deterministic::Runner::default();
2763 executor.start(|context| async move {
2764 let cfg = Config {
2765 partition: "read-many-consecutive-after-reopen".into(),
2766 items_per_section: NZU64!(20),
2767 compression: None,
2768 codec_config: (),
2769 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2770 write_buffer: NZUsize!(1024),
2771 };
2772
2773 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2774 .await
2775 .unwrap();
2776 for i in 0..20u64 {
2777 journal.append(&(i * 100)).await.unwrap();
2778 }
2779 journal.sync().await.unwrap();
2780 drop(journal);
2781
2782 let cfg = Config {
2783 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2784 ..cfg
2785 };
2786 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
2787 .await
2788 .unwrap();
2789 let reader = journal.snapshot().await.unwrap();
2790 let positions: Vec<u64> = (3..10).collect();
2791 let items = reader.read_many(&positions).await.unwrap();
2792 assert_eq!(items, vec![300, 400, 500, 600, 700, 800, 900]);
2793 drop(reader);
2794
2795 journal.destroy().await.unwrap();
2796 });
2797 }
2798
2799 #[test_traced]
2800 fn test_variable_read_many_scattered_single_runs_across_blobs() {
2801 let executor = deterministic::Runner::default();
2807 executor.start(|context| async move {
2808 let cfg = Config {
2809 partition: "read-many-scattered-runs".into(),
2810 items_per_section: NZU64!(5),
2811 compression: None,
2812 codec_config: (),
2813 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(16)),
2814 write_buffer: NZUsize!(1024),
2815 };
2816
2817 let items = (0..30)
2821 .map(|i| FixedBytes::new([i as u8; 300]))
2822 .collect::<Vec<_>>();
2823 let mut journal =
2824 Journal::<_, FixedBytes<300>>::init(context.child("first"), cfg.clone())
2825 .await
2826 .unwrap();
2827 for item in &items {
2828 journal.append(item).await.unwrap();
2829 }
2830 journal.sync().await.unwrap();
2831 drop(journal);
2832
2833 let cfg = Config {
2835 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(16)),
2836 ..cfg
2837 };
2838 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("second"), cfg)
2839 .await
2840 .unwrap();
2841 let reader = journal.snapshot().await.unwrap();
2842
2843 reader.read(6).await.unwrap();
2846 reader.read(16).await.unwrap();
2847
2848 for hit in [5, 6, 15, 17] {
2852 assert!(reader.try_read_sync(hit).is_some(), "position {hit}");
2853 }
2854 let misses = [0, 3, 10, 12, 20, 21, 23];
2855 for miss in misses {
2856 assert!(reader.try_read_sync(miss).is_none(), "position {miss}");
2857 }
2858
2859 let positions = [0, 3, 5, 6, 10, 12, 15, 17, 20, 21, 23];
2862 let expected: Vec<_> = positions
2863 .iter()
2864 .map(|&p| items[p as usize].clone())
2865 .collect();
2866 let before = context.encode();
2867 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
2868
2869 let after = context.encode();
2871 assert_eq!(
2872 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
2873 4
2874 );
2875 assert_eq!(
2876 counter(&after, "second_cache_misses") - counter(&before, "second_cache_misses"),
2877 misses.len() as u64
2878 );
2879
2880 let before = context.encode();
2882 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
2883 let after = context.encode();
2884 assert_eq!(
2885 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
2886 positions.len() as u64
2887 );
2888 assert_eq!(
2889 counter(&after, "second_cache_misses"),
2890 counter(&before, "second_cache_misses")
2891 );
2892 drop(reader);
2893
2894 journal.destroy().await.unwrap();
2895 });
2896 }
2897
2898 #[test_traced]
2904 fn test_variable_offsets_partition_loss_after_prune_unrecoverable() {
2905 let executor = deterministic::Runner::default();
2906 executor.start(|context| async move {
2907 let cfg = Config {
2908 partition: "offsets-loss-after-prune".into(),
2909 items_per_section: NZU64!(10),
2910 compression: None,
2911 codec_config: (),
2912 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
2913 write_buffer: NZUsize!(1024),
2914 };
2915
2916 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2918 .await
2919 .unwrap();
2920
2921 for i in 0..40u64 {
2923 journal.append(&(i * 100)).await.unwrap();
2924 }
2925
2926 journal.prune(20).await.unwrap();
2928 let bounds = journal.bounds();
2929 assert_eq!(bounds.start, 20);
2930 assert_eq!(bounds.end, 40);
2931
2932 journal.sync().await.unwrap();
2933 drop(journal);
2934
2935 context
2938 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
2939 .await
2940 .expect("Failed to remove offsets blobs partition");
2941 context
2942 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
2943 .await
2944 .expect("Failed to remove offsets metadata partition");
2945
2946 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
2948 assert!(matches!(result, Err(Error::Corruption(_))));
2949 });
2950 }
2951
2952 #[test_traced]
2960 fn test_variable_align_data_offsets_mismatch() {
2961 let executor = deterministic::Runner::default();
2962 executor.start(|context| async move {
2963 let cfg = Config {
2964 partition: "data-loss-test".into(),
2965 items_per_section: NZU64!(10),
2966 compression: None,
2967 codec_config: (),
2968 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
2969 write_buffer: NZUsize!(1024),
2970 };
2971
2972 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2974 .await
2975 .unwrap();
2976
2977 for i in 0..20u64 {
2979 variable.append(&(i * 100)).await.unwrap();
2980 }
2981
2982 variable.sync().await.unwrap();
2983 drop(variable);
2984
2985 context
2987 .remove(&cfg.data_partition(), None)
2988 .await
2989 .expect("Failed to remove data partition");
2990
2991 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
2993 .await
2994 .expect("Should align offsets to match empty data");
2995
2996 assert_eq!(journal.size(), 20);
2998
2999 assert!(journal.bounds().is_empty());
3001
3002 for i in 0..20 {
3004 assert!(matches!(
3005 journal.read(i).await,
3006 Err(crate::journal::Error::ItemPruned(_))
3007 ));
3008 }
3009
3010 let pos = journal.append(&999).await.unwrap();
3012 assert_eq!(pos, 20);
3013 assert_eq!(journal.read(20).await.unwrap(), 999);
3014
3015 journal.destroy().await.unwrap();
3016 });
3017 }
3018
3019 #[test_traced]
3021 fn test_variable_replay() {
3022 let executor = deterministic::Runner::default();
3023 executor.start(|context| async move {
3024 let cfg = Config {
3025 partition: "replay".into(),
3026 items_per_section: NZU64!(10),
3027 compression: None,
3028 codec_config: (),
3029 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3030 write_buffer: NZUsize!(1024),
3031 };
3032
3033 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
3035
3036 for i in 0..40u64 {
3038 journal.append(&(i * 100)).await.unwrap();
3039 }
3040
3041 {
3043 let reader = journal.snapshot().await.unwrap();
3044 let stream = reader.replay(0, NZUsize!(20)).await.unwrap();
3045 futures::pin_mut!(stream);
3046 for i in 0..40u64 {
3047 let (pos, item) = stream.next().await.unwrap().unwrap();
3048 assert_eq!(pos, i);
3049 assert_eq!(item, i * 100);
3050 }
3051 assert!(stream.next().await.is_none());
3052 }
3053
3054 {
3056 let reader = journal.snapshot().await.unwrap();
3057 let stream = reader.replay(15, NZUsize!(20)).await.unwrap();
3058 futures::pin_mut!(stream);
3059 for i in 15..40u64 {
3060 let (pos, item) = stream.next().await.unwrap().unwrap();
3061 assert_eq!(pos, i);
3062 assert_eq!(item, i * 100);
3063 }
3064 assert!(stream.next().await.is_none());
3065 }
3066
3067 {
3069 let reader = journal.snapshot().await.unwrap();
3070 let stream = reader.replay(20, NZUsize!(20)).await.unwrap();
3071 futures::pin_mut!(stream);
3072 for i in 20..40u64 {
3073 let (pos, item) = stream.next().await.unwrap().unwrap();
3074 assert_eq!(pos, i);
3075 assert_eq!(item, i * 100);
3076 }
3077 assert!(stream.next().await.is_none());
3078 }
3079
3080 journal.prune(20).await.unwrap();
3082 {
3083 let reader = journal.snapshot().await.unwrap();
3084 let res = reader.replay(0, NZUsize!(20)).await;
3085 assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_))));
3086 }
3087 {
3088 let reader = journal.snapshot().await.unwrap();
3089 let res = reader.replay(19, NZUsize!(20)).await;
3090 assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_))));
3091 }
3092
3093 {
3095 let reader = journal.snapshot().await.unwrap();
3096 let stream = reader.replay(20, NZUsize!(20)).await.unwrap();
3097 futures::pin_mut!(stream);
3098 for i in 20..40u64 {
3099 let (pos, item) = stream.next().await.unwrap().unwrap();
3100 assert_eq!(pos, i);
3101 assert_eq!(item, i * 100);
3102 }
3103 assert!(stream.next().await.is_none());
3104 }
3105
3106 {
3108 let reader = journal.snapshot().await.unwrap();
3109 let stream = reader.replay(40, NZUsize!(20)).await.unwrap();
3110 futures::pin_mut!(stream);
3111 assert!(stream.next().await.is_none());
3112 }
3113
3114 {
3116 let reader = journal.snapshot().await.unwrap();
3117 let res = reader.replay(41, NZUsize!(20)).await;
3118 assert!(matches!(
3119 res,
3120 Err(crate::journal::Error::ItemOutOfRange(41))
3121 ));
3122 }
3123
3124 journal.destroy().await.unwrap();
3125 });
3126 }
3127
3128 #[test_traced]
3129 fn test_variable_replay_stops_after_error() {
3130 let executor = deterministic::Runner::default();
3131 executor.start(|context| async move {
3132 let cfg = Config {
3133 partition: "replay-stops-after-error".into(),
3134 items_per_section: NZU64!(10),
3135 compression: None,
3136 codec_config: (),
3137 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3138 write_buffer: NZUsize!(1024),
3139 };
3140
3141 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
3142 .await
3143 .unwrap();
3144 for i in 0..30u64 {
3145 journal.append(&(i * 100)).await.unwrap();
3146 }
3147 journal.sync().await.unwrap();
3148
3149 let (blob, _) = context
3150 .open(&cfg.data_partition(), &1u64.to_be_bytes())
3151 .await
3152 .unwrap();
3153 blob.write_at_sync(0, vec![0xFF; 1]).await.unwrap();
3154
3155 {
3156 let cache = CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10));
3157 let mut writers = Vec::new();
3158 for blob_index in 0..3u64 {
3159 let (blob, size) = context
3160 .open(&cfg.data_partition(), &blob_index.to_be_bytes())
3161 .await
3162 .unwrap();
3163 writers.push(
3164 Writer::new(blob, size, cfg.write_buffer.get(), cache.clone())
3165 .await
3166 .unwrap(),
3167 );
3168 }
3169
3170 let mut states = Vec::new();
3171 for (blob_index, writer) in writers.iter().enumerate() {
3172 let blob = blob_index as u64;
3173 states.push(ReplayState::<_, u64> {
3174 blob,
3175 replay: Blob::Writer(writer).replay_from(0, NZUsize!(1024)).unwrap(),
3176 budget: 1024,
3177 pos: blob * 10,
3178 end_pos: (blob + 1) * 10,
3179 offset: 0,
3180 codec_config: (),
3181 compressed: false,
3182 _marker: PhantomData,
3183 });
3184 }
3185
3186 let stream = crate::journal::contiguous::replay_stream_from_states(states);
3187 futures::pin_mut!(stream);
3188
3189 for i in 0..10u64 {
3190 let (pos, item) = stream.next().await.unwrap().unwrap();
3191 assert_eq!(pos, i);
3192 assert_eq!(item, i * 100);
3193 }
3194 assert!(matches!(
3195 stream.next().await.unwrap(),
3196 Err(Error::Corruption(_))
3197 ));
3198 assert!(stream.next().await.is_none());
3199 }
3200
3201 journal.destroy().await.unwrap();
3202 });
3203 }
3204
3205 #[test_traced]
3206 fn test_variable_contiguous() {
3207 let executor = deterministic::Runner::default();
3208 executor.start(|context| async move {
3209 run_contiguous_tests(move |test_name: String, idx: usize| {
3210 let label = test_name.replace('-', "_");
3211 let context = context
3212 .child("test")
3213 .with_attribute("name", &label)
3214 .with_attribute("index", idx);
3215 async move {
3216 let cfg = Config {
3217 partition: format!("generic-test-{test_name}"),
3218 items_per_section: NZU64!(10),
3219 compression: None,
3220 codec_config: (),
3221 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3222 write_buffer: NZUsize!(1024),
3223 };
3224 Journal::<_, u64>::init(context, cfg).await
3225 }
3226 .boxed()
3227 })
3228 .await;
3229 });
3230 }
3231
3232 #[test_traced]
3234 fn test_variable_multiple_sequential_prunes() {
3235 let executor = deterministic::Runner::default();
3236 executor.start(|context| async move {
3237 let cfg = Config {
3238 partition: "sequential-prunes".into(),
3239 items_per_section: NZU64!(10),
3240 compression: None,
3241 codec_config: (),
3242 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3243 write_buffer: NZUsize!(1024),
3244 };
3245
3246 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
3247
3248 for i in 0..40u64 {
3250 journal.append(&(i * 100)).await.unwrap();
3251 }
3252
3253 let bounds = journal.bounds();
3255 assert_eq!(bounds.start, 0);
3256 assert_eq!(bounds.end, 40);
3257
3258 let pruned = journal.prune(10).await.unwrap();
3260 assert!(pruned);
3261
3262 assert_eq!(journal.bounds().start, 10);
3264
3265 assert!(matches!(
3267 journal.read(0).await,
3268 Err(crate::journal::Error::ItemPruned(_))
3269 ));
3270 assert_eq!(journal.read(10).await.unwrap(), 1000);
3271 assert_eq!(journal.read(19).await.unwrap(), 1900);
3272
3273 let pruned = journal.prune(20).await.unwrap();
3275 assert!(pruned);
3276
3277 assert_eq!(journal.bounds().start, 20);
3279
3280 assert!(matches!(
3282 journal.read(10).await,
3283 Err(crate::journal::Error::ItemPruned(_))
3284 ));
3285 assert!(matches!(
3286 journal.read(19).await,
3287 Err(crate::journal::Error::ItemPruned(_))
3288 ));
3289 assert_eq!(journal.read(20).await.unwrap(), 2000);
3290 assert_eq!(journal.read(29).await.unwrap(), 2900);
3291
3292 let pruned = journal.prune(30).await.unwrap();
3294 assert!(pruned);
3295
3296 assert_eq!(journal.bounds().start, 30);
3298
3299 assert!(matches!(
3301 journal.read(20).await,
3302 Err(crate::journal::Error::ItemPruned(_))
3303 ));
3304 assert!(matches!(
3305 journal.read(29).await,
3306 Err(crate::journal::Error::ItemPruned(_))
3307 ));
3308 assert_eq!(journal.read(30).await.unwrap(), 3000);
3309 assert_eq!(journal.read(39).await.unwrap(), 3900);
3310
3311 assert_eq!(journal.size(), 40);
3313
3314 journal.destroy().await.unwrap();
3315 });
3316 }
3317
3318 #[test_traced]
3320 fn test_variable_prune_all_then_reinit() {
3321 let executor = deterministic::Runner::default();
3322 executor.start(|context| async move {
3323 let cfg = Config {
3324 partition: "prune-all-reinit".into(),
3325 items_per_section: NZU64!(10),
3326 compression: None,
3327 codec_config: (),
3328 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3329 write_buffer: NZUsize!(1024),
3330 };
3331
3332 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3334 .await
3335 .unwrap();
3336
3337 for i in 0..100u64 {
3338 journal.append(&(i * 100)).await.unwrap();
3339 }
3340
3341 let bounds = journal.bounds();
3342 assert_eq!(bounds.end, 100);
3343 assert_eq!(bounds.start, 0);
3344
3345 let pruned = journal.prune(100).await.unwrap();
3347 assert!(pruned);
3348
3349 let bounds = journal.bounds();
3351 assert_eq!(bounds.end, 100);
3352 assert!(bounds.is_empty());
3353
3354 for i in 0..100 {
3356 assert!(matches!(
3357 journal.read(i).await,
3358 Err(crate::journal::Error::ItemPruned(_))
3359 ));
3360 }
3361
3362 journal.sync().await.unwrap();
3363 drop(journal);
3364
3365 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3367 .await
3368 .unwrap();
3369
3370 let bounds = journal.bounds();
3372 assert_eq!(bounds.end, 100);
3373 assert!(bounds.is_empty());
3374
3375 for i in 0..100 {
3377 assert!(matches!(
3378 journal.read(i).await,
3379 Err(crate::journal::Error::ItemPruned(_))
3380 ));
3381 }
3382
3383 journal.append(&10000).await.unwrap();
3386 let bounds = journal.bounds();
3387 assert_eq!(bounds.end, 101);
3388 assert_eq!(bounds.start, 100);
3390
3391 assert_eq!(journal.read(100).await.unwrap(), 10000);
3393
3394 assert!(matches!(
3396 journal.read(99).await,
3397 Err(crate::journal::Error::ItemPruned(_))
3398 ));
3399
3400 journal.destroy().await.unwrap();
3401 });
3402 }
3403
3404 #[test_traced]
3406 fn test_variable_recovery_prune_crash_offsets_behind() {
3407 let executor = deterministic::Runner::default();
3408 executor.start(|context| async move {
3409 let cfg = Config {
3411 partition: "recovery-prune-crash".into(),
3412 items_per_section: NZU64!(10),
3413 compression: None,
3414 codec_config: (),
3415 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3416 write_buffer: NZUsize!(1024),
3417 };
3418
3419 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3420 .await
3421 .unwrap();
3422
3423 for i in 0..40u64 {
3425 variable.append(&(i * 100)).await.unwrap();
3426 }
3427
3428 variable.prune(10).await.unwrap();
3430 assert_eq!(variable.bounds().start, 10);
3431
3432 variable.test_prune_data(2).await.unwrap();
3435 variable.sync().await.unwrap();
3438 drop(variable);
3439
3440 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3442 .await
3443 .unwrap();
3444
3445 let bounds = variable.bounds();
3447 assert_eq!(bounds.start, 20);
3448 assert_eq!(bounds.end, 40);
3449
3450 assert!(matches!(
3452 variable.read(10).await,
3453 Err(crate::journal::Error::ItemPruned(_))
3454 ));
3455
3456 assert_eq!(variable.read(20).await.unwrap(), 2000);
3458 assert_eq!(variable.read(39).await.unwrap(), 3900);
3459
3460 variable.destroy().await.unwrap();
3461 });
3462 }
3463
3464 #[test_traced]
3467 fn test_variable_recovery_prune_crash_offsets_end_behind() {
3468 let executor = deterministic::Runner::default();
3469 executor.start(|context| async move {
3470 let cfg = Config {
3471 partition: "recovery-prune-offsets-end-behind".into(),
3472 items_per_section: NZU64!(10),
3473 compression: None,
3474 codec_config: (),
3475 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3476 write_buffer: NZUsize!(1024),
3477 };
3478
3479 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3480 .await
3481 .unwrap();
3482
3483 for i in 0..7u64 {
3486 journal.append(&(i * 100)).await.unwrap();
3487 }
3488 journal.sync().await.unwrap();
3489 for i in 7..12u64 {
3490 journal.append(&(i * 100)).await.unwrap();
3491 }
3492
3493 journal.halt_before_offsets_prune = true;
3497 {
3498 let fut = journal.prune(10);
3499 futures::pin_mut!(fut);
3500 assert!(
3501 futures::poll!(fut.as_mut()).is_pending(),
3502 "prune must park before offsets.prune"
3503 );
3504 }
3505 drop(journal);
3506
3507 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3508 .await
3509 .expect("prune crash must leave a recoverable journal");
3510 assert_eq!(journal.bounds(), 10..12);
3511 for i in 10..12u64 {
3512 assert_eq!(journal.read(i).await.unwrap(), i * 100);
3513 }
3514 journal.destroy().await.unwrap();
3515 });
3516 }
3517
3518 #[test_traced]
3523 fn test_variable_recovery_offsets_ahead_corruption() {
3524 let executor = deterministic::Runner::default();
3525 executor.start(|context| async move {
3526 let cfg = Config {
3528 partition: "recovery-offsets-ahead".into(),
3529 items_per_section: NZU64!(10),
3530 compression: None,
3531 codec_config: (),
3532 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3533 write_buffer: NZUsize!(1024),
3534 };
3535
3536 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3537 .await
3538 .unwrap();
3539
3540 for i in 0..40u64 {
3542 variable.append(&(i * 100)).await.unwrap();
3543 }
3544
3545 variable.test_prune_offsets(20).await.unwrap(); variable.test_prune_data(1).await.unwrap(); variable.sync().await.unwrap();
3550 drop(variable);
3551
3552 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
3554 assert!(matches!(result, Err(Error::Corruption(_))));
3555 });
3556 }
3557
3558 #[test_traced]
3561 fn test_variable_recovery_offsets_empty_different_blob_is_corruption() {
3562 let executor = deterministic::Runner::default();
3563 executor.start(|context| async move {
3564 let cfg = Config {
3565 partition: "offsets-empty-diff-blob".into(),
3566 items_per_section: NZU64!(10),
3567 compression: None,
3568 codec_config: (),
3569 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3570 write_buffer: NZUsize!(1024),
3571 };
3572
3573 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3574 .await
3575 .unwrap();
3576
3577 for i in 0..15u64 {
3578 journal.append(&(i * 100)).await.unwrap();
3579 }
3580 journal.sync().await.unwrap();
3581
3582 journal.offsets.clear_to_size(20).await.unwrap();
3585 drop(journal);
3586
3587 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
3588 assert!(matches!(result, Err(Error::Corruption(_))));
3589 });
3590 }
3591
3592 #[test_traced]
3595 fn test_variable_recovery_offsets_end_behind_data_oldest_is_corruption() {
3596 let executor = deterministic::Runner::default();
3597 executor.start(|context| async move {
3598 let cfg = Config {
3599 partition: "offsets-end-behind-data-oldest".into(),
3600 items_per_section: NZU64!(10),
3601 compression: None,
3602 codec_config: (),
3603 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3604 write_buffer: NZUsize!(1024),
3605 };
3606
3607 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3608 .await
3609 .unwrap();
3610
3611 for i in 0..15u64 {
3612 journal.append(&(i * 100)).await.unwrap();
3613 }
3614 journal.sync().await.unwrap();
3615
3616 journal.test_prune_data(1).await.unwrap();
3619 journal.test_rewind_offsets(5).await.unwrap();
3620 drop(journal);
3621
3622 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
3623 assert!(matches!(result, Err(Error::Corruption(_))));
3624 });
3625 }
3626
3627 #[test_traced]
3630 fn test_variable_recovery_offsets_start_mid_blob_ahead_of_data() {
3631 let executor = deterministic::Runner::default();
3632 executor.start(|context| async move {
3633 let cfg = Config {
3634 partition: "offsets-mid-blob-ahead".into(),
3635 items_per_section: NZU64!(10),
3636 compression: None,
3637 codec_config: (),
3638 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3639 write_buffer: NZUsize!(1024),
3640 };
3641
3642 let mut journal =
3646 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
3647 .await
3648 .unwrap();
3649 for i in 0..5u64 {
3650 journal.append(&(i * 100)).await.unwrap();
3651 }
3652 journal.sync().await.unwrap();
3653 drop(journal);
3654
3655 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3656 .await
3657 .unwrap();
3658 assert_eq!(journal.bounds(), 7..12);
3659 assert_eq!(journal.read(7).await.unwrap(), 0);
3660 assert_eq!(journal.read(11).await.unwrap(), 400);
3661 journal.destroy().await.unwrap();
3662 });
3663 }
3664
3665 #[test_traced]
3669 fn test_variable_recovery_watermark_below_offsets_start() {
3670 let executor = deterministic::Runner::default();
3671 executor.start(|context| async move {
3672 let cfg = Config {
3673 partition: "watermark-below-start".into(),
3674 items_per_section: NZU64!(10),
3675 compression: None,
3676 codec_config: (),
3677 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3678 write_buffer: NZUsize!(1024),
3679 };
3680
3681 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3682 .await
3683 .unwrap();
3684 for i in 0..25u64 {
3685 journal.append(&(i * 100)).await.unwrap();
3686 }
3687 journal.sync().await.unwrap();
3688
3689 journal.prune(10).await.unwrap();
3691 journal
3692 .test_set_offsets_recovery_watermark(5)
3693 .await
3694 .unwrap();
3695 drop(journal);
3696
3697 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3699 .await
3700 .unwrap();
3701 assert_eq!(journal.bounds(), 10..25);
3702 assert_eq!(journal.read(10).await.unwrap(), 1000);
3703 assert_eq!(journal.read(24).await.unwrap(), 2400);
3704 journal.destroy().await.unwrap();
3705 });
3706 }
3707
3708 #[test_traced]
3710 fn test_variable_recovery_append_crash_offsets_behind() {
3711 let executor = deterministic::Runner::default();
3712 executor.start(|context| async move {
3713 let cfg = Config {
3715 partition: "recovery-append-crash".into(),
3716 items_per_section: NZU64!(10),
3717 compression: None,
3718 codec_config: (),
3719 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3720 write_buffer: NZUsize!(1024),
3721 };
3722
3723 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3724 .await
3725 .unwrap();
3726
3727 for i in 0..15u64 {
3729 variable.append(&(i * 100)).await.unwrap();
3730 }
3731
3732 assert_eq!(variable.size(), 15);
3733
3734 for i in 15..20u64 {
3736 variable.test_append_data(1, i * 100).await.unwrap();
3737 }
3738 variable.sync().await.unwrap();
3741 drop(variable);
3742
3743 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3745 .await
3746 .unwrap();
3747
3748 let bounds = variable.bounds();
3750 assert_eq!(bounds.end, 20);
3751 assert_eq!(bounds.start, 0);
3752
3753 for i in 0..20u64 {
3755 assert_eq!(variable.read(i).await.unwrap(), i * 100);
3756 }
3757
3758 assert_eq!(variable.test_offsets_size(), 20);
3760
3761 variable.destroy().await.unwrap();
3762 });
3763 }
3764
3765 #[test_traced]
3766 fn test_variable_recovery_rejects_overlong_data_blob() {
3767 let executor = deterministic::Runner::default();
3768 executor.start(|context| async move {
3769 let cfg = Config {
3770 partition: "recovery-overlong-data-blob".into(),
3771 items_per_section: NZU64!(10),
3772 compression: None,
3773 codec_config: (),
3774 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3775 write_buffer: NZUsize!(1024),
3776 };
3777
3778 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3779 .await
3780 .unwrap();
3781
3782 for i in 0..11u64 {
3783 journal.test_append_data(0, i * 100).await.unwrap();
3784 }
3785 journal.test_sync_data().await.unwrap();
3786 drop(journal);
3787
3788 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
3789 assert!(matches!(result, Err(Error::Corruption(_))));
3790 });
3791 }
3792
3793 #[test_traced]
3798 fn test_variable_recovery_rejects_over_capacity_non_newest_blob() {
3799 let executor = deterministic::Runner::default();
3800 executor.start(|context| async move {
3801 let cfg = Config {
3802 partition: "recovery-over-capacity-non-newest".into(),
3803 items_per_section: NZU64!(10),
3804 compression: None,
3805 codec_config: (),
3806 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3807 write_buffer: NZUsize!(1024),
3808 };
3809
3810 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3811 .await
3812 .unwrap();
3813
3814 for i in 0..11u64 {
3816 journal.test_append_data(0, i * 100).await.unwrap();
3817 }
3818 journal.test_sync_data().await.unwrap();
3821 journal.test_append_data(1, 9999).await.unwrap();
3822 drop(journal);
3824
3825 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
3826 assert!(matches!(result, Err(Error::Corruption(_))));
3827 });
3828 }
3829
3830 #[test_traced]
3831 fn test_variable_recovery_handles_multiple_empty_data_tail_blobs() {
3832 let executor = deterministic::Runner::default();
3833 executor.start(|context| async move {
3834 let cfg = Config::<()> {
3835 partition: "recovery-empty-data-tail".into(),
3836 items_per_section: NZU64!(1),
3837 compression: None,
3838 codec_config: (),
3839 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3840 write_buffer: NZUsize!(1024),
3841 };
3842 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
3843 .await
3844 .unwrap();
3845
3846 assert_eq!(journal.append(&10).await.unwrap(), 0);
3850 journal.sync().await.unwrap();
3851 assert_eq!(journal.append(&20).await.unwrap(), 1);
3852 assert_eq!(journal.append(&30).await.unwrap(), 2);
3853 drop(journal);
3854
3855 let data_partition = cfg.data_partition();
3856 let data_blobs = context.scan(&data_partition).await.unwrap();
3857 assert_eq!(data_blobs.len(), 4);
3858 for name in &data_blobs[1..] {
3859 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
3860 assert_eq!(size, 0);
3861 }
3862
3863 let cfg = Config {
3866 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3867 ..cfg
3868 };
3869 let mut journal = Journal::<_, u64>::init(context.child("recovered"), cfg.clone())
3870 .await
3871 .unwrap();
3872 assert_eq!(journal.bounds(), 0..1);
3873 assert_eq!(journal.read(0).await.unwrap(), 10);
3874 assert_eq!(journal.append(&42).await.unwrap(), 1);
3875 assert_eq!(journal.read(1).await.unwrap(), 42);
3876 drop(journal);
3877
3878 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
3881 assert_eq!(data_blobs.len(), 3);
3882
3883 let journal = Journal::<_, u64>::init(context.child("recovered"), cfg)
3884 .await
3885 .unwrap();
3886 journal.destroy().await.unwrap();
3887 });
3888 }
3889
3890 #[test_traced]
3891 fn test_variable_recovery_handles_empty_data_with_no_durable_items() {
3892 let executor = deterministic::Runner::default();
3893 executor.start(|context| async move {
3894 let cfg = Config::<()> {
3895 partition: "recovery-empty-data-no-items".into(),
3896 items_per_section: NZU64!(1),
3897 compression: None,
3898 codec_config: (),
3899 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3900 write_buffer: NZUsize!(1024),
3901 };
3902 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
3903 .await
3904 .unwrap();
3905
3906 assert_eq!(journal.append(&10).await.unwrap(), 0);
3911 assert_eq!(journal.append(&20).await.unwrap(), 1);
3912 drop(journal);
3913
3914 let data_partition = cfg.data_partition();
3915 let data_blobs = context.scan(&data_partition).await.unwrap();
3916 assert_eq!(data_blobs.len(), 3);
3917 for name in &data_blobs {
3918 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
3919 assert_eq!(size, 0);
3920 }
3921
3922 let cfg = Config {
3923 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3924 ..cfg
3925 };
3926 let mut journal = Journal::<_, u64>::init(context.child("recovered"), cfg)
3927 .await
3928 .unwrap();
3929 assert_eq!(journal.bounds(), 0..0);
3930 assert_eq!(journal.append(&42).await.unwrap(), 0);
3931 assert_eq!(journal.read(0).await.unwrap(), 42);
3932 journal.destroy().await.unwrap();
3933 });
3934 }
3935
3936 #[test_traced]
3945 fn test_variable_recovery_rolls_back_durable_blob_after_gap() {
3946 let executor = deterministic::Runner::default();
3947 executor.start(|context| async move {
3948 let cfg = Config {
3949 partition: "recovery-rollback-after-gap".into(),
3950 items_per_section: NZU64!(10),
3951 compression: None,
3952 codec_config: (),
3953 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3954 write_buffer: NZUsize!(1024),
3955 };
3956
3957 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3958 .await
3959 .unwrap();
3960
3961 for i in 0..10u64 {
3963 journal.append(&(i * 100)).await.unwrap();
3964 }
3965 journal.sync().await.unwrap();
3966
3967 for i in 10..30u64 {
3971 journal.append(&(i * 100)).await.unwrap();
3972 }
3973 journal.test_sync_data_blob(2).await.unwrap();
3974 drop(journal);
3975
3976 let data_partition = cfg.data_partition();
3979 let mut names = context.scan(&data_partition).await.unwrap();
3980 names.sort();
3981 assert_eq!(names.len(), 4);
3982 let sizes = {
3983 let mut sizes = Vec::new();
3984 for name in &names {
3985 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
3986 sizes.push(size);
3987 }
3988 sizes
3989 };
3990 assert!(sizes[0] > 0, "blob 0 should be durable");
3991 assert_eq!(sizes[1], 0, "blob 1 should be the gap");
3992 assert!(sizes[2] > 0, "blob 2 should be incidentally durable");
3993 assert_eq!(sizes[3], 0, "blob 3 should be the empty tail");
3994
3995 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3998 .await
3999 .unwrap();
4000 assert_eq!(journal.bounds(), 0..10);
4001 for i in 0..10u64 {
4002 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4003 }
4004 assert!(matches!(
4005 journal.read(10).await,
4006 Err(Error::ItemOutOfRange(10))
4007 ));
4008
4009 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
4012 assert_eq!(data_blobs.len(), 2);
4013
4014 assert_eq!(journal.append(&1234).await.unwrap(), 10);
4016 assert_eq!(journal.read(10).await.unwrap(), 1234);
4017
4018 journal.destroy().await.unwrap();
4019 });
4020 }
4021
4022 #[test_traced]
4032 fn test_variable_recovery_empty_oldest_blob_orphaned_newer_blob() {
4033 let executor = deterministic::Runner::default();
4034 executor.start(|context| async move {
4035 let cfg = Config {
4036 partition: "recovery-empty-oldest-blob".into(),
4037 items_per_section: NZU64!(10),
4038 compression: None,
4039 codec_config: (),
4040 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4041 write_buffer: NZUsize!(1024),
4042 };
4043
4044 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4046 .await
4047 .unwrap();
4048 for i in 0..20u64 {
4049 journal.append(&(i * 100)).await.unwrap();
4050 }
4051 journal.sync().await.unwrap();
4052 drop(journal);
4053
4054 let data_partition = cfg.data_partition();
4057 let mut names = context.scan(&data_partition).await.unwrap();
4058 names.sort();
4059 assert_eq!(names.len(), 3);
4060 let (blob0, size0) = context.open(&data_partition, &names[0]).await.unwrap();
4061 assert!(size0 > 0, "blob 0 should start durable");
4062 blob0.resize(0).await.unwrap();
4063 blob0.sync().await.unwrap();
4064 context
4065 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
4066 .await
4067 .unwrap();
4068 context
4069 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
4070 .await
4071 .unwrap();
4072
4073 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4075 .await
4076 .unwrap();
4077 assert_eq!(journal.bounds(), 0..0);
4078 assert!(matches!(
4079 journal.read(0).await,
4080 Err(Error::ItemOutOfRange(0))
4081 ));
4082
4083 assert_eq!(journal.append(&42).await.unwrap(), 0);
4085 assert_eq!(journal.read(0).await.unwrap(), 42);
4086 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
4087 assert_eq!(
4088 data_blobs.len(),
4089 1,
4090 "orphaned newer blob should be truncated away"
4091 );
4092
4093 journal.destroy().await.unwrap();
4094 });
4095 }
4096
4097 #[test_traced]
4103 fn test_variable_recovery_clean_short_oldest_blob_orphaned_newer_blob() {
4104 let executor = deterministic::Runner::default();
4105 executor.start(|context| async move {
4106 let cfg = Config {
4107 partition: "recovery-clean-short-oldest-blob".into(),
4108 items_per_section: NZU64!(64),
4109 compression: None,
4110 codec_config: (),
4111 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4112 write_buffer: NZUsize!(1024),
4113 };
4114
4115 let mut journal =
4118 Journal::<_, FixedBytes<31>>::init(context.child("first"), cfg.clone())
4119 .await
4120 .unwrap();
4121 for i in 0..128u8 {
4122 journal.append(&FixedBytes::new([i; 31])).await.unwrap();
4123 }
4124 journal.sync().await.unwrap();
4125 drop(journal);
4126
4127 let physical_page_size = LARGE_PAGE_SIZE.get() as u64 + 12;
4128 let items_in_page = LARGE_PAGE_SIZE.get() as u64 / 32;
4129 assert!(items_in_page < cfg.items_per_section.get());
4130
4131 let data_partition = cfg.data_partition();
4132 let mut names = context.scan(&data_partition).await.unwrap();
4133 names.sort();
4134 assert_eq!(names.len(), 3);
4135
4136 let (blob0, size0) = context.open(&data_partition, &names[0]).await.unwrap();
4139 assert!(size0 > physical_page_size);
4140 blob0.resize(physical_page_size).await.unwrap();
4141 blob0.sync().await.unwrap();
4142
4143 context
4146 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
4147 .await
4148 .unwrap();
4149 context
4150 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
4151 .await
4152 .unwrap();
4153
4154 let mut journal =
4157 Journal::<_, FixedBytes<31>>::init(context.child("second"), cfg.clone())
4158 .await
4159 .unwrap();
4160 assert_eq!(journal.bounds(), 0..items_in_page);
4161 assert_eq!(
4162 journal.read(items_in_page - 1).await.unwrap(),
4163 FixedBytes::new([(items_in_page - 1) as u8; 31])
4164 );
4165 assert!(matches!(
4166 journal.read(items_in_page).await,
4167 Err(Error::ItemOutOfRange(pos)) if pos == items_in_page
4168 ));
4169
4170 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
4171 assert_eq!(
4172 data_blobs.len(),
4173 1,
4174 "orphaned newer blob should be truncated away"
4175 );
4176
4177 assert_eq!(
4179 journal.append(&FixedBytes::new([42; 31])).await.unwrap(),
4180 items_in_page
4181 );
4182 assert_eq!(
4183 journal.read(items_in_page).await.unwrap(),
4184 FixedBytes::new([42; 31])
4185 );
4186
4187 journal.destroy().await.unwrap();
4188 });
4189 }
4190
4191 #[test_traced]
4198 fn test_variable_recovery_partial_sync_loop_keeps_contiguous_prefix() {
4199 let executor = deterministic::Runner::default();
4200 executor.start(|context| async move {
4201 let cfg = Config {
4202 partition: "recovery-partial-sync-loop".into(),
4203 items_per_section: NZU64!(10),
4204 compression: None,
4205 codec_config: (),
4206 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4207 write_buffer: NZUsize!(1024),
4208 };
4209
4210 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4211 .await
4212 .unwrap();
4213
4214 for i in 0..25u64 {
4217 journal.append(&(i * 100)).await.unwrap();
4218 }
4219
4220 journal.test_sync_data_blob(0).await.unwrap();
4223 journal.test_sync_data_blob(1).await.unwrap();
4224 drop(journal);
4225
4226 let data_partition = cfg.data_partition();
4229 let mut names = context.scan(&data_partition).await.unwrap();
4230 names.sort();
4231 assert_eq!(names.len(), 3);
4232 for (blob, name) in names.iter().enumerate() {
4233 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
4234 if blob < 2 {
4235 assert!(size > 0, "blob {blob} should be durable");
4236 } else {
4237 assert_eq!(size, 0, "blob {blob} should be empty");
4238 }
4239 }
4240
4241 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4244 .await
4245 .unwrap();
4246 assert_eq!(journal.bounds(), 0..20);
4247 for i in 0..20u64 {
4248 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4249 }
4250 assert!(matches!(
4251 journal.read(20).await,
4252 Err(Error::ItemOutOfRange(20))
4253 ));
4254
4255 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
4258 assert_eq!(data_blobs.len(), 3);
4259 assert_eq!(journal.append(&2000).await.unwrap(), 20);
4260 assert_eq!(journal.read(20).await.unwrap(), 2000);
4261
4262 journal.destroy().await.unwrap();
4263 });
4264 }
4265
4266 #[test_traced]
4268 fn test_variable_recovery_multiple_prunes_crash() {
4269 let executor = deterministic::Runner::default();
4270 executor.start(|context| async move {
4271 let cfg = Config {
4273 partition: "recovery-multiple-prunes".into(),
4274 items_per_section: NZU64!(10),
4275 compression: None,
4276 codec_config: (),
4277 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4278 write_buffer: NZUsize!(1024),
4279 };
4280
4281 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4282 .await
4283 .unwrap();
4284
4285 for i in 0..50u64 {
4287 variable.append(&(i * 100)).await.unwrap();
4288 }
4289
4290 variable.prune(10).await.unwrap();
4292 assert_eq!(variable.bounds().start, 10);
4293
4294 variable.test_prune_data(3).await.unwrap();
4297 variable.sync().await.unwrap();
4300 drop(variable);
4301
4302 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4304 .await
4305 .unwrap();
4306
4307 let bounds = variable.bounds();
4309 assert_eq!(bounds.start, 30);
4310 assert_eq!(bounds.end, 50);
4311
4312 assert!(matches!(
4314 variable.read(10).await,
4315 Err(crate::journal::Error::ItemPruned(_))
4316 ));
4317 assert!(matches!(
4318 variable.read(20).await,
4319 Err(crate::journal::Error::ItemPruned(_))
4320 ));
4321
4322 assert_eq!(variable.read(30).await.unwrap(), 3000);
4324 assert_eq!(variable.read(49).await.unwrap(), 4900);
4325
4326 variable.destroy().await.unwrap();
4327 });
4328 }
4329
4330 #[test_traced]
4336 fn test_variable_recovery_offsets_behind_data_multi_blob() {
4337 let executor = deterministic::Runner::default();
4338 executor.start(|context| async move {
4339 let cfg = Config {
4341 partition: "recovery-rewind-crash".into(),
4342 items_per_section: NZU64!(10),
4343 compression: None,
4344 codec_config: (),
4345 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4346 write_buffer: NZUsize!(1024),
4347 };
4348
4349 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4350 .await
4351 .unwrap();
4352
4353 for i in 0..25u64 {
4355 variable.append(&(i * 100)).await.unwrap();
4356 }
4357
4358 assert_eq!(variable.size(), 25);
4359
4360 variable.test_rewind_offsets(5).await.unwrap();
4362
4363 variable.sync().await.unwrap();
4364 drop(variable);
4365
4366 let mut variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4368 .await
4369 .unwrap();
4370
4371 let bounds = variable.bounds();
4373 assert_eq!(bounds.end, 25);
4374 assert_eq!(bounds.start, 0);
4375
4376 for i in 0..25u64 {
4378 assert_eq!(variable.read(i).await.unwrap(), i * 100);
4379 }
4380
4381 assert_eq!(variable.test_offsets_size(), 25);
4383
4384 let pos = variable.append(&2500).await.unwrap();
4386 assert_eq!(pos, 25);
4387 assert_eq!(variable.read(25).await.unwrap(), 2500);
4388
4389 variable.destroy().await.unwrap();
4390 });
4391 }
4392
4393 #[test_traced]
4394 fn test_variable_rebuild_offsets_rejects_anchor_outside_bounds() {
4395 let executor = deterministic::Runner::default();
4396 executor.start(|context| async move {
4397 let offsets_cfg = fixed::Config {
4398 partition: "rebuild-anchor-outside-offsets".into(),
4399 items_per_blob: NZU64!(10),
4400 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4401 write_buffer: NZUsize!(1024),
4402 };
4403
4404 let partition = Partition::new(
4405 context.child("data"),
4406 "rebuild-anchor-outside-data".into(),
4407 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4408 NZUsize!(1024),
4409 );
4410 let mut pending = BTreeMap::new();
4411 pending.insert(0, partition.open(0).await.unwrap());
4412 let mut offsets = fixed::Journal::<_, u64>::init(context.child("offsets"), offsets_cfg)
4413 .await
4414 .unwrap();
4415
4416 let mut encoded = Vec::new();
4417 encode_frame_into(None, &100u64, &mut encoded).unwrap();
4418 pending.get_mut(&0).unwrap().append(&encoded).await.unwrap();
4419 offsets.append(&0).await.unwrap();
4420
4421 let result = Journal::<_, u64>::rebuild_offsets_from_anchor(
4422 &partition,
4423 &mut pending,
4424 &mut offsets,
4425 10,
4426 2,
4427 &(),
4428 false,
4429 )
4430 .await;
4431 assert!(matches!(result, Err(Error::Corruption(_))));
4432
4433 drop(pending);
4434 Partition::<deterministic::Context>::remove_all(
4435 &context,
4436 "rebuild-anchor-outside-data",
4437 )
4438 .await
4439 .unwrap();
4440 offsets.destroy().await.unwrap();
4441 });
4442 }
4443
4444 #[test_traced]
4445 fn test_variable_recovery_rejects_watermark_beyond_retained_data() {
4446 let executor = deterministic::Runner::default();
4447 executor.start(|context| async move {
4448 let cfg = Config {
4449 partition: "recovery-anchor-too-far".into(),
4450 items_per_section: NZU64!(10),
4451 compression: None,
4452 codec_config: (),
4453 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4454 write_buffer: NZUsize!(1024),
4455 };
4456
4457 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4458 .await
4459 .unwrap();
4460
4461 for i in 0..20u64 {
4462 journal.append(&(i * 100)).await.unwrap();
4463 }
4464 journal.sync().await.unwrap();
4465
4466 journal
4469 .test_set_offsets_recovery_watermark(15)
4470 .await
4471 .unwrap();
4472 journal.test_rewind_data_to_position(12).await.unwrap();
4473 journal.test_sync_data().await.unwrap();
4474 drop(journal);
4475
4476 for child in ["second", "retry"] {
4478 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
4479 Err(Error::Corruption(message)) => assert_eq!(
4480 message,
4481 "offsets recovery watermark 15 exceeds retained data end 12 \
4482 (offsets bounds 0..20)"
4483 ),
4484 Err(error) => panic!("unexpected error: {error}"),
4485 Ok(_) => panic!("missing acknowledged data was accepted"),
4486 }
4487 }
4488 });
4489 }
4490
4491 #[test_traced]
4492 fn test_variable_recovery_rejects_missing_data_below_watermark() {
4493 let executor = deterministic::Runner::default();
4494 executor.start(|context| async move {
4495 let cfg = Config {
4496 partition: "recovery-short-middle-retry".into(),
4497 items_per_section: NZU64!(10),
4498 compression: None,
4499 codec_config: (),
4500 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4501 write_buffer: NZUsize!(1024),
4502 };
4503
4504 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4505 .await
4506 .unwrap();
4507
4508 for i in 0..30u64 {
4509 journal.append(&(i * 100)).await.unwrap();
4510 }
4511 journal.sync().await.unwrap();
4512
4513 journal
4516 .test_set_offsets_recovery_watermark(15)
4517 .await
4518 .unwrap();
4519 journal.test_rewind_data_to_position(12).await.unwrap();
4520 journal.test_sync_data().await.unwrap();
4521 journal.test_append_data(2, 9999).await.unwrap();
4522 journal.test_sync_data().await.unwrap();
4523 drop(journal);
4524
4525 for child in ["second", "retry"] {
4529 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
4530 Err(Error::Corruption(message)) => assert_eq!(
4531 message,
4532 "data blobs shorter than offsets recovery watermark 15"
4533 ),
4534 Err(error) => panic!("unexpected error: {error}"),
4535 Ok(_) => panic!("missing acknowledged data was accepted"),
4536 }
4537 }
4538
4539 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
4540 assert_eq!(
4541 data_blobs.len(),
4542 3,
4543 "corruption evidence should not be removed"
4544 );
4545 });
4546 }
4547
4548 #[test_traced]
4549 fn test_variable_rewind_commit_reopen() {
4550 let executor = deterministic::Runner::default();
4551 executor.start(|context| async move {
4552 let cfg = Config {
4553 partition: "rewind-commit-reopen".into(),
4554 items_per_section: NZU64!(10),
4555 compression: None,
4556 codec_config: (),
4557 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4558 write_buffer: NZUsize!(1024),
4559 };
4560
4561 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4562 .await
4563 .unwrap();
4564
4565 for i in 0..25u64 {
4566 journal.append(&(i * 100)).await.unwrap();
4567 }
4568 journal.sync().await.unwrap();
4569
4570 journal.rewind(12).await.unwrap();
4571 journal.commit().await.unwrap();
4572 drop(journal);
4573
4574 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4575 .await
4576 .unwrap();
4577 assert_eq!(journal.bounds(), 0..12);
4578 for i in 0..12u64 {
4579 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4580 }
4581 assert!(matches!(
4582 journal.read(12).await,
4583 Err(Error::ItemOutOfRange(12))
4584 ));
4585
4586 journal.destroy().await.unwrap();
4587 });
4588 }
4589
4590 #[test_traced]
4591 fn test_variable_recovery_rejects_synced_data_rewind_to_boundary() {
4592 let executor = deterministic::Runner::default();
4593 executor.start(|context| async move {
4594 let cfg = Config {
4595 partition: "recovery-boundary-data-rewind".into(),
4596 items_per_section: NZU64!(10),
4597 compression: None,
4598 codec_config: (),
4599 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4600 write_buffer: NZUsize!(1024),
4601 };
4602
4603 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4604 .await
4605 .unwrap();
4606
4607 for i in 0..20u64 {
4608 journal.append(&(i * 100)).await.unwrap();
4609 }
4610 journal.sync().await.unwrap();
4611
4612 journal.test_rewind_data_to_position(10).await.unwrap();
4613 journal.test_sync_data().await.unwrap();
4614 drop(journal);
4615
4616 for child in ["second", "retry"] {
4619 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
4620 Err(Error::Corruption(message)) => assert_eq!(
4621 message,
4622 "offsets recovery watermark 20 exceeds retained data end 10 \
4623 (offsets bounds 0..20)"
4624 ),
4625 Err(error) => panic!("unexpected error: {error}"),
4626 Ok(_) => panic!("missing acknowledged data was accepted"),
4627 }
4628 }
4629 });
4630 }
4631
4632 #[test_traced]
4633 fn test_variable_recovery_truncates_short_data_blob_after_anchor() {
4634 let executor = deterministic::Runner::default();
4635 executor.start(|context| async move {
4636 let cfg = Config {
4637 partition: "recovery-short-blob-after-anchor".into(),
4638 items_per_section: NZU64!(10),
4639 compression: None,
4640 codec_config: (),
4641 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4642 write_buffer: NZUsize!(1024),
4643 };
4644
4645 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4646 .await
4647 .unwrap();
4648
4649 for i in 0..25u64 {
4650 journal.append(&(i * 100)).await.unwrap();
4651 }
4652 journal.sync().await.unwrap();
4653
4654 journal
4658 .test_set_offsets_recovery_watermark(10)
4659 .await
4660 .unwrap();
4661 let offset = {
4662 let offsets = journal.offsets.snapshot().await.unwrap();
4663 offsets.read(12).await.unwrap()
4664 };
4665 drop(journal);
4666
4667 let (blob, size) = context
4669 .open(&cfg.data_partition(), &1u64.to_be_bytes())
4670 .await
4671 .unwrap();
4672 let mut writer = Writer::new(blob, size, 1024, cfg.page_cache.clone())
4673 .await
4674 .unwrap();
4675 writer.resize(offset).await.unwrap();
4676 writer.sync().await.unwrap();
4677 drop(writer);
4678
4679 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4680 .await
4681 .unwrap();
4682 assert_eq!(journal.bounds(), 0..12);
4683 assert_eq!(journal.test_offsets_size(), 12);
4684 for i in 0..12u64 {
4685 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4686 }
4687 assert!(matches!(
4688 journal.read(12).await,
4689 Err(Error::ItemOutOfRange(12))
4690 ));
4691
4692 journal.destroy().await.unwrap();
4693 });
4694 }
4695
4696 #[test_traced]
4697 fn test_variable_init_persists_offsets_trailing_item_repair() {
4698 let executor = deterministic::Runner::default();
4699 let ((offsets_blob_partition, expected_size), checkpoint) =
4700 executor.start_and_recover(|context| async move {
4701 let cfg = Config {
4702 partition: "offsets-init-repair-sync".into(),
4703 items_per_section: NZU64!(10),
4704 compression: None,
4705 codec_config: (),
4706 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4707 write_buffer: NZUsize!(1024),
4708 };
4709 let offsets_blob_partition = format!("{}-blobs", cfg.offsets_partition());
4710 let expected_size = 2 * std::mem::size_of::<u64>() as u64;
4711
4712 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4713 .await
4714 .unwrap();
4715 journal.append(&10).await.unwrap();
4716 journal.append(&20).await.unwrap();
4717 journal.sync().await.unwrap();
4718 drop(journal);
4719
4720 let (blob, raw_size) = context
4721 .open(&offsets_blob_partition, &0u64.to_be_bytes())
4722 .await
4723 .unwrap();
4724 let mut append = Writer::new(
4725 blob,
4726 raw_size,
4727 2048,
4728 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4729 )
4730 .await
4731 .unwrap();
4732 assert_eq!(append.size(), expected_size);
4733 append.resize(expected_size + 1).await.unwrap();
4734 append.sync().await.unwrap();
4735 drop(append);
4736
4737 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
4738 .await
4739 .unwrap();
4740 assert_eq!(journal.bounds(), 0..2);
4741 drop(journal);
4742
4743 (offsets_blob_partition, expected_size)
4744 });
4745
4746 deterministic::Runner::from(checkpoint).start(move |context| async move {
4747 let (blob, raw_size) = context
4748 .open(&offsets_blob_partition, &0u64.to_be_bytes())
4749 .await
4750 .unwrap();
4751 let append = Writer::new(
4752 blob,
4753 raw_size,
4754 2048,
4755 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4756 )
4757 .await
4758 .unwrap();
4759 assert_eq!(append.size(), expected_size);
4760 });
4761 }
4762
4763 #[test_traced]
4764 fn test_variable_init_persists_data_tail_repair() {
4765 let executor = deterministic::Runner::default();
4766 let ((data_partition, expected_size), checkpoint) =
4767 executor.start_and_recover(|context| async move {
4768 let cfg = Config {
4769 partition: "data-init-repair-sync".into(),
4770 items_per_section: NZU64!(10),
4771 compression: None,
4772 codec_config: (),
4773 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4774 write_buffer: NZUsize!(1024),
4775 };
4776 let data_partition = cfg.data_partition();
4777
4778 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4779 .await
4780 .unwrap();
4781 journal.append(&10).await.unwrap();
4782 journal.append(&20).await.unwrap();
4783 journal.sync().await.unwrap();
4784 drop(journal);
4785
4786 let (blob, raw_size) = context
4787 .open(&data_partition, &0u64.to_be_bytes())
4788 .await
4789 .unwrap();
4790 let mut append = Writer::new(
4791 blob,
4792 raw_size,
4793 2048,
4794 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4795 )
4796 .await
4797 .unwrap();
4798 let expected_size = append.size();
4799 append.append(&[0xFF, 0xFF]).await.unwrap();
4800 append.sync().await.unwrap();
4801 drop(append);
4802
4803 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
4804 .await
4805 .unwrap();
4806 assert_eq!(journal.bounds(), 0..2);
4807 drop(journal);
4808
4809 (data_partition, expected_size)
4810 });
4811
4812 deterministic::Runner::from(checkpoint).start(move |context| async move {
4813 let (blob, raw_size) = context
4814 .open(&data_partition, &0u64.to_be_bytes())
4815 .await
4816 .unwrap();
4817 let append = Writer::new(
4818 blob,
4819 raw_size,
4820 2048,
4821 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4822 )
4823 .await
4824 .unwrap();
4825 assert_eq!(append.size(), expected_size);
4826 });
4827 }
4828
4829 #[test_traced]
4832 fn test_variable_recovery_empty_offsets_after_prune_and_append() {
4833 let executor = deterministic::Runner::default();
4834 executor.start(|context| async move {
4835 let cfg = Config {
4836 partition: "recovery-empty-after-prune".into(),
4837 items_per_section: NZU64!(10),
4838 compression: None,
4839 codec_config: (),
4840 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4841 write_buffer: NZUsize!(1024),
4842 };
4843
4844 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4846 .await
4847 .unwrap();
4848
4849 for i in 0..10u64 {
4851 journal.append(&(i * 100)).await.unwrap();
4852 }
4853 let bounds = journal.bounds();
4854 assert_eq!(bounds.end, 10);
4855 assert_eq!(bounds.start, 0);
4856
4857 journal.prune(10).await.unwrap();
4859 let bounds = journal.bounds();
4860 assert_eq!(bounds.end, 10);
4861 assert!(bounds.is_empty()); for i in 10..20u64 {
4867 journal.test_append_data(1, i * 100).await.unwrap();
4868 }
4869 journal.test_sync_data().await.unwrap();
4871 drop(journal);
4875
4876 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4878 .await
4879 .expect("Should recover from crash after data sync but before offsets sync");
4880
4881 let bounds = journal.bounds();
4883 assert_eq!(bounds.end, 20);
4884 assert_eq!(bounds.start, 10);
4885
4886 for i in 10..20u64 {
4888 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4889 }
4890
4891 for i in 0..10 {
4893 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
4894 }
4895
4896 journal.destroy().await.unwrap();
4897 });
4898 }
4899
4900 #[test_traced]
4902 fn test_variable_concurrent_sync_recovery() {
4903 let executor = deterministic::Runner::default();
4904 executor.start(|context| async move {
4905 let cfg = Config {
4906 partition: "concurrent-sync-recovery".into(),
4907 items_per_section: NZU64!(10),
4908 compression: None,
4909 codec_config: (),
4910 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4911 write_buffer: NZUsize!(1024),
4912 };
4913
4914 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4915 .await
4916 .unwrap();
4917
4918 for i in 0..15u64 {
4920 journal.append(&(i * 100)).await.unwrap();
4921 }
4922
4923 journal.commit().await.unwrap();
4925
4926 drop(journal);
4928
4929 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4930 .await
4931 .unwrap();
4932
4933 assert_eq!(journal.size(), 15);
4935 for i in 0..15u64 {
4936 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4937 }
4938
4939 journal.destroy().await.unwrap();
4940 });
4941 }
4942
4943 #[test_traced]
4944 fn test_variable_recovery_from_mid_blob_durable_anchor() {
4945 let executor = deterministic::Runner::default();
4946 executor.start(|context| async move {
4947 let cfg = Config {
4948 partition: "mid-blob-durable-anchor".into(),
4949 items_per_section: NZU64!(5),
4950 compression: None,
4951 codec_config: (),
4952 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
4953 write_buffer: NZUsize!(1024),
4954 };
4955
4956 let mut journal =
4957 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
4958 .await
4959 .unwrap();
4960 assert_eq!(journal.append(&700).await.unwrap(), 7);
4961 journal.sync().await.unwrap();
4962
4963 for i in 1..6u64 {
4964 assert_eq!(journal.append(&(700 + i)).await.unwrap(), 7 + i);
4965 }
4966 journal.commit().await.unwrap();
4967 drop(journal);
4968
4969 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4970 .await
4971 .unwrap();
4972 assert_eq!(journal.bounds(), 7..13);
4973 for i in 0..6u64 {
4974 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
4975 }
4976
4977 journal.destroy().await.unwrap();
4978 });
4979 }
4980
4981 #[test_traced]
4982 fn test_init_at_size_rejects_conflicting_offsets_partitions() {
4983 let executor = deterministic::Runner::default();
4984 executor.start(|context| async move {
4985 let cfg = Config {
4986 partition: "init-at-size-conflicting-offsets".into(),
4987 items_per_section: NZU64!(5),
4988 compression: None,
4989 codec_config: (),
4990 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
4991 write_buffer: NZUsize!(1024),
4992 };
4993 let legacy_partition = cfg.offsets_partition();
4994 let blobs_partition = format!("{legacy_partition}-blobs");
4995
4996 for partition in [&legacy_partition, &blobs_partition] {
4997 let (blob, _) = context.open(partition, &0u64.to_be_bytes()).await.unwrap();
4998 blob.write_at_sync(0, vec![0]).await.unwrap();
4999 }
5000
5001 let result = Journal::<_, u64>::init_at_size(context.child("storage"), cfg, 7).await;
5002 assert!(matches!(result, Err(Error::Corruption(_))));
5003
5004 assert_eq!(context.scan(&legacy_partition).await.unwrap().len(), 1);
5007 assert_eq!(context.scan(&blobs_partition).await.unwrap().len(), 1);
5008 });
5009 }
5010
5011 #[test_traced]
5012 fn test_init_at_size_zero() {
5013 let executor = deterministic::Runner::default();
5014 executor.start(|context| async move {
5015 let cfg = Config {
5016 partition: "init-at-size-zero".into(),
5017 items_per_section: NZU64!(5),
5018 compression: None,
5019 codec_config: (),
5020 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5021 write_buffer: NZUsize!(1024),
5022 };
5023
5024 let mut journal =
5025 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 0)
5026 .await
5027 .unwrap();
5028
5029 assert_eq!(journal.size(), 0);
5031
5032 assert!(journal.bounds().is_empty());
5034
5035 let pos = journal.append(&100).await.unwrap();
5037 assert_eq!(pos, 0);
5038 assert_eq!(journal.size(), 1);
5039 assert_eq!(journal.read(0).await.unwrap(), 100);
5040
5041 journal.destroy().await.unwrap();
5042 });
5043 }
5044
5045 #[test_traced]
5046 fn test_init_at_size_blob_boundary() {
5047 let executor = deterministic::Runner::default();
5048 executor.start(|context| async move {
5049 let cfg = Config {
5050 partition: "init-at-size-boundary".into(),
5051 items_per_section: NZU64!(5),
5052 compression: None,
5053 codec_config: (),
5054 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5055 write_buffer: NZUsize!(1024),
5056 };
5057
5058 let mut journal =
5060 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 10)
5061 .await
5062 .unwrap();
5063
5064 let bounds = journal.bounds();
5066 assert_eq!(bounds.end, 10);
5067
5068 assert!(bounds.is_empty());
5070
5071 let pos = journal.append(&1000).await.unwrap();
5073 assert_eq!(pos, 10);
5074 assert_eq!(journal.size(), 11);
5075 assert_eq!(journal.read(10).await.unwrap(), 1000);
5076
5077 let pos = journal.append(&1001).await.unwrap();
5079 assert_eq!(pos, 11);
5080 assert_eq!(journal.read(11).await.unwrap(), 1001);
5081
5082 journal.destroy().await.unwrap();
5083 });
5084 }
5085
5086 #[test_traced]
5087 fn test_init_at_size_mid_blob() {
5088 let executor = deterministic::Runner::default();
5089 executor.start(|context| async move {
5090 let cfg = Config {
5091 partition: "init-at-size-mid".into(),
5092 items_per_section: NZU64!(5),
5093 compression: None,
5094 codec_config: (),
5095 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5096 write_buffer: NZUsize!(1024),
5097 };
5098
5099 let mut journal =
5101 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 7)
5102 .await
5103 .unwrap();
5104
5105 let bounds = journal.bounds();
5107 assert_eq!(bounds.end, 7);
5108
5109 assert!(bounds.is_empty());
5111
5112 let pos = journal.append(&700).await.unwrap();
5114 assert_eq!(pos, 7);
5115 assert_eq!(journal.size(), 8);
5116 assert_eq!(journal.read(7).await.unwrap(), 700);
5117
5118 journal.destroy().await.unwrap();
5119 });
5120 }
5121
5122 #[test_traced]
5123 fn test_init_at_size_persistence() {
5124 let executor = deterministic::Runner::default();
5125 executor.start(|context| async move {
5126 let cfg = Config {
5127 partition: "init-at-size-persist".into(),
5128 items_per_section: NZU64!(5),
5129 compression: None,
5130 codec_config: (),
5131 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5132 write_buffer: NZUsize!(1024),
5133 };
5134
5135 let mut journal =
5137 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15)
5138 .await
5139 .unwrap();
5140
5141 for i in 0..5u64 {
5143 let pos = journal.append(&(1500 + i)).await.unwrap();
5144 assert_eq!(pos, 15 + i);
5145 }
5146
5147 assert_eq!(journal.size(), 20);
5148
5149 journal.sync().await.unwrap();
5151 drop(journal);
5152
5153 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5154 .await
5155 .unwrap();
5156
5157 let bounds = journal.bounds();
5159 assert_eq!(bounds.end, 20);
5160 assert_eq!(bounds.start, 15);
5161
5162 for i in 0..5u64 {
5164 assert_eq!(journal.read(15 + i).await.unwrap(), 1500 + i);
5165 }
5166
5167 let pos = journal.append(&9999).await.unwrap();
5169 assert_eq!(pos, 20);
5170 assert_eq!(journal.read(20).await.unwrap(), 9999);
5171
5172 journal.destroy().await.unwrap();
5173 });
5174 }
5175
5176 #[test_traced]
5177 fn test_init_at_size_persistence_without_data() {
5178 let executor = deterministic::Runner::default();
5179 executor.start(|context| async move {
5180 let cfg = Config {
5181 partition: "init-at-size-persist-empty".into(),
5182 items_per_section: NZU64!(5),
5183 compression: None,
5184 codec_config: (),
5185 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5186 write_buffer: NZUsize!(1024),
5187 };
5188
5189 let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15)
5191 .await
5192 .unwrap();
5193
5194 let bounds = journal.bounds();
5195 assert_eq!(bounds.end, 15);
5196 assert!(bounds.is_empty());
5197
5198 drop(journal);
5200
5201 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5203 .await
5204 .unwrap();
5205
5206 let bounds = journal.bounds();
5207 assert_eq!(bounds.end, 15);
5208 assert!(bounds.is_empty());
5209
5210 let pos = journal.append(&1500).await.unwrap();
5212 assert_eq!(pos, 15);
5213 assert_eq!(journal.read(15).await.unwrap(), 1500);
5214
5215 journal.destroy().await.unwrap();
5216 });
5217 }
5218
5219 #[test_traced]
5220 fn test_init_at_size_clears_existing_data() {
5221 let executor = deterministic::Runner::default();
5222 executor.start(|context| async move {
5223 let cfg = Config {
5224 partition: "init-at-size-clears-existing".into(),
5225 items_per_section: NZU64!(5),
5226 compression: None,
5227 codec_config: (),
5228 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5229 write_buffer: NZUsize!(1024),
5230 };
5231
5232 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5233 .await
5234 .unwrap();
5235 for i in 0..12u64 {
5236 journal.append(&(100 + i)).await.unwrap();
5237 }
5238 journal.sync().await.unwrap();
5239 drop(journal);
5240
5241 let mut journal =
5242 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 7)
5243 .await
5244 .unwrap();
5245 assert_eq!(journal.bounds(), 7..7);
5246 assert_eq!(journal.append(&700).await.unwrap(), 7);
5247 journal.sync().await.unwrap();
5248 drop(journal);
5249
5250 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5251 .await
5252 .unwrap();
5253 assert_eq!(journal.bounds(), 7..8);
5254 assert_eq!(journal.read(7).await.unwrap(), 700);
5255 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
5256 assert!(matches!(
5257 journal.read(8).await,
5258 Err(Error::ItemOutOfRange(8))
5259 ));
5260
5261 journal.destroy().await.unwrap();
5262 });
5263 }
5264
5265 #[test_traced]
5266 fn test_init_at_size_stages_reset_before_clearing_data() {
5267 let partition = "init-at-size-stage-before-clear-failure".to_string();
5268 let executor = deterministic::Runner::default();
5269 let ((), checkpoint) = executor.start_and_recover({
5270 let partition = partition.clone();
5271 |context| async move {
5272 let cfg = Config {
5273 partition,
5274 items_per_section: NZU64!(5),
5275 compression: None,
5276 codec_config: (),
5277 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5278 write_buffer: NZUsize!(1024),
5279 };
5280
5281 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5282 .await
5283 .unwrap();
5284 for i in 0..12u64 {
5285 journal.append(&(100 + i)).await.unwrap();
5286 }
5287 journal.sync().await.unwrap();
5288 drop(journal);
5289
5290 *context.storage_fault_config().write() = deterministic::FaultConfig {
5291 sync_rate: Some(1.0),
5292 ..Default::default()
5293 };
5294 assert!(
5295 Journal::<_, u64>::init_at_size(context.child("reset"), cfg, 7)
5296 .await
5297 .is_err()
5298 );
5299 }
5300 });
5301
5302 deterministic::Runner::from(checkpoint).start(move |context| async move {
5303 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
5304 let cfg = Config {
5305 partition,
5306 items_per_section: NZU64!(5),
5307 compression: None,
5308 codec_config: (),
5309 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5310 write_buffer: NZUsize!(1024),
5311 };
5312
5313 let journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
5314 .await
5315 .unwrap();
5316 assert_eq!(journal.bounds(), 0..12);
5317 for i in 0..12u64 {
5318 assert_eq!(journal.read(i).await.unwrap(), 100 + i);
5319 }
5320
5321 journal.destroy().await.unwrap();
5322 });
5323 }
5324
5325 #[test_traced]
5326 fn test_clear_to_size_stages_reset_before_clearing_data() {
5327 let partition = "clear-to-size-stage-before-clear-failure".to_string();
5328 let executor = deterministic::Runner::default();
5329 let ((), checkpoint) = executor.start_and_recover({
5330 let partition = partition.clone();
5331 |context| async move {
5332 let cfg = Config {
5333 partition,
5334 items_per_section: NZU64!(5),
5335 compression: None,
5336 codec_config: (),
5337 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5338 write_buffer: NZUsize!(1024),
5339 };
5340
5341 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5342 .await
5343 .unwrap();
5344 for i in 0..12u64 {
5345 journal.append(&(100 + i)).await.unwrap();
5346 }
5347 journal.sync().await.unwrap();
5348
5349 *context.storage_fault_config().write() = deterministic::FaultConfig {
5352 sync_rate: Some(1.0),
5353 ..Default::default()
5354 };
5355 assert!(journal.clear_to_size(7).await.is_err());
5356 }
5357 });
5358
5359 deterministic::Runner::from(checkpoint).start(move |context| async move {
5360 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
5361 let cfg = Config {
5362 partition,
5363 items_per_section: NZU64!(5),
5364 compression: None,
5365 codec_config: (),
5366 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5367 write_buffer: NZUsize!(1024),
5368 };
5369
5370 let journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
5371 .await
5372 .unwrap();
5373 assert_eq!(journal.bounds(), 0..12);
5374 for i in 0..12u64 {
5375 assert_eq!(journal.read(i).await.unwrap(), 100 + i);
5376 }
5377
5378 journal.destroy().await.unwrap();
5379 });
5380 }
5381
5382 #[test_traced]
5383 fn test_clear_to_size_crash_after_staging_completes_on_init() {
5384 let partition = "clear-to-size-crash-after-staging".to_string();
5385 let executor = deterministic::Runner::default();
5386 let ((), checkpoint) = executor.start_and_recover({
5387 let partition = partition.clone();
5388 |context| async move {
5389 let cfg = Config {
5390 partition,
5391 items_per_section: NZU64!(5),
5392 compression: None,
5393 codec_config: (),
5394 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5395 write_buffer: NZUsize!(1024),
5396 };
5397
5398 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5399 .await
5400 .unwrap();
5401 for i in 0..12u64 {
5402 journal.append(&(100 + i)).await.unwrap();
5403 }
5404 journal.sync().await.unwrap();
5405
5406 *context.storage_fault_config().write() = deterministic::FaultConfig {
5410 remove_rate: Some(1.0),
5411 ..Default::default()
5412 };
5413 assert!(journal.clear_to_size(7).await.is_err());
5414 }
5415 });
5416
5417 deterministic::Runner::from(checkpoint).start(move |context| async move {
5418 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
5419 let cfg = Config {
5420 partition,
5421 items_per_section: NZU64!(5),
5422 compression: None,
5423 codec_config: (),
5424 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5425 write_buffer: NZUsize!(1024),
5426 };
5427
5428 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
5430 .await
5431 .unwrap();
5432 assert_eq!(journal.bounds(), 7..7);
5433 assert_eq!(journal.append(&700).await.unwrap(), 7);
5434 journal.sync().await.unwrap();
5435 drop(journal);
5436
5437 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg.clone())
5439 .await
5440 .unwrap();
5441 assert_eq!(journal.bounds(), 7..8);
5442 assert_eq!(journal.read(7).await.unwrap(), 700);
5443
5444 journal.destroy().await.unwrap();
5445 });
5446 }
5447
5448 #[test_traced]
5449 fn test_init_at_size_recovers_staged_reset_crash_points() {
5450 let executor = deterministic::Runner::default();
5451 executor.start(|context| async move {
5452 for (index, clear_data) in [false, true].into_iter().enumerate() {
5453 let cfg = Config {
5454 partition: format!("init-at-size-staged-reset-crash-{index}"),
5455 items_per_section: NZU64!(5),
5456 compression: None,
5457 codec_config: (),
5458 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5459 write_buffer: NZUsize!(1024),
5460 };
5461
5462 let mut journal = Journal::<_, u64>::init(
5463 context.child("first").with_attribute("index", index),
5464 cfg.clone(),
5465 )
5466 .await
5467 .unwrap();
5468 for i in 0..12u64 {
5469 journal.append(&(100 + i)).await.unwrap();
5470 }
5471 journal.sync().await.unwrap();
5472 drop(journal);
5473
5474 let offsets_cfg = fixed::Config {
5475 partition: cfg.offsets_partition(),
5476 items_per_blob: cfg.items_per_section,
5477 page_cache: cfg.page_cache.clone(),
5478 write_buffer: cfg.write_buffer,
5479 };
5480 let intent_ctx = context.child("intent").with_attribute("index", index);
5484 fixed::Journal::<_, u64>::test_stage_clear(
5485 intent_ctx.child("meta"),
5486 &offsets_cfg.partition,
5487 7,
5488 )
5489 .await
5490 .unwrap();
5491
5492 if clear_data {
5493 Partition::<deterministic::Context>::remove_all(
5494 &context,
5495 &cfg.data_partition(),
5496 )
5497 .await
5498 .unwrap();
5499 }
5500
5501 let mut journal = Journal::<_, u64>::init(
5502 context.child("recover").with_attribute("index", index),
5503 cfg.clone(),
5504 )
5505 .await
5506 .unwrap();
5507 assert_eq!(journal.bounds(), 7..7);
5508 assert_eq!(journal.append(&700).await.unwrap(), 7);
5509 journal.sync().await.unwrap();
5510 drop(journal);
5511
5512 let journal = Journal::<_, u64>::init(
5513 context.child("reopen").with_attribute("index", index),
5514 cfg.clone(),
5515 )
5516 .await
5517 .unwrap();
5518 assert_eq!(journal.bounds(), 7..8);
5519 assert_eq!(journal.read(7).await.unwrap(), 700);
5520
5521 journal.destroy().await.unwrap();
5522 }
5523 });
5524 }
5525
5526 #[test_traced]
5527 fn test_init_at_size_overwrites_pending_clear_target() {
5528 let executor = deterministic::Runner::default();
5529 executor.start(|context| async move {
5530 let cfg = Config {
5531 partition: "init-at-size-overwrites-pending-target".into(),
5532 items_per_section: NZU64!(5),
5533 compression: None,
5534 codec_config: (),
5535 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5536 write_buffer: NZUsize!(1024),
5537 };
5538
5539 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5540 .await
5541 .unwrap();
5542 for i in 0..12u64 {
5543 journal.append(&(100 + i)).await.unwrap();
5544 }
5545 journal.sync().await.unwrap();
5546 drop(journal);
5547
5548 let offsets_cfg = fixed::Config {
5551 partition: cfg.offsets_partition(),
5552 items_per_blob: cfg.items_per_section,
5553 page_cache: cfg.page_cache.clone(),
5554 write_buffer: cfg.write_buffer,
5555 };
5556 let stale_ctx = context.child("stale");
5557 fixed::Journal::<_, u64>::test_stage_clear(
5558 stale_ctx.child("meta"),
5559 &offsets_cfg.partition,
5560 5,
5561 )
5562 .await
5563 .unwrap();
5564
5565 let mut journal =
5567 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 10)
5568 .await
5569 .unwrap();
5570 assert_eq!(journal.bounds(), 10..10);
5571 assert_eq!(journal.append(&700).await.unwrap(), 10);
5572 journal.sync().await.unwrap();
5573 drop(journal);
5574
5575 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg.clone())
5577 .await
5578 .unwrap();
5579 assert_eq!(journal.bounds(), 10..11);
5580 assert_eq!(journal.read(10).await.unwrap(), 700);
5581
5582 journal.destroy().await.unwrap();
5583 });
5584 }
5585
5586 #[test_traced]
5587 fn test_init_at_size_discards_same_blob_stale_data() {
5588 let executor = deterministic::Runner::default();
5589 executor.start(|context| async move {
5590 let cfg = Config {
5591 partition: "init-at-size-discards-same-blob-stale-data".into(),
5592 items_per_section: NZU64!(5),
5593 compression: None,
5594 codec_config: (),
5595 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5596 write_buffer: NZUsize!(1024),
5597 };
5598
5599 let mut journal =
5600 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 5)
5601 .await
5602 .unwrap();
5603 for i in 0..4u64 {
5604 assert_eq!(journal.append(&(500 + i)).await.unwrap(), 5 + i);
5605 }
5606 journal.sync().await.unwrap();
5607 drop(journal);
5608
5609 let journal = Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 7)
5610 .await
5611 .unwrap();
5612 drop(journal);
5613
5614 let mut journal = Journal::<_, u64>::init(context.child("after_reset"), cfg.clone())
5615 .await
5616 .unwrap();
5617 assert_eq!(journal.bounds(), 7..7);
5618 assert!(matches!(
5619 journal.read(7).await,
5620 Err(Error::ItemOutOfRange(7))
5621 ));
5622
5623 assert_eq!(journal.append(&700).await.unwrap(), 7);
5624 journal.sync().await.unwrap();
5625 drop(journal);
5626
5627 let journal = Journal::<_, u64>::init(context.child("after_append"), cfg.clone())
5628 .await
5629 .unwrap();
5630 assert_eq!(journal.bounds(), 7..8);
5631 assert_eq!(journal.read(7).await.unwrap(), 700);
5632 assert!(matches!(
5633 journal.read(8).await,
5634 Err(Error::ItemOutOfRange(8))
5635 ));
5636
5637 journal.destroy().await.unwrap();
5638 });
5639 }
5640
5641 #[test_traced]
5643 fn test_init_at_size_mid_blob_persistence() {
5644 let executor = deterministic::Runner::default();
5645 executor.start(|context| async move {
5646 let cfg = Config {
5647 partition: "init-at-size-mid-blob".into(),
5648 items_per_section: NZU64!(5),
5649 compression: None,
5650 codec_config: (),
5651 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5652 write_buffer: NZUsize!(1024),
5653 };
5654
5655 let mut journal =
5657 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
5658 .await
5659 .unwrap();
5660
5661 for i in 0..3u64 {
5663 let pos = journal.append(&(700 + i)).await.unwrap();
5664 assert_eq!(pos, 7 + i);
5665 }
5666
5667 let bounds = journal.bounds();
5668 assert_eq!(bounds.end, 10);
5669 assert_eq!(bounds.start, 7);
5670
5671 journal.sync().await.unwrap();
5673 drop(journal);
5674
5675 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5677 .await
5678 .unwrap();
5679
5680 let bounds = journal.bounds();
5682 assert_eq!(bounds.end, 10);
5683 assert_eq!(bounds.start, 7);
5684
5685 for i in 0..3u64 {
5687 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
5688 }
5689
5690 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
5692
5693 journal.destroy().await.unwrap();
5694 });
5695 }
5696
5697 #[test_traced]
5699 fn test_init_at_size_mid_blob_multi_blob_persistence() {
5700 let executor = deterministic::Runner::default();
5701 executor.start(|context| async move {
5702 let cfg = Config {
5703 partition: "init-at-size-multi-blob".into(),
5704 items_per_section: NZU64!(5),
5705 compression: None,
5706 codec_config: (),
5707 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5708 write_buffer: NZUsize!(1024),
5709 };
5710
5711 let mut journal =
5713 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
5714 .await
5715 .unwrap();
5716
5717 for i in 0..8u64 {
5719 let pos = journal.append(&(700 + i)).await.unwrap();
5720 assert_eq!(pos, 7 + i);
5721 }
5722
5723 let bounds = journal.bounds();
5724 assert_eq!(bounds.end, 15);
5725 assert_eq!(bounds.start, 7);
5726
5727 journal.sync().await.unwrap();
5729 drop(journal);
5730
5731 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5733 .await
5734 .unwrap();
5735
5736 let bounds = journal.bounds();
5738 assert_eq!(bounds.end, 15);
5739 assert_eq!(bounds.start, 7);
5740
5741 for i in 0..8u64 {
5743 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
5744 }
5745
5746 journal.destroy().await.unwrap();
5747 });
5748 }
5749
5750 #[test_traced]
5752 fn test_align_journals_data_empty_mid_blob_pruning_boundary() {
5753 let executor = deterministic::Runner::default();
5754 executor.start(|context| async move {
5755 let cfg = Config {
5756 partition: "align-journals-mid-blob-pruning-boundary".into(),
5757 items_per_section: NZU64!(5),
5758 compression: None,
5759 codec_config: (),
5760 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5761 write_buffer: NZUsize!(1024),
5762 };
5763
5764 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5766 .await
5767 .unwrap();
5768 for i in 0..7u64 {
5769 journal.append(&(100 + i)).await.unwrap();
5770 }
5771 journal.sync().await.unwrap();
5772
5773 drop(journal);
5775 Partition::<deterministic::Context>::remove_all(&context, &cfg.data_partition())
5776 .await
5777 .unwrap();
5778
5779 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5781 .await
5782 .unwrap();
5783 let bounds = journal.bounds();
5784 assert_eq!(bounds.end, 7);
5785 assert!(bounds.is_empty());
5786
5787 let pos = journal.append(&777).await.unwrap();
5789 assert_eq!(pos, 7);
5790 assert_eq!(journal.size(), 8);
5791 assert_eq!(journal.read(7).await.unwrap(), 777);
5792
5793 journal.test_sync_data().await.unwrap();
5795 drop(journal);
5796
5797 let journal = Journal::<_, u64>::init(context.child("third"), cfg.clone())
5799 .await
5800 .unwrap();
5801 let bounds = journal.bounds();
5802 assert_eq!(bounds.end, 8);
5803 assert_eq!(bounds.start, 7);
5804 assert_eq!(journal.read(7).await.unwrap(), 777);
5805
5806 journal.destroy().await.unwrap();
5807 });
5808 }
5809
5810 #[test_traced]
5812 fn test_init_at_size_crash_data_synced_offsets_not() {
5813 let executor = deterministic::Runner::default();
5814 executor.start(|context| async move {
5815 let cfg = Config {
5816 partition: "init-at-size-crash-recovery".into(),
5817 items_per_section: NZU64!(5),
5818 compression: None,
5819 codec_config: (),
5820 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5821 write_buffer: NZUsize!(1024),
5822 };
5823
5824 let mut journal =
5826 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
5827 .await
5828 .unwrap();
5829
5830 for i in 0..3u64 {
5832 journal.append(&(700 + i)).await.unwrap();
5833 }
5834
5835 journal.test_sync_data_blob(1).await.unwrap();
5838 drop(journal);
5840
5841 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5843 .await
5844 .unwrap();
5845
5846 let bounds = journal.bounds();
5848 assert_eq!(bounds.end, 10);
5849 assert_eq!(bounds.start, 7);
5850
5851 for i in 0..3u64 {
5853 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
5854 }
5855
5856 journal.destroy().await.unwrap();
5857 });
5858 }
5859
5860 #[test_traced]
5861 fn test_prune_does_not_move_oldest_retained_backwards() {
5862 let executor = deterministic::Runner::default();
5863 executor.start(|context| async move {
5864 let cfg = Config {
5865 partition: "prune-no-backwards".into(),
5866 items_per_section: NZU64!(5),
5867 compression: None,
5868 codec_config: (),
5869 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5870 write_buffer: NZUsize!(1024),
5871 };
5872
5873 let mut journal =
5874 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
5875 .await
5876 .unwrap();
5877
5878 for i in 0..3u64 {
5880 let pos = journal.append(&(700 + i)).await.unwrap();
5881 assert_eq!(pos, 7 + i);
5882 }
5883 assert_eq!(journal.bounds().start, 7);
5884
5885 journal.prune(8).await.unwrap();
5887 assert_eq!(journal.bounds().start, 7);
5888 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
5889 assert_eq!(journal.read(7).await.unwrap(), 700);
5890
5891 journal.destroy().await.unwrap();
5892 });
5893 }
5894
5895 #[test_traced]
5896 fn test_variable_recovery_near_max_data_synced_offsets_not() {
5897 let executor = deterministic::Runner::default();
5898 executor.start(|context| async move {
5899 let cfg = Config {
5900 partition: "near-max-data-synced-offsets-not".into(),
5901 items_per_section: NZU64!(10),
5902 compression: None,
5903 codec_config: (),
5904 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5905 write_buffer: NZUsize!(1024),
5906 };
5907
5908 let mut journal =
5909 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), u64::MAX - 1)
5910 .await
5911 .unwrap();
5912 assert_eq!(journal.append(&7).await.unwrap(), u64::MAX - 1);
5913 journal
5914 .test_sync_data_blob(position_to_blob(u64::MAX - 1, cfg.items_per_section.get()))
5915 .await
5916 .unwrap();
5917 drop(journal);
5918
5919 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5920 .await
5921 .unwrap();
5922 assert_eq!(journal.bounds(), (u64::MAX - 1)..u64::MAX);
5923 assert_eq!(journal.read(u64::MAX - 1).await.unwrap(), 7);
5924
5925 journal.destroy().await.unwrap();
5926 });
5927 }
5928
5929 #[test_traced]
5930 fn test_init_at_size_large_offset() {
5931 let executor = deterministic::Runner::default();
5932 executor.start(|context| async move {
5933 let cfg = Config {
5934 partition: "init-at-size-large".into(),
5935 items_per_section: NZU64!(5),
5936 compression: None,
5937 codec_config: (),
5938 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5939 write_buffer: NZUsize!(1024),
5940 };
5941
5942 let mut journal =
5944 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 1000)
5945 .await
5946 .unwrap();
5947
5948 let bounds = journal.bounds();
5949 assert_eq!(bounds.end, 1000);
5950 assert!(bounds.is_empty());
5952
5953 let pos = journal.append(&100000).await.unwrap();
5955 assert_eq!(pos, 1000);
5956 assert_eq!(journal.read(1000).await.unwrap(), 100000);
5957
5958 journal.destroy().await.unwrap();
5959 });
5960 }
5961
5962 #[test_traced]
5963 fn test_init_at_size_prune_and_append() {
5964 let executor = deterministic::Runner::default();
5965 executor.start(|context| async move {
5966 let cfg = Config {
5967 partition: "init-at-size-prune".into(),
5968 items_per_section: NZU64!(5),
5969 compression: None,
5970 codec_config: (),
5971 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
5972 write_buffer: NZUsize!(1024),
5973 };
5974
5975 let mut journal =
5977 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 20)
5978 .await
5979 .unwrap();
5980
5981 for i in 0..10u64 {
5983 journal.append(&(2000 + i)).await.unwrap();
5984 }
5985
5986 assert_eq!(journal.size(), 30);
5987
5988 journal.prune(25).await.unwrap();
5990
5991 let bounds = journal.bounds();
5992 assert_eq!(bounds.end, 30);
5993 assert_eq!(bounds.start, 25);
5994
5995 for i in 25..30u64 {
5997 assert_eq!(journal.read(i).await.unwrap(), 2000 + (i - 20));
5998 }
5999
6000 let pos = journal.append(&3000).await.unwrap();
6002 assert_eq!(pos, 30);
6003
6004 journal.destroy().await.unwrap();
6005 });
6006 }
6007
6008 #[test_traced]
6010 fn test_init_sync_no_existing_data() {
6011 let executor = deterministic::Runner::default();
6012 executor.start(|context| async move {
6013 let cfg = Config {
6014 partition: "test-fresh-start".into(),
6015 items_per_section: NZU64!(5),
6016 compression: None,
6017 codec_config: (),
6018 write_buffer: NZUsize!(1024),
6019 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6020 };
6021
6022 let lower_bound = 10;
6024 let upper_bound = 26;
6025 let mut journal = Journal::init_sync(
6026 context.child("storage"),
6027 cfg.clone(),
6028 lower_bound..upper_bound,
6029 )
6030 .await
6031 .expect("Failed to initialize journal with sync boundaries");
6032
6033 let bounds = journal.bounds();
6034 assert_eq!(bounds.end, lower_bound);
6035 assert!(bounds.is_empty());
6036
6037 let pos1 = journal.append(&42u64).await.unwrap();
6039 assert_eq!(pos1, lower_bound);
6040 assert_eq!(journal.read(pos1).await.unwrap(), 42u64);
6041
6042 let pos2 = journal.append(&43u64).await.unwrap();
6043 assert_eq!(pos2, lower_bound + 1);
6044 assert_eq!(journal.read(pos2).await.unwrap(), 43u64);
6045
6046 journal.destroy().await.unwrap();
6047 });
6048 }
6049
6050 #[test_traced]
6052 fn test_init_sync_existing_data_overlap() {
6053 let executor = deterministic::Runner::default();
6054 executor.start(|context| async move {
6055 let cfg = Config {
6056 partition: "test-overlap".into(),
6057 items_per_section: NZU64!(5),
6058 compression: None,
6059 codec_config: (),
6060 write_buffer: NZUsize!(1024),
6061 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6062 };
6063
6064 let mut journal =
6066 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
6067 .await
6068 .expect("Failed to create initial journal");
6069
6070 for i in 0..20u64 {
6072 journal.append(&(i * 100)).await.unwrap();
6073 }
6074 journal.sync().await.unwrap();
6075 drop(journal);
6076
6077 let lower_bound = 8;
6080 let upper_bound = 31;
6081 let mut journal = Journal::<deterministic::Context, u64>::init_sync(
6082 context.child("storage"),
6083 cfg.clone(),
6084 lower_bound..upper_bound,
6085 )
6086 .await
6087 .expect("Failed to initialize journal with overlap");
6088
6089 assert_eq!(journal.size(), 20);
6090
6091 assert_eq!(journal.bounds().start, 5); assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
6096 assert!(matches!(journal.read(4).await, Err(Error::ItemPruned(_))));
6097
6098 assert_eq!(journal.read(5).await.unwrap(), 500);
6100 assert_eq!(journal.read(8).await.unwrap(), 800);
6101 assert_eq!(journal.read(19).await.unwrap(), 1900);
6102
6103 assert!(matches!(
6105 journal.read(20).await,
6106 Err(Error::ItemOutOfRange(_))
6107 ));
6108
6109 let pos = journal.append(&999).await.unwrap();
6111 assert_eq!(pos, 20);
6112 assert_eq!(journal.read(20).await.unwrap(), 999);
6113
6114 journal.destroy().await.unwrap();
6115 });
6116 }
6117
6118 #[should_panic]
6120 #[test_traced]
6121 fn test_init_sync_invalid_parameters() {
6122 let executor = deterministic::Runner::default();
6123 executor.start(|context| async move {
6124 let cfg = Config {
6125 partition: "test-invalid".into(),
6126 items_per_section: NZU64!(5),
6127 compression: None,
6128 codec_config: (),
6129 write_buffer: NZUsize!(1024),
6130 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6131 };
6132
6133 #[allow(clippy::reversed_empty_ranges)]
6134 let _result = Journal::<deterministic::Context, u64>::init_sync(
6135 context.child("storage"),
6136 cfg,
6137 10..5, )
6139 .await;
6140 });
6141 }
6142
6143 #[test_traced]
6145 fn test_init_sync_existing_data_exact_match() {
6146 let executor = deterministic::Runner::default();
6147 executor.start(|context| async move {
6148 let items_per_section = NZU64!(5);
6149 let cfg = Config {
6150 partition: "test-exact-match".into(),
6151 items_per_section,
6152 compression: None,
6153 codec_config: (),
6154 write_buffer: NZUsize!(1024),
6155 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6156 };
6157
6158 let mut journal =
6160 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
6161 .await
6162 .expect("Failed to create initial journal");
6163
6164 for i in 0..20u64 {
6166 journal.append(&(i * 100)).await.unwrap();
6167 }
6168 journal.sync().await.unwrap();
6169 drop(journal);
6170
6171 let lower_bound = 5; let upper_bound = 20; let mut journal = Journal::<deterministic::Context, u64>::init_sync(
6175 context.child("storage"),
6176 cfg.clone(),
6177 lower_bound..upper_bound,
6178 )
6179 .await
6180 .expect("Failed to initialize journal with exact match");
6181
6182 assert_eq!(journal.size(), 20);
6183
6184 assert_eq!(journal.bounds().start, 5); assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
6189 assert!(matches!(journal.read(4).await, Err(Error::ItemPruned(_))));
6190
6191 assert_eq!(journal.read(5).await.unwrap(), 500);
6193 assert_eq!(journal.read(10).await.unwrap(), 1000);
6194 assert_eq!(journal.read(19).await.unwrap(), 1900);
6195
6196 assert!(matches!(
6198 journal.read(20).await,
6199 Err(Error::ItemOutOfRange(_))
6200 ));
6201
6202 let pos = journal.append(&999).await.unwrap();
6204 assert_eq!(pos, 20);
6205 assert_eq!(journal.read(20).await.unwrap(), 999);
6206
6207 journal.destroy().await.unwrap();
6208 });
6209 }
6210
6211 #[test_traced]
6214 fn test_init_sync_existing_data_exceeds_upper_bound() {
6215 let executor = deterministic::Runner::default();
6216 executor.start(|context| async move {
6217 let items_per_section = NZU64!(5);
6218 let cfg = Config {
6219 partition: "test-unexpected-data".into(),
6220 items_per_section,
6221 compression: None,
6222 codec_config: (),
6223 write_buffer: NZUsize!(1024),
6224 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6225 };
6226
6227 let mut journal =
6229 Journal::<deterministic::Context, u64>::init(context.child("initial"), cfg.clone())
6230 .await
6231 .expect("Failed to create initial journal");
6232
6233 for i in 0..30u64 {
6235 journal.append(&(i * 1000)).await.unwrap();
6236 }
6237 journal.sync().await.unwrap();
6238 drop(journal);
6239
6240 let lower_bound = 8; for (i, upper_bound) in (9..29).enumerate() {
6243 let result = Journal::<deterministic::Context, u64>::init_sync(
6244 context.child("sync").with_attribute("index", i),
6245 cfg.clone(),
6246 lower_bound..upper_bound,
6247 )
6248 .await;
6249
6250 assert!(matches!(result, Err(Error::ItemOutOfRange(_))));
6252 }
6253 });
6254 }
6255
6256 #[test_traced]
6258 fn test_init_sync_empty_stale_position_beyond_upper_bound() {
6259 let executor = deterministic::Runner::default();
6260 executor.start(|context| async move {
6261 let cfg = Config {
6262 partition: "test-empty-stale-position".into(),
6263 items_per_section: NZU64!(5),
6264 compression: None,
6265 codec_config: (),
6266 write_buffer: NZUsize!(1024),
6267 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6268 };
6269
6270 let stale_size = 30;
6271 let journal = Journal::<deterministic::Context, u64>::init_at_size(
6272 context.child("first"),
6273 cfg.clone(),
6274 stale_size,
6275 )
6276 .await
6277 .expect("Failed to create stale empty journal");
6278 assert_eq!(journal.size(), stale_size);
6279 assert!(journal.bounds().is_empty());
6280 drop(journal);
6281
6282 let lower_bound = 10;
6283 let upper_bound = 26;
6284 let mut journal = Journal::<deterministic::Context, u64>::init_sync(
6285 context.child("second"),
6286 cfg.clone(),
6287 lower_bound..upper_bound,
6288 )
6289 .await
6290 .expect("Failed to repair stale empty journal");
6291
6292 assert_eq!(journal.size(), lower_bound);
6293 assert!(journal.bounds().is_empty());
6294
6295 let pos = journal.append(&999).await.unwrap();
6296 assert_eq!(pos, lower_bound);
6297 assert_eq!(journal.read(pos).await.unwrap(), 999);
6298
6299 journal.destroy().await.unwrap();
6300 });
6301 }
6302
6303 #[test_traced]
6305 fn test_init_sync_recovers_from_stale_clear_to_size() {
6306 let executor = deterministic::Runner::default();
6307 executor.start(|context| async move {
6308 let cfg = Config {
6309 partition: "test-stale-clear-to-size".into(),
6310 items_per_section: NZU64!(5),
6311 compression: None,
6312 codec_config: (),
6313 write_buffer: NZUsize!(1024),
6314 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6315 };
6316
6317 let mut journal = Journal::<deterministic::Context, u64>::init_at_size(
6318 context.child("first"),
6319 cfg.clone(),
6320 9,
6321 )
6322 .await
6323 .expect("Failed to create stale empty journal");
6324 journal.sync().await.unwrap();
6325 drop(journal);
6326
6327 match context.remove(&cfg.data_partition(), None).await {
6330 Ok(()) | Err(commonware_runtime::Error::PartitionMissing(_)) => {}
6331 Err(error) => panic!("failed to clear data partition: {error}"),
6332 }
6333
6334 let lower_bound = 7;
6335 let upper_bound = 20;
6336 let journal = Journal::<deterministic::Context, u64>::init_sync(
6337 context.child("second"),
6338 cfg.clone(),
6339 lower_bound..upper_bound,
6340 )
6341 .await
6342 .expect("Failed to repair stale empty journal");
6343
6344 assert_eq!(journal.size(), lower_bound);
6345 let bounds = journal.bounds();
6346 assert!(bounds.is_empty());
6347 assert_eq!(bounds.start, lower_bound);
6348
6349 journal.destroy().await.unwrap();
6350 });
6351 }
6352
6353 #[test_traced]
6355 fn test_init_sync_existing_data_stale() {
6356 let executor = deterministic::Runner::default();
6357 executor.start(|context| async move {
6358 let items_per_section = NZU64!(5);
6359 let cfg = Config {
6360 partition: "test-stale".into(),
6361 items_per_section,
6362 compression: None,
6363 codec_config: (),
6364 write_buffer: NZUsize!(1024),
6365 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6366 };
6367
6368 let mut journal =
6370 Journal::<deterministic::Context, u64>::init(context.child("first"), cfg.clone())
6371 .await
6372 .expect("Failed to create initial journal");
6373
6374 for i in 0..10u64 {
6376 journal.append(&(i * 100)).await.unwrap();
6377 }
6378 journal.sync().await.unwrap();
6379 drop(journal);
6380
6381 let lower_bound = 15; let upper_bound = 26; let journal = Journal::<deterministic::Context, u64>::init_sync(
6385 context.child("second"),
6386 cfg.clone(),
6387 lower_bound..upper_bound,
6388 )
6389 .await
6390 .expect("Failed to initialize journal with stale data");
6391
6392 assert_eq!(journal.size(), 15);
6393
6394 assert!(journal.bounds().is_empty());
6396
6397 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
6399 assert!(matches!(journal.read(9).await, Err(Error::ItemPruned(_))));
6400 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(_))));
6401
6402 journal.destroy().await.unwrap();
6403 });
6404 }
6405
6406 #[test_traced]
6408 fn test_init_sync_blob_boundaries() {
6409 let executor = deterministic::Runner::default();
6410 executor.start(|context| async move {
6411 let items_per_section = NZU64!(5);
6412 let cfg = Config {
6413 partition: "test-boundaries".into(),
6414 items_per_section,
6415 compression: None,
6416 codec_config: (),
6417 write_buffer: NZUsize!(1024),
6418 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6419 };
6420
6421 let mut journal =
6423 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
6424 .await
6425 .expect("Failed to create initial journal");
6426
6427 for i in 0..25u64 {
6429 journal.append(&(i * 100)).await.unwrap();
6430 }
6431 journal.sync().await.unwrap();
6432 drop(journal);
6433
6434 let lower_bound = 15; let upper_bound = 25; let mut journal = Journal::<deterministic::Context, u64>::init_sync(
6438 context.child("storage"),
6439 cfg.clone(),
6440 lower_bound..upper_bound,
6441 )
6442 .await
6443 .expect("Failed to initialize journal at boundaries");
6444
6445 assert_eq!(journal.size(), 25);
6446
6447 assert_eq!(journal.bounds().start, 15);
6449
6450 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
6452 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(_))));
6453
6454 assert_eq!(journal.read(15).await.unwrap(), 1500);
6456 assert_eq!(journal.read(20).await.unwrap(), 2000);
6457 assert_eq!(journal.read(24).await.unwrap(), 2400);
6458
6459 assert!(matches!(
6461 journal.read(25).await,
6462 Err(Error::ItemOutOfRange(_))
6463 ));
6464
6465 let pos = journal.append(&999).await.unwrap();
6467 assert_eq!(pos, 25);
6468 assert_eq!(journal.read(25).await.unwrap(), 999);
6469
6470 journal.destroy().await.unwrap();
6471 });
6472 }
6473
6474 #[test_traced]
6476 fn test_init_sync_same_blob_bounds() {
6477 let executor = deterministic::Runner::default();
6478 executor.start(|context| async move {
6479 let items_per_section = NZU64!(5);
6480 let cfg = Config {
6481 partition: "test-same-blob".into(),
6482 items_per_section,
6483 compression: None,
6484 codec_config: (),
6485 write_buffer: NZUsize!(1024),
6486 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
6487 };
6488
6489 let mut journal =
6491 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
6492 .await
6493 .expect("Failed to create initial journal");
6494
6495 for i in 0..15u64 {
6497 journal.append(&(i * 100)).await.unwrap();
6498 }
6499 journal.sync().await.unwrap();
6500 drop(journal);
6501
6502 let lower_bound = 10; let upper_bound = 15; let mut journal = Journal::<deterministic::Context, u64>::init_sync(
6506 context.child("storage"),
6507 cfg.clone(),
6508 lower_bound..upper_bound,
6509 )
6510 .await
6511 .expect("Failed to initialize journal with same-blob bounds");
6512
6513 assert_eq!(journal.size(), 15);
6514
6515 assert_eq!(journal.bounds().start, 10);
6518
6519 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
6521 assert!(matches!(journal.read(9).await, Err(Error::ItemPruned(_))));
6522
6523 assert_eq!(journal.read(10).await.unwrap(), 1000);
6525 assert_eq!(journal.read(11).await.unwrap(), 1100);
6526 assert_eq!(journal.read(14).await.unwrap(), 1400);
6527
6528 assert!(matches!(
6530 journal.read(15).await,
6531 Err(Error::ItemOutOfRange(_))
6532 ));
6533
6534 let pos = journal.append(&999).await.unwrap();
6536 assert_eq!(pos, 15);
6537 assert_eq!(journal.read(15).await.unwrap(), 999);
6538
6539 journal.destroy().await.unwrap();
6540 });
6541 }
6542
6543 #[test_traced]
6548 fn test_single_item_per_blob() {
6549 let executor = deterministic::Runner::default();
6550 executor.start(|context| async move {
6551 let cfg = Config {
6552 partition: "single-item-per-blob".into(),
6553 items_per_section: NZU64!(1),
6554 compression: None,
6555 codec_config: (),
6556 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
6557 write_buffer: NZUsize!(1024),
6558 };
6559
6560 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6562 .await
6563 .unwrap();
6564
6565 let bounds = journal.bounds();
6567 assert_eq!(bounds.end, 0);
6568 assert!(bounds.is_empty());
6569
6570 let pos = journal.append(&0).await.unwrap();
6572 assert_eq!(pos, 0);
6573 assert_eq!(journal.size(), 1);
6574
6575 journal.sync().await.unwrap();
6577
6578 let value = journal.read(journal.size() - 1).await.unwrap();
6580 assert_eq!(value, 0);
6581
6582 for i in 1..10u64 {
6584 let pos = journal.append(&(i * 100)).await.unwrap();
6585 assert_eq!(pos, i);
6586 assert_eq!(journal.size(), i + 1);
6587
6588 let value = journal.read(journal.size() - 1).await.unwrap();
6590 assert_eq!(value, i * 100);
6591 }
6592
6593 for i in 0..10u64 {
6595 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6596 }
6597
6598 journal.sync().await.unwrap();
6599
6600 let pruned = journal.prune(5).await.unwrap();
6603 assert!(pruned);
6604
6605 assert_eq!(journal.size(), 10);
6607
6608 assert_eq!(journal.bounds().start, 5);
6610
6611 let value = journal.read(journal.size() - 1).await.unwrap();
6613 assert_eq!(value, 900);
6614
6615 for i in 0..5 {
6617 assert!(matches!(
6618 journal.read(i).await,
6619 Err(crate::journal::Error::ItemPruned(_))
6620 ));
6621 }
6622
6623 for i in 5..10u64 {
6625 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6626 }
6627
6628 for i in 10..15u64 {
6630 let pos = journal.append(&(i * 100)).await.unwrap();
6631 assert_eq!(pos, i);
6632
6633 let value = journal.read(journal.size() - 1).await.unwrap();
6635 assert_eq!(value, i * 100);
6636 }
6637
6638 journal.sync().await.unwrap();
6639 drop(journal);
6640
6641 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6643 .await
6644 .unwrap();
6645
6646 assert_eq!(journal.size(), 15);
6648
6649 assert_eq!(journal.bounds().start, 5);
6651
6652 let value = journal.read(journal.size() - 1).await.unwrap();
6654 assert_eq!(value, 1400);
6655
6656 for i in 5..15u64 {
6658 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6659 }
6660
6661 journal.destroy().await.unwrap();
6662
6663 let mut journal = Journal::<_, u64>::init(context.child("third"), cfg.clone())
6666 .await
6667 .unwrap();
6668
6669 for i in 0..10u64 {
6671 journal.append(&(i * 1000)).await.unwrap();
6672 }
6673
6674 journal.prune(5).await.unwrap();
6676 let bounds = journal.bounds();
6677 assert_eq!(bounds.end, 10);
6678 assert_eq!(bounds.start, 5);
6679
6680 journal.sync().await.unwrap();
6682 drop(journal);
6683
6684 let journal = Journal::<_, u64>::init(context.child("fourth"), cfg.clone())
6686 .await
6687 .unwrap();
6688
6689 let bounds = journal.bounds();
6691 assert_eq!(bounds.end, 10);
6692 assert_eq!(bounds.start, 5);
6693
6694 let value = journal.read(journal.size() - 1).await.unwrap();
6696 assert_eq!(value, 9000);
6697
6698 for i in 5..10u64 {
6700 assert_eq!(journal.read(i).await.unwrap(), i * 1000);
6701 }
6702
6703 journal.destroy().await.unwrap();
6704
6705 let mut journal = Journal::<_, u64>::init(context.child("fifth"), cfg.clone())
6709 .await
6710 .unwrap();
6711
6712 for i in 0..5u64 {
6713 journal.append(&(i * 100)).await.unwrap();
6714 }
6715 journal.sync().await.unwrap();
6716
6717 journal.prune(5).await.unwrap();
6719 let bounds = journal.bounds();
6720 assert_eq!(bounds.end, 5); assert!(bounds.is_empty()); let result = journal.read(journal.size() - 1).await;
6725 assert!(matches!(result, Err(crate::journal::Error::ItemPruned(4))));
6726
6727 journal.append(&500).await.unwrap();
6729 let bounds = journal.bounds();
6730 assert_eq!(bounds.start, 5);
6731 assert_eq!(journal.read(bounds.end - 1).await.unwrap(), 500);
6732
6733 journal.destroy().await.unwrap();
6734 });
6735 }
6736
6737 #[test_traced]
6738 fn test_variable_journal_clear_to_size() {
6739 let executor = deterministic::Runner::default();
6740 executor.start(|context| async move {
6741 let cfg = Config {
6742 partition: "clear-test".into(),
6743 items_per_section: NZU64!(10),
6744 compression: None,
6745 codec_config: (),
6746 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
6747 write_buffer: NZUsize!(1024),
6748 };
6749
6750 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
6751 .await
6752 .unwrap();
6753
6754 for i in 0..25u64 {
6756 journal.append(&(i * 100)).await.unwrap();
6757 }
6758 let bounds = journal.bounds();
6759 assert_eq!(bounds.end, 25);
6760 assert_eq!(bounds.start, 0);
6761 journal.sync().await.unwrap();
6762
6763 journal.clear_to_size(100).await.unwrap();
6765 let bounds = journal.bounds();
6766 assert_eq!(bounds.end, 100);
6767 assert!(bounds.is_empty());
6768
6769 for i in 0..25 {
6771 assert!(matches!(
6772 journal.read(i).await,
6773 Err(crate::journal::Error::ItemPruned(_))
6774 ));
6775 }
6776
6777 drop(journal);
6779 let mut journal =
6780 Journal::<_, u64>::init(context.child("journal_after_clear"), cfg.clone())
6781 .await
6782 .unwrap();
6783 let bounds = journal.bounds();
6784 assert_eq!(bounds.end, 100);
6785 assert!(bounds.is_empty());
6786
6787 for i in 100..105u64 {
6789 let pos = journal.append(&(i * 100)).await.unwrap();
6790 assert_eq!(pos, i);
6791 }
6792 let bounds = journal.bounds();
6793 assert_eq!(bounds.end, 105);
6794 assert_eq!(bounds.start, 100);
6795
6796 for i in 100..105u64 {
6798 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6799 }
6800
6801 journal.sync().await.unwrap();
6803 drop(journal);
6804
6805 let journal = Journal::<_, u64>::init(context.child("journal_reopened"), cfg)
6806 .await
6807 .unwrap();
6808
6809 let bounds = journal.bounds();
6810 assert_eq!(bounds.end, 105);
6811 assert_eq!(bounds.start, 100);
6812 for i in 100..105u64 {
6813 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6814 }
6815
6816 journal.destroy().await.unwrap();
6817 });
6818 }
6819
6820 #[test_traced]
6821 fn test_variable_journal_metrics() {
6822 let executor = deterministic::Runner::default();
6823 executor.start(|context| async move {
6824 let cfg = Config {
6825 partition: "metrics".into(),
6826 items_per_section: NZU64!(2),
6827 compression: None,
6828 codec_config: (),
6829 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10)),
6830 write_buffer: NZUsize!(1024),
6831 };
6832 let mut journal = Journal::<_, u64>::init(context.child("variable_metrics"), cfg)
6833 .await
6834 .unwrap();
6835
6836 let items = [0, 1, 2, 3, 4];
6837 journal.append_many(Many::Flat(&items)).await.unwrap();
6838 journal.append(&5).await.unwrap();
6839 let reader = journal.snapshot().await.unwrap();
6840 reader.read(0).await.unwrap();
6841 reader.read_many(&[1, 2]).await.unwrap();
6842 reader.try_read_sync(3).unwrap();
6843 drop(reader);
6844 journal.commit().await.unwrap();
6845 journal.sync().await.unwrap();
6846 journal.prune(2).await.unwrap();
6847 journal.rewind(4).await.unwrap();
6848
6849 let buffer = context.encode();
6850 for expected in [
6851 "variable_metrics_size 4",
6852 "variable_metrics_pruning_boundary 2",
6853 "variable_metrics_retained 2",
6854 "variable_metrics_tail_items 2",
6855 "variable_metrics_append_calls_total 1",
6856 "variable_metrics_append_many_calls_total 1",
6857 "variable_metrics_read_calls_total 1",
6858 "variable_metrics_read_many_calls_total 1",
6859 "variable_metrics_items_read_total 4",
6860 "variable_metrics_commit_calls_total 1",
6861 "variable_metrics_sync_calls_total 1",
6862 "variable_metrics_append_duration_count 1",
6863 "variable_metrics_append_many_duration_count 1",
6864 "variable_metrics_read_duration_count 0",
6865 "variable_metrics_read_many_duration_count 1",
6866 "variable_metrics_commit_duration_count 1",
6867 "variable_metrics_sync_duration_count 1",
6868 "variable_metrics_cache_hits_total 4",
6869 "variable_metrics_cache_misses_total 0",
6870 "variable_metrics_data_tracked",
6871 "variable_metrics_offsets_size 4",
6872 "variable_metrics_offsets_blobs_tracked",
6873 ] {
6874 assert!(buffer.contains(expected), "{expected}\n{buffer}");
6875 }
6876
6877 journal.destroy().await.unwrap();
6878 });
6879 }
6880
6881 #[test_traced]
6882 fn test_variable_journal_read_miss_timed() {
6883 let executor = deterministic::Runner::default();
6885 executor.start(|context| async move {
6886 let cfg = Config {
6889 partition: "miss".into(),
6890 items_per_section: NZU64!(50),
6891 compression: None,
6892 codec_config: (),
6893 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10)),
6894 write_buffer: NZUsize!(1024),
6895 };
6896 let mut journal = Journal::<_, u64>::init(context.child("miss"), cfg)
6897 .await
6898 .unwrap();
6899 for i in 0..200u64 {
6900 journal.append(&i).await.unwrap();
6901 }
6902 journal.sync().await.unwrap();
6903
6904 let reader = journal.snapshot().await.unwrap();
6906 let pos = (0..200)
6907 .find(|&pos| reader.try_read_sync(pos).is_none())
6908 .expect("some position should be cold");
6909 assert_eq!(reader.read(pos).await.unwrap(), pos);
6910 drop(reader);
6911
6912 let buffer = context.encode();
6913 assert!(buffer.contains("miss_read_duration_count 1"), "{buffer}");
6914
6915 journal.destroy().await.unwrap();
6916 });
6917 }
6918
6919 #[test_traced]
6920 fn test_variable_snapshot_frozen_across_roll() {
6921 let executor = deterministic::Runner::default();
6922 executor.start(|context| async move {
6923 let cfg = Config {
6924 partition: "snapshot-frozen".into(),
6925 items_per_section: NZU64!(5),
6926 compression: None,
6927 codec_config: (),
6928 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
6929 write_buffer: NZUsize!(1024),
6930 };
6931 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
6932 .await
6933 .unwrap();
6934 for i in 0..7u64 {
6935 journal.append(&(i * 100)).await.unwrap();
6936 }
6937
6938 let snapshot = journal.snapshot().await.unwrap();
6939 assert_eq!(snapshot.bounds(), 0..7);
6940
6941 for i in 7..23u64 {
6944 journal.append(&(i * 100)).await.unwrap();
6945 }
6946 assert_eq!(snapshot.bounds(), 0..7);
6947 for i in 0..7u64 {
6948 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
6949 }
6950 assert!(matches!(
6951 snapshot.read(7).await,
6952 Err(Error::ItemOutOfRange(7))
6953 ));
6954
6955 let fresh = journal.snapshot().await.unwrap();
6956 assert_eq!(fresh.bounds(), 0..23);
6957 assert_eq!(fresh.read(22).await.unwrap(), 2200);
6958
6959 drop(snapshot);
6960 drop(fresh);
6961 journal.destroy().await.unwrap();
6962 });
6963 }
6964
6965 #[test_traced]
6966 fn test_variable_prune_under_snapshot() {
6967 let executor = deterministic::Runner::default();
6968 executor.start(|context| async move {
6969 let cfg = Config {
6970 partition: "snapshot-prune".into(),
6971 items_per_section: NZU64!(5),
6972 compression: Some(3),
6973 codec_config: (),
6974 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
6975 write_buffer: NZUsize!(1024),
6976 };
6977 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
6978 .await
6979 .unwrap();
6980 for i in 0..17u64 {
6981 journal.append(&(i * 100)).await.unwrap();
6982 }
6983 journal.sync().await.unwrap();
6984
6985 let snapshot = journal.snapshot().await.unwrap();
6986 assert!(journal.prune(12).await.unwrap());
6987
6988 assert_eq!(snapshot.bounds(), 0..17);
6990 for i in 0..17u64 {
6991 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
6992 }
6993 assert_eq!(
6994 snapshot.read_many(&[1, 2, 3, 11, 16]).await.unwrap(),
6995 vec![100, 200, 300, 1100, 1600]
6996 );
6997
6998 let fresh = journal.snapshot().await.unwrap();
6999 assert_eq!(fresh.bounds(), 10..17);
7000 assert!(matches!(fresh.read(3).await, Err(Error::ItemPruned(3))));
7001
7002 drop(snapshot);
7003 drop(fresh);
7004 journal.destroy().await.unwrap();
7005 });
7006 }
7007
7008 #[test_traced]
7009 fn test_variable_snapshots_readable_during_concurrent_appends() {
7010 let executor = deterministic::Runner::seeded(7);
7011 executor.start(|context| async move {
7012 let cfg = Config {
7013 partition: "snapshot-concurrent".into(),
7014 items_per_section: NZU64!(5),
7015 compression: None,
7016 codec_config: (),
7017 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7018 write_buffer: NZUsize!(1024),
7019 };
7020 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
7021 .await
7022 .unwrap();
7023
7024 let (mut tx, mut rx) =
7025 futures::channel::mpsc::channel::<Reader<'static, deterministic::Context, u64>>(8);
7026 let validator = context.child("validator").spawn(|_| async move {
7027 let mut validated = 0usize;
7028 while let Some(snapshot) = rx.next().await {
7029 let bounds = snapshot.bounds();
7030 for i in bounds.clone() {
7031 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
7032 }
7033 validated += (bounds.end - bounds.start) as usize;
7034 }
7035 validated
7036 });
7037
7038 for i in 0..40u64 {
7039 journal.append(&(i * 100)).await.unwrap();
7040 if i % 7 == 0 {
7041 let snapshot = journal.snapshot().await.unwrap();
7042 if tx.try_send(snapshot).is_err() {
7043 break;
7044 }
7045 }
7046 }
7047 drop(tx);
7048 assert!(validator.await.unwrap() > 0);
7049
7050 journal.destroy().await.unwrap();
7051 });
7052 }
7053
7054 #[test_traced]
7055 fn test_variable_replay_from_stale_snapshot() {
7056 let executor = deterministic::Runner::default();
7057 executor.start(|context| async move {
7058 let cfg = Config {
7059 partition: "snapshot-replay".into(),
7060 items_per_section: NZU64!(5),
7061 compression: None,
7062 codec_config: (),
7063 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7064 write_buffer: NZUsize!(1024),
7065 };
7066 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
7067 .await
7068 .unwrap();
7069 for i in 0..7u64 {
7070 journal.append(&(i * 100)).await.unwrap();
7071 }
7072
7073 let snapshot = journal.snapshot().await.unwrap();
7075 assert_eq!(snapshot.bounds(), 0..7);
7076
7077 for i in 7..23u64 {
7079 journal.append(&(i * 100)).await.unwrap();
7080 }
7081 assert!(journal.prune(12).await.unwrap());
7082
7083 {
7084 let stream = snapshot.replay(0, NZUsize!(1024)).await.unwrap();
7085 futures::pin_mut!(stream);
7086 let mut expected = 0u64;
7087 while let Some(result) = stream.next().await {
7088 let (pos, item) = result.unwrap();
7089 assert_eq!(pos, expected);
7090 assert_eq!(item, pos * 100);
7091 expected += 1;
7092 }
7093 assert_eq!(expected, 7);
7094 }
7095
7096 drop(snapshot);
7097 journal.destroy().await.unwrap();
7098 });
7099 }
7100
7101 #[test_traced]
7104 fn test_variable_recovery_full_newest_blob_without_successor() {
7105 let executor = deterministic::Runner::default();
7106 executor.start(|context| async move {
7107 let cfg = Config {
7108 partition: "recovery-full-newest-no-successor".into(),
7109 items_per_section: NZU64!(10),
7110 compression: None,
7111 codec_config: (),
7112 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7113 write_buffer: NZUsize!(1024),
7114 };
7115
7116 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7117 .await
7118 .unwrap();
7119 for i in 0..10u64 {
7120 journal.append(&(i * 100)).await.unwrap();
7121 }
7122 journal.sync().await.unwrap();
7123 drop(journal);
7124
7125 let data_partition = cfg.data_partition();
7128 context
7129 .remove(&data_partition, Some(&1u64.to_be_bytes()))
7130 .await
7131 .unwrap();
7132
7133 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7134 .await
7135 .unwrap();
7136 assert_eq!(journal.bounds(), 0..10);
7137 for i in 0..10u64 {
7138 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7139 }
7140 assert_eq!(journal.append(&1000).await.unwrap(), 10);
7141 assert_eq!(journal.read(10).await.unwrap(), 1000);
7142
7143 journal.destroy().await.unwrap();
7144 });
7145 }
7146
7147 #[test_traced]
7151 fn test_variable_rewind_crash_before_data_truncation() {
7152 let executor = deterministic::Runner::default();
7153 executor.start(|context| async move {
7154 let cfg = Config {
7155 partition: "rewind-crash-offsets-only".into(),
7156 items_per_section: NZU64!(10),
7157 compression: None,
7158 codec_config: (),
7159 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7160 write_buffer: NZUsize!(1024),
7161 };
7162
7163 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7164 .await
7165 .unwrap();
7166 for i in 0..25u64 {
7167 journal.append(&(i * 100)).await.unwrap();
7168 }
7169 journal.sync().await.unwrap();
7170
7171 journal.test_rewind_offsets(12).await.unwrap();
7174 drop(journal);
7175
7176 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7177 .await
7178 .unwrap();
7179 assert_eq!(journal.bounds(), 0..25);
7180 for i in 0..25u64 {
7181 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7182 }
7183
7184 journal.destroy().await.unwrap();
7185 });
7186 }
7187
7188 #[test_traced]
7190 fn test_variable_recovery_rejects_gap_in_retained_blobs() {
7191 let executor = deterministic::Runner::default();
7192 executor.start(|context| async move {
7193 let cfg = Config {
7194 partition: "recovery-gap-in-retained-blobs".into(),
7195 items_per_section: NZU64!(10),
7196 compression: None,
7197 codec_config: (),
7198 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7199 write_buffer: NZUsize!(1024),
7200 };
7201
7202 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7203 .await
7204 .unwrap();
7205 for i in 0..25u64 {
7206 journal.append(&(i * 100)).await.unwrap();
7207 }
7208 journal.sync().await.unwrap();
7209 drop(journal);
7210
7211 context
7213 .remove(&cfg.data_partition(), Some(&1u64.to_be_bytes()))
7214 .await
7215 .unwrap();
7216
7217 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
7218 assert!(matches!(result, Err(Error::Corruption(_))));
7219 });
7220 }
7221}