1use super::{
15 Contiguous, Many, Mutable, blob_first_position,
16 blobs::{Blob, Blobs, Partition, Replay as BlobReplay, Writable},
17 fixed,
18 metrics::Metrics,
19 position_to_blob,
20};
21#[commonware_macros::stability(ALPHA)]
22use crate::journal::authenticated;
23use crate::{
24 Context, SyncCompletion,
25 journal::{
26 Error,
27 durability::Barrier,
28 frame::{
29 FrameInfo, decode_item, decode_length_prefix, encode_frame_into, find_frame,
30 read_frame_at,
31 },
32 },
33};
34use commonware_codec::{Codec, CodecShared, varint::MAX_U32_VARINT_SIZE};
35use commonware_macros::boxed;
36use commonware_runtime::{
37 Blob as RBlob, Buf, Handle, IoBuf, ReadOptions,
38 buffer::paged::{CacheRef, Replay, Writer},
39};
40use futures::{
41 FutureExt as _, Stream,
42 future::{try_join, try_join_all},
43};
44use std::{
45 collections::BTreeMap,
46 io::Cursor,
47 marker::PhantomData,
48 num::{NonZeroU64, NonZeroUsize},
49 ops::Range,
50 sync::Arc,
51};
52#[commonware_macros::stability(ALPHA)]
53use tracing::debug;
54use tracing::warn;
55
56pub struct PreparedAppend<V> {
59 encoded: Vec<u8>,
60 item_starts: Vec<usize>,
61 compressed: bool,
62 _marker: PhantomData<V>,
63}
64
65const DATA_SUFFIX: &str = "_data";
67
68const OFFSETS_SUFFIX: &str = "_offsets";
70
71fn decode_frame_from_span<V: CodecShared>(
75 bytes: &[u8],
76 frame_len: usize,
77 codec_config: &V::Cfg,
78 compressed: bool,
79) -> Option<V> {
80 let mut cursor = Cursor::new(bytes);
81 let (size, varint_len) = decode_length_prefix(&mut cursor).ok()?;
82 let actual_len = size.checked_add(varint_len)?;
83 if actual_len != frame_len || frame_len > bytes.len() {
84 return None;
85 }
86 decode_item::<V>(&bytes[varint_len..frame_len], codec_config, compressed).ok()
87}
88
89enum Frame {
91 Item { offset: u64 },
93 End { valid_size: u64, torn: bool },
96}
97
98struct FrameScanner<'a, B: RBlob, V: Codec> {
100 replay: Replay<B>,
101 offset: u64,
103 codec_config: &'a V::Cfg,
104 compressed: bool,
105}
106
107impl<'a, B: RBlob, V: CodecShared> FrameScanner<'a, B, V> {
108 const fn new(replay: Replay<B>, codec_config: &'a V::Cfg, compressed: bool) -> Self {
109 Self {
110 replay,
111 offset: 0,
112 codec_config,
113 compressed,
114 }
115 }
116
117 async fn next(&mut self) -> Result<Frame, Error> {
122 match self.replay.ensure(MAX_U32_VARINT_SIZE).await {
123 Ok(true) => {}
124 Ok(false) if self.replay.remaining() == 0 => {
125 return Ok(Frame::End {
126 valid_size: self.offset,
127 torn: false,
128 });
129 }
130 Ok(false) => {}
132 Err(err) => return Err(err.into()),
133 }
134
135 let before_remaining = self.replay.remaining();
136 let (item_size, varint_len) = match decode_length_prefix(&mut self.replay) {
137 Ok(result) => result,
138 Err(err) => {
139 if self.replay.is_exhausted() || before_remaining < MAX_U32_VARINT_SIZE {
142 return Ok(Frame::End {
143 valid_size: self.offset,
144 torn: true,
145 });
146 }
147 return Err(err);
148 }
149 };
150
151 match self.replay.ensure(item_size).await {
152 Ok(true) => {}
153 Ok(false) => {
154 return Ok(Frame::End {
155 valid_size: self.offset,
156 torn: true,
157 });
158 }
159 Err(err) => return Err(err.into()),
160 }
161
162 let item_offset = self.offset;
163 let next_offset = item_offset
164 .checked_add(varint_len as u64)
165 .and_then(|offset| offset.checked_add(item_size as u64))
166 .ok_or(Error::OffsetOverflow)?;
167 decode_item::<V>(
168 (&mut self.replay).take(item_size),
169 self.codec_config,
170 self.compressed,
171 )?;
172 self.offset = next_offset;
173 Ok(Frame::Item {
174 offset: item_offset,
175 })
176 }
177}
178
179struct BlobScan {
181 items: u64,
183 valid_size: u64,
185 torn: bool,
187}
188
189struct ReplayState<'a, B: RBlob, V: Codec> {
194 blob: u64,
196 replay: BlobReplay<'a, B>,
198 budget: u64,
200 pos: u64,
202 end_pos: u64,
204 offset: u64,
206 codec_config: V::Cfg,
208 compressed: bool,
210 _marker: PhantomData<V>,
211}
212
213impl<B: RBlob, V: CodecShared> super::ReplayBatchState for ReplayState<'_, B, V> {
214 type Item = V;
215
216 async fn next_batch(mut self) -> Option<(Vec<Result<(u64, V), Error>>, Self)> {
218 if self.pos == self.end_pos {
219 return None;
220 }
221
222 let mut batch = Vec::new();
223 let mut consumed = 0u64;
224 loop {
225 if self.pos == self.end_pos {
226 return (!batch.is_empty()).then_some((batch, self));
227 }
228
229 match self.replay.ensure(MAX_U32_VARINT_SIZE).await {
232 Ok(true) => {}
233 Ok(false) if self.replay.remaining() == 0 => {
234 batch.push(Err(Error::Corruption(format!(
235 "data blob {} ended before position {}",
236 self.blob, self.pos
237 ))));
238 self.pos = self.end_pos;
239 return Some((batch, self));
240 }
241 Ok(false) => {}
242 Err(err) => {
243 batch.push(Err(err));
244 self.pos = self.end_pos;
245 return Some((batch, self));
246 }
247 }
248
249 let before_remaining = self.replay.remaining();
250 let (item_size, varint_len) = match decode_length_prefix(&mut self.replay) {
251 Ok(result) => result,
252 Err(err) => {
253 if self.replay.is_exhausted() || before_remaining < MAX_U32_VARINT_SIZE {
254 batch.push(Err(Error::Corruption(format!(
255 "incomplete frame header in data blob {} at offset {}",
256 self.blob, self.offset
257 ))));
258 } else {
259 batch.push(Err(err));
260 }
261 self.pos = self.end_pos;
262 return Some((batch, self));
263 }
264 };
265
266 match self.replay.ensure(item_size).await {
267 Ok(true) => {}
268 Ok(false) => {
269 batch.push(Err(Error::Corruption(format!(
270 "incomplete frame in data blob {} at offset {}",
271 self.blob, self.offset
272 ))));
273 self.pos = self.end_pos;
274 return Some((batch, self));
275 }
276 Err(err) => {
277 batch.push(Err(err));
278 self.pos = self.end_pos;
279 return Some((batch, self));
280 }
281 }
282
283 let next_offset = self
284 .offset
285 .checked_add(varint_len as u64)
286 .and_then(|offset| offset.checked_add(item_size as u64));
287 let Some(next_offset) = next_offset else {
288 batch.push(Err(Error::OffsetOverflow));
289 self.pos = self.end_pos;
290 return Some((batch, self));
291 };
292 let item_len = next_offset - self.offset;
293
294 match decode_item::<V>(
297 (&mut self.replay).take(item_size),
298 &self.codec_config,
299 self.compressed,
300 ) {
301 Ok(item) => {
302 let pos = self.pos;
303 let Some(next_pos) = self.pos.checked_add(1) else {
304 batch.push(Err(Error::OffsetOverflow));
305 self.pos = self.end_pos;
306 return Some((batch, self));
307 };
308 self.pos = next_pos;
309 self.offset = next_offset;
310 consumed = match consumed.checked_add(item_len) {
311 Some(consumed) => consumed,
312 None => {
313 batch.push(Err(Error::OffsetOverflow));
314 self.pos = self.end_pos;
315 return Some((batch, self));
316 }
317 };
318 batch.push(Ok((pos, item)));
319 }
320 Err(err) => {
321 batch.push(Err(err));
322 self.pos = self.end_pos;
323 return Some((batch, self));
324 }
325 }
326
327 if consumed >= self.budget {
330 return Some((batch, self));
331 }
332 if self.replay.remaining() < MAX_U32_VARINT_SIZE {
333 return Some((batch, self));
334 }
335 }
336 }
337}
338
339#[derive(Clone)]
341pub struct Config<C> {
342 pub partition: String,
344
345 pub items_per_section: NonZeroU64,
350
351 pub compression: Option<u8>,
353
354 pub codec_config: C,
356
357 pub page_cache: CacheRef,
359
360 pub write_buffer: NonZeroUsize,
362
363 pub replay_buffer: NonZeroUsize,
365}
366
367impl<C> Config<C> {
368 fn data_partition(&self) -> String {
370 format!("{}{}", self.partition, DATA_SUFFIX)
371 }
372
373 fn offsets_partition(&self) -> String {
375 format!("{}{}", self.partition, OFFSETS_SUFFIX)
376 }
377}
378
379struct Inner<E: Context, V: Codec> {
381 blobs: Writable<E>,
383
384 offsets: Box<fixed::Inner<E, u64>>,
387
388 bounds: Range<u64>,
390
391 #[cfg(test)]
394 halt_before_offsets_prune: bool,
395
396 items_per_blob: NonZeroU64,
403
404 compression: Option<u8>,
406
407 codec_config: V::Cfg,
409
410 metrics: Arc<Metrics<E>>,
412
413 barrier: Barrier,
417}
418
419pub struct Reader<'a, E: Context, V: Codec> {
421 data: Blobs<'a, E::Blob>,
423
424 bounds: Range<u64>,
426
427 offsets: fixed::Reader<'a, E, u64>,
429
430 items_per_blob: NonZeroU64,
432
433 codec_config: V::Cfg,
435
436 compressed: bool,
438
439 metrics: Arc<Metrics<E>>,
441}
442
443impl<'a, E: Context, V: CodecShared> Reader<'a, E, V> {
444 const fn validate_readable(&self, position: u64) -> Result<(), Error> {
446 if position >= self.bounds.end {
447 return Err(Error::ItemOutOfRange(position));
448 }
449 if position < self.bounds.start {
450 return Err(Error::ItemPruned(position));
451 }
452 Ok(())
453 }
454
455 async fn read_at_offset(&self, blob: &Blob<'_, E::Blob>, offset: u64) -> Result<V, Error> {
457 read_frame_at(blob, offset, &self.codec_config, self.compressed)
458 .await
459 .map(|(_, _, item)| item)
460 }
461
462 async fn read_consecutive(
469 &self,
470 blob_handle: &Blob<'_, E::Blob>,
471 blob: u64,
472 offsets: &[u64],
473 ) -> Result<Vec<V>, Error> {
474 if offsets.len() <= 1 {
476 let mut items = Vec::with_capacity(offsets.len());
477 for &offset in offsets {
478 items.push(self.read_at_offset(blob_handle, offset).await?);
479 }
480 return Ok(items);
481 }
482
483 for window in offsets.windows(2) {
484 if window[1] <= window[0] {
485 return Err(Error::Corruption(format!(
486 "non-increasing offsets in blob {blob}: {} >= {}",
487 window[0], window[1]
488 )));
489 }
490 }
491
492 let start = offsets[0];
495 let end = offsets[offsets.len() - 1];
496 let range_len = usize::try_from(end - start).map_err(|_| Error::OffsetOverflow)?;
497 let bytes = blob_handle.read_at(start, range_len).await?.coalesce();
498 let bytes = bytes.as_ref();
499
500 let mut items = Vec::with_capacity(offsets.len());
501 let mut local_offset = 0usize;
502 for window in offsets.windows(2) {
503 let offset = window[0];
504 let next_offset = window[1];
505 let item_len =
506 usize::try_from(next_offset - offset).map_err(|_| Error::OffsetOverflow)?;
507
508 let mut cursor = Cursor::new(&bytes[local_offset..]);
509 let (size, varint_len) = decode_length_prefix(&mut cursor)?;
510 let actual_len = size.checked_add(varint_len).ok_or(Error::OffsetOverflow)?;
511 if actual_len != item_len {
512 return Err(Error::OffsetDataMismatch {
513 section: blob,
514 offset,
515 expected_len: item_len,
516 actual_len,
517 });
518 }
519
520 let data_start = local_offset
523 .checked_add(varint_len)
524 .ok_or(Error::OffsetOverflow)?;
525 let data_end = local_offset
526 .checked_add(item_len)
527 .ok_or(Error::OffsetOverflow)?;
528 items.push(decode_item::<V>(
529 &bytes[data_start..data_end],
530 &self.codec_config,
531 self.compressed,
532 )?);
533
534 local_offset = data_end;
535 }
536
537 items.push(self.read_at_offset(blob_handle, end).await?);
538 Ok(items)
539 }
540
541 fn try_read_frame_sync(&self, position: u64, offset: u64, buf: &mut Vec<u8>) -> Option<V> {
544 let blob = self
545 .data
546 .get(position_to_blob(position, self.items_per_blob.get()))?;
547 let remaining = blob.size().checked_sub(offset)?;
548 let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
549 if header_len == 0 {
550 return None;
551 }
552
553 let mut header = [0u8; MAX_U32_VARINT_SIZE];
555 if !blob.try_read_sync_into(&mut header[..header_len], offset) {
556 return None;
557 }
558 let mut cursor = Cursor::new(&header[..header_len]);
559 let (_, item_info) = find_frame(&mut cursor, offset).ok()?;
560
561 let (varint_len, data_len) = match item_info {
562 FrameInfo::Complete {
563 varint_len,
564 data_len,
565 } => (varint_len, data_len),
566 FrameInfo::Incomplete {
567 varint_len,
568 total_len,
569 ..
570 } => (varint_len, total_len),
571 };
572 let item_len = varint_len.checked_add(data_len)?;
573 if item_len > usize::try_from(remaining).ok()? {
574 return None;
575 }
576
577 if item_len <= header_len {
579 return decode_item::<V>(
580 &header[varint_len..varint_len + data_len],
581 &self.codec_config,
582 self.compressed,
583 )
584 .ok();
585 }
586
587 buf.resize(item_len, 0);
589 if !blob.try_read_sync_into(buf, offset) {
590 return None;
591 }
592 decode_item::<V>(
593 &buf[varint_len..varint_len + data_len],
594 &self.codec_config,
595 self.compressed,
596 )
597 .ok()
598 }
599
600 async fn replay_states(
602 &self,
603 start_pos: u64,
604 buffer: NonZeroUsize,
605 read_options: ReadOptions,
606 ) -> Result<Vec<ReplayState<'a, E::Blob, V>>, Error> {
607 let bounds = self.bounds();
608 if start_pos > bounds.end {
609 return Err(Error::ItemOutOfRange(start_pos));
610 }
611 if start_pos < bounds.start {
612 return Err(Error::ItemPruned(start_pos));
613 }
614
615 let mut states = Vec::new();
616 if start_pos < bounds.end {
617 let items_per_blob = self.items_per_blob.get();
620 let start_blob = position_to_blob(start_pos, items_per_blob);
621 let end_blob = position_to_blob(bounds.end - 1, items_per_blob);
622 let start_offset = self.offsets.read(start_pos).await?;
623
624 for blob in start_blob..=end_blob {
625 let blob_handle = self
626 .data
627 .get(blob)
628 .expect("positions in bounds map to a retained blob");
629 let offset = if blob == start_blob { start_offset } else { 0 };
630
631 let first_pos = if blob == start_blob {
632 start_pos
633 } else {
634 blob_first_position(blob, items_per_blob)?
635 };
636 let end_pos = super::blob_end_position(blob, items_per_blob, bounds.end);
637
638 states.push(ReplayState::<E::Blob, V> {
641 blob,
642 replay: blob_handle.replay_from(offset, buffer, read_options)?,
643 budget: buffer.get() as u64,
644 pos: first_pos,
645 end_pos,
646 offset,
647 codec_config: self.codec_config.clone(),
648 compressed: self.compressed,
649 _marker: PhantomData,
650 });
651 }
652 }
653
654 Ok(states)
655 }
656
657 fn validate_read_many(&self, positions: &[u64]) -> Result<(), Error> {
660 if positions[0] < self.bounds.start {
661 return Err(Error::ItemPruned(positions[0]));
662 }
663 let last_position = *positions.last().expect("positions is not empty");
664 if last_position >= self.bounds.end {
665 return Err(Error::ItemOutOfRange(last_position));
666 }
667 assert!(
668 positions.is_sorted_by(|a, b| a < b),
669 "positions must be strictly increasing"
670 );
671 Ok(())
672 }
673
674 async fn read_misses(
678 &self,
679 result: &mut [Option<V>],
680 miss_indices: Option<&[usize]>,
681 miss_positions: &[u64],
682 miss_offsets: &[u64],
683 ) -> Result<(), Error> {
684 let items_per_blob = self.items_per_blob.get();
687 let mut runs = Vec::new();
688 let mut group_start = 0;
689 while group_start < miss_positions.len() {
690 let blob = position_to_blob(miss_positions[group_start], items_per_blob);
691 let mut group_end = group_start + 1;
692 while group_end < miss_positions.len()
693 && position_to_blob(miss_positions[group_end], items_per_blob) == blob
694 {
695 group_end += 1;
696 }
697
698 let blob_handle = self
699 .data
700 .get(blob)
701 .expect("positions in bounds map to a retained blob");
702 let mut run_start = group_start;
705 while run_start < group_end {
706 let mut run_end = run_start + 1;
707 while run_end < group_end
708 && miss_positions[run_end - 1].checked_add(1) == Some(miss_positions[run_end])
709 {
710 run_end += 1;
711 }
712 runs.push((run_start, run_end, blob, blob_handle.clone()));
713 run_start = run_end;
714 }
715 group_start = group_end;
716 }
717
718 let run_items = try_join_all(runs.iter().map(|(run_start, run_end, blob, handle)| {
719 self.read_consecutive(handle, *blob, &miss_offsets[*run_start..*run_end])
720 }))
721 .await?;
722 for ((run_start, _, _, _), items) in runs.iter().zip(run_items) {
723 for (k, item) in items.into_iter().enumerate() {
724 let slot = miss_indices.map_or(run_start + k, |indices| indices[run_start + k]);
725 result[slot] = Some(item);
726 }
727 }
728
729 Ok(())
730 }
731
732 fn read_many_sync_pass(&self, positions: &[u64], out: &mut [Option<V>]) -> Vec<Option<u64>> {
738 let mut resolved: Vec<Option<u64>> = vec![None; positions.len()];
739 if positions.is_empty() {
740 return resolved;
741 }
742
743 let mut lookups: Vec<u64> = Vec::with_capacity(positions.len() * 2);
748 for &position in positions {
749 if lookups.last() != Some(&position) {
750 lookups.push(position);
751 }
752 match position.checked_add(1) {
753 Some(next) if next < self.bounds.end => lookups.push(next),
754 _ => {}
755 }
756 }
757 let offsets = self.offsets.probe_items(&lookups);
758
759 let items_per_blob = self.items_per_blob.get();
763 let mut extents: Vec<(usize, u64, usize)> = Vec::with_capacity(positions.len());
764 let mut singles: Vec<(usize, u64)> = Vec::new();
765 let mut lookup_idx = 0;
766 for (idx, &position) in positions.iter().enumerate() {
767 while lookups[lookup_idx] != position {
768 lookup_idx += 1;
769 }
770 if self.validate_readable(position).is_err() {
771 continue;
772 }
773 let Some(offset) = offsets[lookup_idx] else {
774 continue;
775 };
776 resolved[idx] = Some(offset);
777
778 let next = position + 1;
782 let next_offset = if next < self.bounds.end
783 && position_to_blob(position, items_per_blob)
784 == position_to_blob(next, items_per_blob)
785 {
786 offsets[lookup_idx + 1]
787 } else {
788 None
789 };
790 match next_offset {
791 Some(next) if next > offset => {
792 extents.push((idx, offset, (next - offset) as usize))
793 }
794 _ => singles.push((idx, offset)),
795 }
796 }
797
798 let mut buf = Vec::new();
799 let mut hits = 0u64;
800
801 let mut group_start = 0;
803 while group_start < extents.len() {
804 let blob_num = position_to_blob(positions[extents[group_start].0], items_per_blob);
805 let mut group_end = group_start + 1;
806 while group_end < extents.len()
807 && position_to_blob(positions[extents[group_end].0], items_per_blob) == blob_num
808 {
809 group_end += 1;
810 }
811 let group = &extents[group_start..group_end];
812 group_start = group_end;
813
814 let Some(blob) = self.data.get(blob_num) else {
815 continue;
816 };
817 let ranges: Vec<(u64, usize)> = group
818 .iter()
819 .map(|&(_, offset, len)| (offset, len))
820 .collect();
821 let total: usize = ranges.iter().map(|&(_, len)| len).sum();
822 buf.resize(total, 0);
823 let missed = blob.try_read_ranges_sync_into(&mut buf, &ranges);
824 let mut missed = missed.into_iter().peekable();
825 let mut local = 0usize;
826 for (range_idx, &(idx, _, len)) in group.iter().enumerate() {
827 let slot = &buf[local..local + len];
828 local += len;
829 if missed.peek() == Some(&range_idx) {
830 missed.next();
831 continue;
832 }
833 if let Some(item) =
834 decode_frame_from_span(slot, len, &self.codec_config, self.compressed)
835 {
836 out[idx] = Some(item);
837 hits += 1;
838 }
839 }
840 }
841
842 let mut frame_buf = Vec::new();
844 for (idx, offset) in singles {
845 if let Some(item) = self.try_read_frame_sync(positions[idx], offset, &mut frame_buf) {
846 out[idx] = Some(item);
847 hits += 1;
848 }
849 }
850 self.metrics.cache_hits.inc_by(hits);
851 self.metrics.items_read.inc_by(hits);
852 resolved
853 }
854}
855
856#[derive(Clone, Copy)]
859struct Miss {
860 position: u64,
861 offset: Option<u64>,
862}
863
864async fn complete<E: Context, V: CodecShared>(
867 reader: &Reader<'_, E, V>,
868 items: Vec<Option<V>>,
869 misses: Vec<Miss>,
870) -> Result<Vec<V>, Error> {
871 if misses.is_empty() {
872 return Ok(items
873 .into_iter()
874 .map(|item| item.expect("complete probe has no misses"))
875 .collect());
876 }
877
878 let fetched = reader.fetch_misses(&misses).await?;
879 let mut fetched = fetched.into_iter();
880 Ok(items
881 .into_iter()
882 .map(|item| item.unwrap_or_else(|| fetched.next().expect("one fetched item per miss")))
883 .collect())
884}
885
886impl<E: Context, V: CodecShared> Reader<'_, E, V> {
887 fn probe_parts(&self, positions: &[u64]) -> (Vec<Option<V>>, Vec<Miss>) {
890 let mut items: Vec<Option<V>> = (0..positions.len()).map(|_| None).collect();
891 let resolved = self.read_many_sync_pass(positions, &mut items);
892 let misses = positions
893 .iter()
894 .zip(&items)
895 .zip(resolved)
896 .filter_map(|((&position, item), offset)| {
897 item.is_none().then_some(Miss { position, offset })
898 })
899 .collect();
900 (items, misses)
901 }
902
903 async fn fetch_misses(&self, misses: &[Miss]) -> Result<Vec<V>, Error> {
907 if misses.is_empty() {
908 return Ok(Vec::new());
909 }
910
911 for miss in misses {
914 self.validate_readable(miss.position)?;
915 }
916
917 let unresolved: Vec<u64> = misses
918 .iter()
919 .filter(|miss| miss.offset.is_none())
920 .map(|miss| miss.position)
921 .collect();
922
923 let fetched = self
926 .offsets
927 .read_many_inner(&unresolved)
928 .await
929 .map_err(|e| match e {
930 Error::ItemOutOfRange(e) | Error::ItemPruned(e) => {
931 Error::Corruption(format!("blob/item should be found, but got: {e}"))
932 }
933 other => other,
934 })?;
935 let mut fetched = fetched.into_iter();
936 let offsets: Vec<u64> = misses
937 .iter()
938 .map(|miss| {
939 miss.offset.unwrap_or_else(|| {
940 fetched
941 .next()
942 .expect("one fetched offset per unresolved miss")
943 })
944 })
945 .collect();
946 let positions: Vec<u64> = misses.iter().map(|miss| miss.position).collect();
947
948 let mut result: Vec<Option<V>> = (0..misses.len()).map(|_| None).collect();
949 self.read_misses(&mut result, None, &positions, &offsets)
950 .await?;
951 self.metrics.cache_misses.inc_by(positions.len() as u64);
952 self.metrics.items_read.inc_by(positions.len() as u64);
953 Ok(result
954 .into_iter()
955 .map(|item| item.expect("read_misses fills every slot"))
956 .collect())
957 }
958}
959
960impl<E: Context, V: CodecShared> super::Contiguous for Reader<'_, E, V> {
961 type Item = V;
962
963 fn bounds(&self) -> Range<u64> {
964 self.bounds.clone()
965 }
966
967 async fn read(&self, position: u64) -> Result<V, Error> {
968 self.metrics.read_calls.inc();
969 self.validate_readable(position)?;
970
971 let cached_offset = self.offsets.try_read_sync(position);
975 if let Some(offset) = cached_offset {
976 let mut buf = Vec::new();
977 if let Some(item) = self.try_read_frame_sync(position, offset, &mut buf) {
978 self.metrics.cache_hits.inc();
979 self.metrics.items_read.inc();
980 return Ok(item);
981 }
982 }
983
984 let _timer = self.metrics.read_timer();
985 let offset = match cached_offset {
986 Some(offset) => offset,
987 None => self.offsets.read(position).await?,
988 };
989 let blob = self
990 .data
991 .get(position_to_blob(position, self.items_per_blob.get()))
992 .expect("position in bounds maps to a retained blob");
993 self.metrics.cache_misses.inc();
994 let item = self.read_at_offset(&blob, offset).await?;
995 self.metrics.items_read.inc();
996 Ok(item)
997 }
998
999 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
1000 if positions.is_empty() {
1001 return Ok(Vec::new());
1002 }
1003 let _timer = self.metrics.read_many_timer();
1004 self.metrics.read_many_calls.inc();
1005 self.validate_read_many(positions)?;
1006 let (items, misses) = self.probe_parts(positions);
1007 complete(self, items, misses).await
1008 }
1009
1010 fn try_read_sync(&self, position: u64) -> Option<V> {
1011 self.validate_readable(position).ok()?;
1012 let offset = self.offsets.try_read_sync(position)?;
1013 let mut buf = Vec::new();
1014 let item = self.try_read_frame_sync(position, offset, &mut buf)?;
1015 self.metrics.cache_hits.inc();
1016 self.metrics.items_read.inc();
1017 Some(item)
1018 }
1019
1020 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
1021 assert!(
1022 positions.is_sorted_by(|a, b| a < b),
1023 "positions must be strictly increasing"
1024 );
1025 let mut items: Vec<Option<V>> = (0..positions.len()).map(|_| None).collect();
1026 self.read_many_sync_pass(positions, &mut items);
1027 items
1028 }
1029
1030 async fn replay(
1031 &self,
1032 start_pos: u64,
1033 buffer: NonZeroUsize,
1034 read_options: ReadOptions,
1035 ) -> Result<impl Stream<Item = Result<(u64, V), Error>> + Send, Error> {
1036 let states = self.replay_states(start_pos, buffer, read_options).await?;
1037
1038 Ok(super::replay_stream_from_states(states))
1039 }
1040}
1041
1042impl<E: Context, V: CodecShared> Inner<E, V> {
1043 #[boxed]
1045 pub(crate) async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
1046 let items_per_blob = cfg.items_per_section.get();
1047 let data_partition = cfg.data_partition();
1048 let data_context = context.child("data");
1049
1050 let offsets = fixed::Inner::<E, u64>::init_cleared(
1054 context.child("offsets"),
1055 fixed::Config {
1056 partition: cfg.offsets_partition(),
1057 items_per_blob: cfg.items_per_section,
1058 page_cache: cfg.page_cache.clone(),
1059 write_buffer: cfg.write_buffer,
1060 replay_buffer: cfg.replay_buffer,
1061 },
1062 || Partition::<E>::remove_all(&data_context, &data_partition),
1063 )
1064 .await?;
1065
1066 let partition = Partition::new(
1067 data_context,
1068 data_partition,
1069 cfg.page_cache,
1070 cfg.write_buffer,
1071 );
1072 let mut pending = partition.open_all().await?;
1073
1074 let floor = offsets.recovery_watermark().max(offsets.pruning_boundary());
1077 let floor_blob = position_to_blob(floor, items_per_blob);
1078
1079 let suspects: Vec<u64> = pending.keys().rev().take(2).copied().collect();
1085 for blob in suspects {
1086 if blob < floor_blob {
1092 continue;
1093 }
1094
1095 let writer = pending.get_mut(&blob).expect("suspect blob is present");
1098 let valid = writer
1099 .recoverable_prefix_len(0, cfg.replay_buffer, ReadOptions::default())
1100 .await?;
1101 let size = writer.size();
1102 if valid == size {
1103 continue;
1104 }
1105
1106 if blob == floor_blob
1111 && floor > blob_first_position(blob, items_per_blob)?
1112 && floor > offsets.pruning_boundary()
1113 && valid <= offsets.read(floor - 1).await?
1114 {
1115 return Err(Error::Corruption(format!(
1116 "blob {blob} no longer backs acknowledged items: well-formed prefix {valid} \
1117 of size {size}"
1118 )));
1119 }
1120 warn!(blob, valid, size, "truncating to last well-formed page");
1121 writer.resize(valid).await?;
1122 writer.sync().await?;
1123 }
1124
1125 let (offsets, bounds) = Self::align(
1127 &partition,
1128 &mut pending,
1129 Box::new(offsets),
1130 items_per_blob,
1131 cfg.replay_buffer,
1132 &cfg.codec_config,
1133 cfg.compression.is_some(),
1134 )
1135 .await?;
1136
1137 let tail_blob = position_to_blob(bounds.end, items_per_blob);
1139 let blobs = Writable::recover(partition, pending, tail_blob).await?;
1140
1141 let metrics = Metrics::new(context);
1146 metrics.update(bounds.end, bounds.start, items_per_blob);
1147
1148 let barrier = Barrier::new(offsets.recovery_watermark());
1151 Ok(Self {
1152 blobs,
1153 offsets,
1154 bounds,
1155 #[cfg(test)]
1156 halt_before_offsets_prune: false,
1157 items_per_blob: cfg.items_per_section,
1158 compression: cfg.compression,
1159 codec_config: cfg.codec_config,
1160 metrics: Arc::new(metrics),
1161 barrier,
1162 })
1163 }
1164
1165 #[commonware_macros::stability(ALPHA)]
1167 pub(crate) async fn init_at_size(
1168 context: E,
1169 cfg: Config<V::Cfg>,
1170 size: u64,
1171 ) -> Result<Self, Error> {
1172 let items_per_blob = cfg.items_per_section.get();
1173 let data_partition = cfg.data_partition();
1174 let data_context = context.child("data");
1175 let offsets_partition = cfg.offsets_partition();
1176 let offsets_context = context.child("offsets");
1177
1178 Partition::select(&offsets_context, &offsets_partition).await?;
1180
1181 let offsets = Box::new(
1185 fixed::Inner::<E, u64>::init_at_size_cleared(
1186 offsets_context,
1187 fixed::Config {
1188 partition: offsets_partition,
1189 items_per_blob: cfg.items_per_section,
1190 page_cache: cfg.page_cache.clone(),
1191 write_buffer: cfg.write_buffer,
1192 replay_buffer: cfg.replay_buffer,
1193 },
1194 size,
1195 || Partition::<E>::remove_all(&data_context, &data_partition),
1196 )
1197 .await?,
1198 );
1199
1200 let partition = Partition::new(
1201 data_context,
1202 data_partition,
1203 cfg.page_cache,
1204 cfg.write_buffer,
1205 );
1206 let blobs = Writable::recover(
1207 partition,
1208 BTreeMap::new(),
1209 position_to_blob(size, items_per_blob),
1210 )
1211 .await?;
1212
1213 let metrics = Metrics::new(context);
1214 metrics.update(size, size, items_per_blob);
1215
1216 Ok(Self {
1217 blobs,
1218 offsets,
1219 bounds: size..size,
1220 #[cfg(test)]
1221 halt_before_offsets_prune: false,
1222 items_per_blob: cfg.items_per_section,
1223 compression: cfg.compression,
1224 codec_config: cfg.codec_config,
1225 metrics: Arc::new(metrics),
1226 barrier: Barrier::new(size),
1227 })
1228 }
1229
1230 #[commonware_macros::stability(ALPHA)]
1232 pub(crate) async fn init_sync(
1233 context: E,
1234 cfg: Config<V::Cfg>,
1235 range: Range<u64>,
1236 ) -> Result<Box<Self>, Error> {
1237 assert!(!range.is_empty(), "range must not be empty");
1238
1239 debug!(
1240 range.start,
1241 range.end,
1242 items_per_blob = cfg.items_per_section.get(),
1243 "initializing contiguous variable journal for sync"
1244 );
1245
1246 let journal = Box::new(Self::init(context.child("journal"), cfg.clone()).await?);
1248
1249 let size = journal.size();
1250
1251 if size == 0 {
1253 if range.start == 0 {
1254 debug!("no existing journal data, returning empty journal");
1255 return Ok(journal);
1256 } else {
1257 debug!(
1258 range.start,
1259 "no existing journal data, resetting to sync range start"
1260 );
1261 return journal.clear_to_size(range.start).await;
1262 }
1263 }
1264
1265 let bounds = journal.bounds.clone();
1267 if bounds.start > range.start {
1268 debug!(
1269 size,
1270 bounds.start,
1271 range.start,
1272 range.end,
1273 "existing journal is incompatible with sync range, resetting to start position"
1274 );
1275 return journal.clear_to_size(range.start).await;
1276 }
1277
1278 let journal = if size > range.end {
1281 debug!(size, range.end, "rewinding journal to sync range end");
1282 journal.rewind(range.end).await?
1283 } else {
1284 journal
1285 };
1286 let size = journal.size();
1287
1288 if size <= range.start {
1290 debug!(
1291 size,
1292 range.start, "existing journal data is stale, resetting to start position"
1293 );
1294 return journal.clear_to_size(range.start).await;
1295 }
1296
1297 if !bounds.is_empty() && bounds.start < range.start {
1299 debug!(
1300 oldest_pos = bounds.start,
1301 range.start, "pruning journal to sync range start"
1302 );
1303 let (journal, _) = journal.prune(range.start).await?;
1304 return Ok(journal);
1305 }
1306
1307 Ok(journal)
1308 }
1309
1310 pub(crate) async fn rewind(mut self: Box<Self>, size: u64) -> Result<Box<Self>, Error> {
1312 match size.cmp(&self.bounds.end) {
1313 std::cmp::Ordering::Greater => return Err(Error::InvalidRewind(size)),
1314 std::cmp::Ordering::Equal => return Ok(self),
1315 std::cmp::Ordering::Less => {}
1316 }
1317
1318 if size < self.bounds.start {
1320 return Err(Error::ItemPruned(size));
1321 }
1322
1323 let discard_blob = position_to_blob(size, self.items_per_blob.get());
1324
1325 let discard_offset = self.offsets.read(size).await?;
1327
1328 self.offsets = self.offsets.rewind(size).await?;
1334
1335 if discard_blob == self.blobs.tail_blob_index() {
1336 self.blobs.rewind_tail(discard_offset).await?;
1337 } else {
1338 self.blobs
1339 .rewind_into_sealed(discard_blob, discard_offset)
1340 .await?;
1341 }
1342
1343 self.bounds.end = size;
1344 self.barrier.truncate(size);
1345 self.metrics.update(
1346 self.bounds.end,
1347 self.bounds.start,
1348 self.items_per_blob.get(),
1349 );
1350
1351 Ok(self)
1352 }
1353
1354 pub(crate) async fn append(&mut self, item: &V) -> Result<u64, Error> {
1356 let _timer = self.metrics.append_timer();
1357 self.metrics.append_calls.inc();
1358 self.append_many_inner(Many::Flat(std::slice::from_ref(item)))
1359 .await
1360 }
1361
1362 pub(crate) async fn append_many<'a>(&'a mut self, items: Many<'a, V>) -> Result<u64, Error> {
1364 let _timer = self.metrics.append_many_timer();
1365 self.metrics.append_many_calls.inc();
1366 self.append_many_inner(items).await
1367 }
1368
1369 async fn append_many_inner<'a>(&'a mut self, items: Many<'a, V>) -> Result<u64, Error> {
1370 self.write_encoded(self.prepare_append(items)?).await
1371 }
1372
1373 pub(crate) fn prepare_append(&self, items: Many<'_, V>) -> Result<PreparedAppend<V>, Error> {
1375 let mut encoded = Vec::new();
1376 let mut item_starts = Vec::with_capacity(items.len());
1377 let mut encode = |item: &V| {
1378 item_starts.push(encoded.len());
1379 encode_frame_into(self.compression, item, &mut encoded)
1380 };
1381 match items {
1382 Many::Flat(items) => {
1383 for item in items {
1384 encode(item)?;
1385 }
1386 }
1387 Many::Nested(nested_items) => {
1388 for items in nested_items {
1389 for item in *items {
1390 encode(item)?;
1391 }
1392 }
1393 }
1394 }
1395 Ok(PreparedAppend {
1396 encoded,
1397 item_starts,
1398 compressed: self.compression.is_some(),
1399 _marker: PhantomData,
1400 })
1401 }
1402
1403 pub(crate) async fn append_prepared(
1405 &mut self,
1406 prepared: PreparedAppend<V>,
1407 ) -> Result<u64, Error> {
1408 let _timer = self.metrics.append_prepared_timer();
1409 self.metrics.append_prepared_calls.inc();
1410 self.write_encoded(prepared).await
1411 }
1412
1413 async fn write_encoded(&mut self, prepared: PreparedAppend<V>) -> Result<u64, Error> {
1415 let PreparedAppend {
1416 encoded,
1417 item_starts,
1418 compressed,
1419 ..
1420 } = prepared;
1421 let items_count = item_starts.len();
1422 if items_count == 0 {
1423 return Err(Error::EmptyAppend);
1424 }
1425 if compressed != self.compression.is_some() {
1426 return Err(Error::InvalidConfiguration(
1427 "prepared append compression setting does not match journal".into(),
1428 ));
1429 }
1430 let encoded = IoBuf::from(encoded);
1431
1432 self.bounds
1435 .end
1436 .checked_add(items_count as u64)
1437 .ok_or(Error::SizeOverflow)?;
1438
1439 let items_per_blob = self.items_per_blob.get();
1440 let mut written = 0;
1441 while written < items_count {
1442 let batch_count = super::batch_count_to_blob_boundary(
1443 self.bounds.end,
1444 items_count - written,
1445 items_per_blob,
1446 );
1447 let batch_start = item_starts[written];
1448 let batch_end = item_starts
1449 .get(written + batch_count)
1450 .copied()
1451 .unwrap_or(encoded.len());
1452
1453 let base_offset = self
1457 .blobs
1458 .tail_writer()
1459 .append_owned(encoded.slice(batch_start..batch_end))
1460 .await?;
1461
1462 let absolute_offsets = item_starts[written..written + batch_count]
1463 .iter()
1464 .map(|&start| {
1465 base_offset
1466 .checked_add((start - batch_start) as u64)
1467 .ok_or(Error::OffsetOverflow)
1468 })
1469 .collect::<Result<Vec<u64>, _>>()?;
1470
1471 let last_offsets_pos = self
1473 .offsets
1474 .append_many(Many::Flat(&absolute_offsets))
1475 .await?;
1476 assert_eq!(last_offsets_pos, self.bounds.end + batch_count as u64 - 1);
1477
1478 self.bounds.end += batch_count as u64;
1479 written += batch_count;
1480
1481 if self.bounds.end.is_multiple_of(items_per_blob) {
1483 self.blobs.seal_tail().await?;
1484 }
1485 }
1486
1487 self.metrics.update(
1488 self.bounds.end,
1489 self.bounds.start,
1490 self.items_per_blob.get(),
1491 );
1492 Ok(self.bounds.end - 1)
1493 }
1494
1495 pub(crate) async fn snapshot(&mut self) -> Result<Reader<'static, E, V>, Error> {
1497 Ok(Reader {
1498 data: self.blobs.snapshot().await?,
1499 bounds: self.bounds.clone(),
1500 offsets: self.offsets.snapshot().await?,
1501 items_per_blob: self.items_per_blob,
1502 codec_config: self.codec_config.clone(),
1503 compressed: self.compression.is_some(),
1504 metrics: self.metrics.clone(),
1505 })
1506 }
1507
1508 fn reader(&self) -> Reader<'_, E, V> {
1510 Reader {
1511 data: self.blobs.reader(),
1512 bounds: self.bounds.clone(),
1513 offsets: self.offsets.reader(),
1514 items_per_blob: self.items_per_blob,
1515 codec_config: self.codec_config.clone(),
1516 compressed: self.compression.is_some(),
1517 metrics: self.metrics.clone(),
1518 }
1519 }
1520
1521 pub const fn size(&self) -> u64 {
1524 self.bounds.end
1525 }
1526
1527 pub(crate) async fn prune(
1529 mut self: Box<Self>,
1530 min_position: u64,
1531 ) -> Result<(Box<Self>, bool), Error> {
1532 let items_per_blob = self.items_per_blob.get();
1533
1534 let target_blob = position_to_blob(min_position, items_per_blob);
1537 let tail_blob = position_to_blob(self.bounds.end, items_per_blob);
1538 let min_blob = target_blob.min(tail_blob);
1539
1540 if min_blob <= self.blobs.oldest_blob_index() {
1541 return Ok((self, false));
1542 }
1543
1544 let new_boundary = blob_first_position(min_blob, items_per_blob)?;
1545
1546 let data_sync = self.blobs.start_sync().await;
1558 data_sync.await?;
1559 self.offsets = self.offsets.commit().await?;
1560 self.barrier.mark_durable(self.bounds.end);
1561
1562 self.blobs.prune(min_blob).await?;
1563 self.bounds.start = new_boundary;
1564
1565 #[cfg(test)]
1566 if self.halt_before_offsets_prune {
1567 std::future::pending::<()>().await;
1568 }
1569
1570 let (offsets, _) = self.offsets.prune(new_boundary).await?;
1573 self.offsets = offsets;
1574 self.metrics.update(
1575 self.bounds.end,
1576 self.bounds.start,
1577 self.items_per_blob.get(),
1578 );
1579
1580 Ok((self, true))
1581 }
1582
1583 pub(crate) async fn start_sync(mut self: Box<Self>) -> Result<(Box<Self>, Handle<()>), Error> {
1585 self.metrics.start_sync_calls.inc();
1586 let data = self.blobs.start_sync().await;
1587 let (offsets_journal, offsets) = self.offsets.start_data_sync().await;
1588
1589 let size = self.barrier.boundary();
1590 let (offsets_journal, watermark_handle) =
1591 offsets_journal.start_watermark_sync(size).await?;
1592 self.offsets = offsets_journal;
1593
1594 let journal_completion: SyncCompletion =
1595 async move { try_join(data, offsets).await.map(|_| ()) }
1596 .boxed()
1597 .shared();
1598 self.barrier
1599 .record(self.bounds.end, journal_completion.clone());
1600 let handle = Handle::from_future(async move {
1601 journal_completion.await?;
1602 watermark_handle.await
1603 });
1604 Ok((self, handle))
1605 }
1606
1607 pub(crate) async fn commit(mut self: Box<Self>) -> Result<Box<Self>, Error> {
1609 let _timer = self.metrics.commit_timer();
1610 self.metrics.commit_calls.inc();
1611 let handle = self.blobs.start_sync().await;
1612 handle.await?;
1613 Ok(self)
1614 }
1615
1616 pub(crate) async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
1618 let _timer = self.metrics.sync_timer();
1619 self.metrics.sync_calls.inc();
1620 let size = self.bounds.end;
1621 let handle = self.blobs.start_sync().await;
1622 handle.await?;
1623 self.offsets = self.offsets.sync().await?;
1624 self.barrier.mark_durable(size);
1625 Ok(self)
1626 }
1627
1628 pub(crate) async fn destroy(self) -> Result<(), Error> {
1630 self.blobs.destroy().await?;
1631 self.offsets.destroy().await
1632 }
1633
1634 #[commonware_macros::stability(ALPHA)]
1641 pub(crate) async fn clear_to_size(
1642 mut self: Box<Self>,
1643 new_size: u64,
1644 ) -> Result<Box<Self>, Error> {
1645 self.offsets = self.offsets.stage_clear_intent(new_size).await?;
1648 self.blobs
1649 .clear(position_to_blob(new_size, self.items_per_blob.get()))
1650 .await?;
1651 self.offsets = self.offsets.clear_to_size(new_size).await?;
1652
1653 self.bounds = new_size..new_size;
1654 self.barrier = Barrier::new(new_size);
1655 self.metrics.update(
1656 self.bounds.end,
1657 self.bounds.start,
1658 self.items_per_blob.get(),
1659 );
1660 Ok(self)
1661 }
1662
1663 async fn scan_blob(
1665 writer: &mut Writer<E::Blob>,
1666 buffer: NonZeroUsize,
1667 codec_config: &V::Cfg,
1668 compressed: bool,
1669 ) -> Result<BlobScan, Error> {
1670 let replay = writer.replay(buffer, ReadOptions::default()).await?;
1671 let mut scanner = FrameScanner::<E::Blob, V>::new(replay, codec_config, compressed);
1672 let mut items = 0u64;
1673 loop {
1674 match scanner.next().await? {
1675 Frame::Item { .. } => items += 1,
1676 Frame::End { valid_size, torn } => {
1677 return Ok(BlobScan {
1678 items,
1679 valid_size,
1680 torn,
1681 });
1682 }
1683 }
1684 }
1685 }
1686
1687 async fn align(
1696 partition: &Partition<E>,
1697 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1698 mut offsets: Box<fixed::Inner<E, u64>>,
1699 items_per_blob: u64,
1700 buffer: NonZeroUsize,
1701 codec_config: &V::Cfg,
1702 compressed: bool,
1703 ) -> Result<(Box<fixed::Inner<E, u64>>, Range<u64>), Error> {
1704 let scanned: Vec<u64> = pending.keys().rev().copied().collect();
1707 let mut items_in_newest = 0;
1708 let mut newest_blob = None;
1709 for &blob in &scanned {
1710 let writer = pending.get_mut(&blob).expect("blob came from pending");
1711 let scan = Self::scan_blob(writer, buffer, codec_config, compressed).await?;
1712 if scan.items > items_per_blob {
1713 return Err(Error::Corruption(format!(
1714 "blob {blob} has too many items: expected at most {items_per_blob}, got {}",
1715 scan.items
1716 )));
1717 }
1718 if scan.torn {
1719 warn!(
1720 blob,
1721 new_size = scan.valid_size,
1722 "crash repair: truncating trailing bytes"
1723 );
1724 writer.resize(scan.valid_size).await?;
1725 writer.sync().await?;
1726 }
1727 if scan.items > 0 {
1728 items_in_newest = scan.items;
1729 newest_blob = Some(blob);
1730 break;
1731 }
1732 }
1733
1734 let tail_blob = match newest_blob {
1738 Some(blob) if items_in_newest == items_per_blob => blob.saturating_add(1),
1739 Some(blob) => blob,
1740 None => pending.keys().next().copied().unwrap_or(0),
1741 };
1742 for &blob in &scanned {
1743 if blob <= tail_blob {
1744 break;
1745 }
1746 warn!(blob, "crash repair: removing empty trailing data blob");
1747 pending.remove(&blob);
1748 partition.remove(blob).await?;
1749 }
1750
1751 let Some(newest_blob) = newest_blob else {
1752 return Self::align_empty(partition, pending, offsets, items_per_blob).await;
1753 };
1754
1755 let oldest_blob = *pending.keys().next().expect("pending is non-empty");
1759 let data_oldest_pos = blob_first_position(oldest_blob, items_per_blob)?;
1760 {
1761 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1762
1763 if offsets_bounds.end < data_oldest_pos {
1766 return Err(Error::Corruption(format!(
1767 "offsets journal size {} is behind data oldest position {data_oldest_pos}",
1768 offsets_bounds.end
1769 )));
1770 }
1771 let offsets_start_blob = position_to_blob(offsets_bounds.start, items_per_blob);
1772 match offsets_start_blob.cmp(&oldest_blob) {
1773 std::cmp::Ordering::Less => {
1774 warn!("crash repair: pruning offsets journal to {data_oldest_pos}");
1775 let (pruned, _) = offsets.prune(data_oldest_pos).await?;
1776 offsets = pruned;
1777 }
1778 std::cmp::Ordering::Equal => {}
1779 std::cmp::Ordering::Greater => {
1780 return Err(Error::Corruption(format!(
1783 "offsets start blob {offsets_start_blob} ahead of \
1784 oldest data blob {oldest_blob}"
1785 )));
1786 }
1787 }
1788 }
1789
1790 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1792
1793 let retained_data_end_bound = blob_first_position(newest_blob, items_per_blob)?
1796 .max(offsets_bounds.start)
1797 .checked_add(items_in_newest)
1798 .ok_or(Error::OffsetOverflow)?;
1799 let data_sync_start =
1800 Self::recovery_anchor(&offsets, &offsets_bounds, retained_data_end_bound)?;
1801
1802 let data_size;
1804 (offsets, data_size) = Self::rebuild_offsets_from_anchor(
1805 partition,
1806 pending,
1807 offsets,
1808 items_per_blob,
1809 data_sync_start,
1810 buffer,
1811 codec_config,
1812 compressed,
1813 )
1814 .await?;
1815
1816 let pruning_boundary = {
1819 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1820 if offsets_bounds.end != data_size {
1821 return Err(Error::Corruption(format!(
1822 "recovered offsets end {} does not match data size {data_size}",
1823 offsets_bounds.end
1824 )));
1825 }
1826
1827 if !offsets_bounds.is_empty()
1832 && position_to_blob(offsets_bounds.start, items_per_blob) != oldest_blob
1833 {
1834 return Err(Error::Corruption(format!(
1835 "recovered offsets and data start in different blobs: {} != {oldest_blob}",
1836 position_to_blob(offsets_bounds.start, items_per_blob)
1837 )));
1838 }
1839
1840 offsets_bounds.start
1842 };
1843
1844 Self::sync_data_range(pending, data_sync_start, data_size, items_per_blob).await?;
1847 let offsets = offsets.sync().await?;
1848 Ok((offsets, pruning_boundary..data_size))
1849 }
1850
1851 async fn align_empty(
1857 partition: &Partition<E>,
1858 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1859 mut offsets: Box<fixed::Inner<E, u64>>,
1860 items_per_blob: u64,
1861 ) -> Result<(Box<fixed::Inner<E, u64>>, Range<u64>), Error> {
1862 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1863
1864 let Some(&blob) = pending.keys().next() else {
1865 let size = offsets_bounds.end;
1869 if !offsets_bounds.is_empty() {
1870 warn!("crash repair: clearing offsets to {size} (prune-all crash)");
1871 offsets = offsets.clear_to_size(size).await?;
1872 }
1873 return Ok((offsets, size..size));
1874 };
1875
1876 let blob_start = blob_first_position(blob, items_per_blob)?;
1879 let target = blob_start.max(offsets_bounds.start);
1880
1881 let aligned = position_to_blob(target, items_per_blob) == blob;
1883 if aligned && offsets_bounds == (target..target) {
1884 return Ok((offsets, target..target));
1885 }
1886
1887 if !aligned {
1890 warn!(blob, "crash repair: removing empty data blob");
1891 pending.remove(&blob);
1892 partition.remove(blob).await?;
1893 }
1894 warn!("crash repair: clearing offsets to {target} (empty data)");
1895 let offsets = offsets.clear_to_size(target).await?;
1896 Ok((offsets, target..target))
1897 }
1898
1899 fn recovery_anchor(
1902 offsets: &fixed::Inner<E, u64>,
1903 offsets_bounds: &Range<u64>,
1904 retained_data_end_bound: u64,
1905 ) -> Result<u64, Error> {
1906 let recovery_watermark = offsets.recovery_watermark();
1907 if recovery_watermark > offsets_bounds.end {
1908 return Err(Error::Corruption(format!(
1911 "offsets recovery watermark {recovery_watermark} exceeds offsets size {}",
1912 offsets_bounds.end
1913 )));
1914 }
1915 if recovery_watermark < offsets_bounds.start {
1916 warn!(
1917 recovery_watermark,
1918 start = offsets_bounds.start,
1919 end = offsets_bounds.end,
1920 retained_data_end_bound,
1921 "crash repair: offsets recovery watermark is unusable, rebuilding from offsets start"
1922 );
1923 return Ok(offsets_bounds.start);
1924 }
1925 if recovery_watermark > retained_data_end_bound {
1926 return Err(Error::Corruption(format!(
1927 "offsets recovery watermark {recovery_watermark} exceeds retained data end \
1928 {retained_data_end_bound} (offsets bounds {}..{})",
1929 offsets_bounds.start, offsets_bounds.end
1930 )));
1931 }
1932 Ok(recovery_watermark)
1933 }
1934
1935 async fn sync_data_range(
1937 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1938 start_position: u64,
1939 end_position: u64,
1940 items_per_blob: u64,
1941 ) -> Result<(), Error> {
1942 if start_position >= end_position {
1943 return Ok(());
1944 }
1945
1946 let start_blob = position_to_blob(start_position, items_per_blob);
1947 let end_blob = position_to_blob(end_position - 1, items_per_blob);
1948 futures::future::try_join_all(
1949 pending
1950 .range_mut(start_blob..=end_blob)
1951 .map(|(_, writer)| writer.sync()),
1952 )
1953 .await?;
1954 Ok(())
1955 }
1956
1957 #[allow(clippy::too_many_arguments)]
1963 async fn rebuild_offsets_from_anchor(
1964 partition: &Partition<E>,
1965 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
1966 mut offsets: Box<fixed::Inner<E, u64>>,
1967 items_per_blob: u64,
1968 anchor: u64,
1969 buffer: NonZeroUsize,
1970 codec_config: &V::Cfg,
1971 compressed: bool,
1972 ) -> Result<(Box<fixed::Inner<E, u64>>, u64), Error> {
1973 assert!(
1974 !pending.is_empty(),
1975 "rebuild_offsets called with no data blobs"
1976 );
1977
1978 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
1979 let data_too_short = || {
1980 if anchor == offsets_bounds.start {
1981 Error::Corruption(format!(
1982 "data blobs shorter than pruning boundary {}",
1983 offsets_bounds.start
1984 ))
1985 } else {
1986 Error::Corruption(format!(
1987 "data blobs shorter than offsets recovery watermark {anchor}"
1988 ))
1989 }
1990 };
1991 if anchor < offsets_bounds.start || anchor > offsets_bounds.end {
1992 return Err(data_too_short());
1993 }
1994
1995 if offsets_bounds.end > anchor {
1996 offsets = offsets.rewind(anchor).await?;
1997 }
1998
1999 let start_blob = position_to_blob(anchor, items_per_blob);
2000 let first_position = offsets_bounds
2001 .start
2002 .max(blob_first_position(start_blob, items_per_blob)?);
2003
2004 let mut skip = anchor - first_position;
2007 let mut size = anchor;
2008 let mut blob = start_blob;
2009 loop {
2010 let Some(writer) = pending.get_mut(&blob) else {
2011 if skip > 0 {
2012 return Err(data_too_short());
2014 }
2015 if pending.keys().next_back().is_some_and(|&n| n > blob) {
2018 warn!(
2019 blob,
2020 size, "crash repair: truncating data after missing blob"
2021 );
2022 Self::remove_blobs_after(partition, pending, blob).await?;
2023 }
2024 return Ok((offsets, size));
2025 };
2026
2027 let replay = writer.replay(buffer, ReadOptions::default()).await?;
2028 let mut scanner = FrameScanner::<E::Blob, V>::new(replay, codec_config, compressed);
2029 let blob_end_pos = super::blob_end_position(blob, items_per_blob, u64::MAX);
2030
2031 let end = loop {
2032 if size == blob_end_pos {
2033 match scanner.next().await? {
2036 Frame::Item { .. } => {
2037 return Err(Error::Corruption(format!(
2038 "blob {blob} over capacity at logical position {size}"
2039 )));
2040 }
2041 Frame::End {
2042 valid_size,
2043 torn: true,
2044 } => break Some((valid_size, true)),
2045 Frame::End { .. } => break None,
2046 }
2047 }
2048 match scanner.next().await? {
2049 Frame::Item { offset } => {
2050 if skip > 0 {
2051 skip -= 1;
2052 } else {
2053 offsets.append(&offset).await?;
2054 size += 1;
2055 }
2056 }
2057 Frame::End { valid_size, torn } => break Some((valid_size, torn)),
2058 }
2059 };
2060
2061 if let Some((valid_size, torn)) = end {
2062 if skip > 0 {
2064 return Err(data_too_short());
2066 }
2067 if torn {
2068 warn!(
2069 blob,
2070 new_size = valid_size,
2071 "crash repair: truncating trailing bytes"
2072 );
2073 writer.resize(valid_size).await?;
2074 writer.sync().await?;
2075 }
2076 if pending.keys().next_back().is_some_and(|&n| n > blob) {
2079 warn!(blob, size, "crash repair: truncating data after short blob");
2080 Self::remove_blobs_after(partition, pending, blob).await?;
2081 }
2082 return Ok((offsets, size));
2083 }
2084
2085 blob = blob.checked_add(1).ok_or(Error::OffsetOverflow)?;
2086 }
2087 }
2088
2089 async fn remove_blobs_after(
2091 partition: &Partition<E>,
2092 pending: &mut BTreeMap<u64, Writer<E::Blob>>,
2093 blob: u64,
2094 ) -> Result<(), Error> {
2095 while let Some((&newest, _)) = pending.last_key_value() {
2096 if newest <= blob {
2097 break;
2098 }
2099 drop(pending.remove(&newest));
2100 partition.remove(newest).await?;
2101 }
2102 Ok(())
2103 }
2104}
2105
2106pub struct Journal<E: Context, V: Codec>(Box<Inner<E, V>>);
2154
2155impl<E: Context, V: CodecShared> std::fmt::Debug for Journal<E, V> {
2156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2157 f.debug_struct("Journal")
2158 .field("bounds", &super::Contiguous::bounds(self))
2159 .finish_non_exhaustive()
2160 }
2161}
2162
2163impl<E: Context, V: CodecShared> Journal<E, V> {
2164 pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
2171 Ok(Self(Box::new(Inner::init(context, cfg).await?)))
2172 }
2173
2174 #[commonware_macros::stability(ALPHA)]
2183 pub async fn init_at_size(context: E, cfg: Config<V::Cfg>, size: u64) -> Result<Self, Error> {
2184 Ok(Self(Box::new(
2185 Inner::init_at_size(context, cfg, size).await?,
2186 )))
2187 }
2188
2189 #[commonware_macros::stability(ALPHA)]
2213 pub(crate) async fn init_sync(
2214 context: E,
2215 cfg: Config<V::Cfg>,
2216 range: core::ops::Range<u64>,
2217 ) -> Result<Self, Error> {
2218 Ok(Self(Inner::init_sync(context, cfg, range).await?))
2219 }
2220
2221 #[commonware_macros::stability(ALPHA)]
2223 pub(crate) async fn clear_to_size(mut self, new_size: u64) -> Result<Self, Error> {
2224 self.0 = self.0.clear_to_size(new_size).await?;
2225 Ok(self)
2226 }
2227
2228 pub async fn rewind(mut self, size: u64) -> Result<Self, Error> {
2243 self.0 = self.0.rewind(size).await?;
2244 Ok(self)
2245 }
2246
2247 pub async fn append(mut self, item: &V) -> Result<(Self, u64), Error> {
2257 let position = self.0.append(item).await?;
2258 Ok((self, position))
2259 }
2260
2261 pub async fn append_many(mut self, items: Many<'_, V>) -> Result<(Self, u64), Error> {
2265 let position = self.0.append_many(items).await?;
2266 Ok((self, position))
2267 }
2268
2269 pub fn prepare_append(&self, items: Many<'_, V>) -> Result<PreparedAppend<V>, Error> {
2274 self.0.prepare_append(items)
2275 }
2276
2277 pub async fn append_prepared(
2284 mut self,
2285 prepared: PreparedAppend<V>,
2286 ) -> Result<(Self, u64), Error> {
2287 let position = self.0.append_prepared(prepared).await?;
2288 Ok((self, position))
2289 }
2290
2291 pub async fn snapshot(mut self) -> Result<(Self, Reader<'static, E, V>), Error> {
2297 let reader = self.0.snapshot().await?;
2298 Ok((self, reader))
2299 }
2300
2301 pub fn size(&self) -> u64 {
2304 self.0.size()
2305 }
2306
2307 pub async fn prune(mut self, min_position: u64) -> Result<(Self, bool), Error> {
2315 let (inner, pruned) = self.0.prune(min_position).await?;
2316 self.0 = inner;
2317 Ok((self, pruned))
2318 }
2319
2320 pub async fn commit(mut self) -> Result<Self, Error> {
2324 self.0 = self.0.commit().await?;
2325 Ok(self)
2326 }
2327
2328 pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
2341 let (inner, handle) = self.0.start_sync().await?;
2342 self.0 = inner;
2343 Ok((self, handle))
2344 }
2345
2346 pub async fn sync(mut self) -> Result<Self, Error> {
2348 self.0 = self.0.sync().await?;
2349 Ok(self)
2350 }
2351
2352 pub async fn destroy(self) -> Result<(), Error> {
2362 self.0.destroy().await
2363 }
2364}
2365
2366impl<E: Context, V: CodecShared> Contiguous for Inner<E, V> {
2367 type Item = V;
2368
2369 fn bounds(&self) -> Range<u64> {
2370 self.bounds.clone()
2371 }
2372
2373 async fn read(&self, position: u64) -> Result<V, Error> {
2374 self.reader().read(position).await
2375 }
2376
2377 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
2378 self.reader().read_many(positions).await
2379 }
2380
2381 fn try_read_sync(&self, position: u64) -> Option<V> {
2382 self.reader().try_read_sync(position)
2383 }
2384
2385 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
2386 self.reader().try_read_many_sync(positions)
2387 }
2388
2389 async fn replay(
2390 &self,
2391 start_pos: u64,
2392 buffer: NonZeroUsize,
2393 read_options: ReadOptions,
2394 ) -> Result<impl Stream<Item = Result<(u64, V), Error>> + Send, Error> {
2395 let reader = self.reader();
2396 let states = reader
2397 .replay_states(start_pos, buffer, read_options)
2398 .await?;
2399
2400 Ok(super::replay_stream_from_states(states))
2401 }
2402}
2403
2404impl<E: Context, V: CodecShared> Contiguous for Journal<E, V> {
2405 type Item = V;
2406
2407 fn bounds(&self) -> Range<u64> {
2408 Contiguous::bounds(&*self.0)
2409 }
2410
2411 async fn read(&self, position: u64) -> Result<V, Error> {
2412 Contiguous::read(&*self.0, position).await
2413 }
2414
2415 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
2416 Contiguous::read_many(&*self.0, positions).await
2417 }
2418
2419 fn try_read_sync(&self, position: u64) -> Option<V> {
2420 Contiguous::try_read_sync(&*self.0, position)
2421 }
2422
2423 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
2424 Contiguous::try_read_many_sync(&*self.0, positions)
2425 }
2426
2427 async fn replay(
2428 &self,
2429 start_pos: u64,
2430 buffer: NonZeroUsize,
2431 read_options: ReadOptions,
2432 ) -> Result<impl Stream<Item = Result<(u64, V), Error>> + Send, Error> {
2433 Contiguous::replay(&*self.0, start_pos, buffer, read_options).await
2434 }
2435}
2436
2437impl<E: Context, V: CodecShared> Mutable for Journal<E, V> {
2438 async fn append(self, item: &Self::Item) -> Result<(Self, u64), Error> {
2439 Self::append(self, item).await
2440 }
2441
2442 async fn append_many(self, items: Many<'_, Self::Item>) -> Result<(Self, u64), Error> {
2443 Self::append_many(self, items).await
2444 }
2445
2446 async fn prune(self, min_position: u64) -> Result<(Self, bool), Error> {
2447 Self::prune(self, min_position).await
2448 }
2449
2450 async fn rewind(self, size: u64) -> Result<Self, Error> {
2451 Self::rewind(self, size).await
2452 }
2453
2454 async fn start_sync(self) -> Result<(Self, Handle<()>), Error> {
2455 Self::start_sync(self).await
2456 }
2457
2458 async fn commit(self) -> Result<Self, Error> {
2459 Self::commit(self).await
2460 }
2461
2462 async fn sync(self) -> Result<Self, Error> {
2463 Self::sync(self).await
2464 }
2465
2466 async fn destroy(self) -> Result<(), Error> {
2467 Self::destroy(self).await
2468 }
2469}
2470
2471#[commonware_macros::stability(ALPHA)]
2472impl<E: Context, V: CodecShared> authenticated::Backing<E> for Journal<E, V> {
2473 type Config = Config<V::Cfg>;
2474
2475 async fn init(context: E, cfg: Self::Config) -> Result<Self, Error> {
2476 Self::init(context, cfg).await
2477 }
2478}
2479
2480#[cfg(test)]
2481impl<E: Context, V: CodecShared> Journal<E, V> {
2482 pub(crate) async fn test_prune_data(&mut self, min_blob: u64) -> Result<bool, Error> {
2484 let min_blob = min_blob.min(self.0.blobs.tail_blob_index());
2485 if min_blob <= self.0.blobs.oldest_blob_index() {
2486 return Ok(false);
2487 }
2488 self.0.blobs.prune(min_blob).await?;
2489 Ok(true)
2490 }
2491
2492 pub(crate) async fn test_prune_offsets(mut self, position: u64) -> Result<(Self, bool), Error> {
2494 let (offsets, pruned) = self.0.offsets.prune(position).await?;
2495 self.0.offsets = offsets;
2496 Ok((self, pruned))
2497 }
2498
2499 pub(crate) async fn test_rewind_offsets(mut self, position: u64) -> Result<Self, Error> {
2501 self.0.offsets = self.0.offsets.rewind(position).await?;
2502 Ok(self)
2503 }
2504
2505 pub(crate) async fn test_set_offsets_recovery_watermark(
2507 mut self,
2508 watermark: u64,
2509 ) -> Result<Self, Error> {
2510 self.0.offsets = self
2511 .0
2512 .offsets
2513 .test_set_recovery_watermark(watermark)
2514 .await?;
2515 Ok(self)
2516 }
2517
2518 pub(crate) fn test_offsets_size(&self) -> u64 {
2520 self.0.offsets.size()
2521 }
2522
2523 pub(crate) async fn test_rewind_data_to_position(
2525 &mut self,
2526 position: u64,
2527 ) -> Result<(), Error> {
2528 let offset = self.0.offsets.read(position).await?;
2529 let blob = position_to_blob(position, self.0.items_per_blob.get());
2530 if blob == self.0.blobs.tail_blob_index() {
2531 self.0.blobs.rewind_tail(offset).await
2532 } else {
2533 self.0.blobs.rewind_into_sealed(blob, offset).await
2534 }
2535 }
2536
2537 pub(crate) async fn test_append_data(
2540 &mut self,
2541 blob: u64,
2542 item: V,
2543 ) -> Result<(u64, u32), Error> {
2544 let mut encoded = Vec::new();
2545 encode_frame_into(self.0.compression, &item, &mut encoded)?;
2546 let item_len = encoded.len() as u32;
2547
2548 let tail_blob = self.0.blobs.tail_blob_index();
2549 if blob == tail_blob {
2550 let writer = self.0.blobs.tail_writer();
2551 let offset = writer.size();
2552 writer.append(&encoded).await?;
2553 return Ok((offset, item_len));
2554 }
2555 assert!(blob > tail_blob, "cannot append to a sealed blob");
2556 let mut writer = self.0.blobs.open_blob(blob).await?;
2557 let offset = writer.size();
2558 writer.append(&encoded).await?;
2559 writer.sync().await?;
2560 Ok((offset, item_len))
2561 }
2562
2563 pub(crate) async fn test_sync_data_blob(&mut self, blob: u64) -> Result<(), Error> {
2565 self.0.blobs.sync_blob(blob).await
2566 }
2567}
2568
2569#[cfg(test)]
2570mod tests {
2571 use super::*;
2572 use crate::journal::contiguous::tests::run_contiguous_tests;
2573 use commonware_macros::test_traced;
2574 use commonware_runtime::{
2575 BufferPooler, Metrics as _, ReadOptions, Runner, Spawner as _, Storage, Supervisor as _,
2576 WriteOptions,
2577 buffer::paged::{CacheRef, Writer, corrupt_page},
2578 deterministic,
2579 mocks::{
2580 DelayedSyncContext, PendingSyncs, RecordingContext, drive_pending_syncs,
2581 fail_pending_syncs, next_pending_sync, release_pending_syncs,
2582 },
2583 };
2584 use commonware_utils::{NZU16, NZU64, NZUsize, probability, sequence::FixedBytes};
2585 use futures::StreamExt as _;
2586 use std::num::NonZeroU16;
2587
2588 const PAGE_SIZE: NonZeroU16 = NZU16!(101);
2590 const PAGE_CACHE_SIZE: usize = 2;
2591 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(1024);
2593 const SMALL_PAGE_SIZE: NonZeroU16 = NZU16!(512);
2594
2595 #[test_traced]
2596 fn test_replay_and_writable_tip_request_dont_cache() {
2597 let executor = deterministic::Runner::default();
2598 executor.start(|context| async move {
2599 let (context, recordings) = RecordingContext::new(context);
2600 let page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(8));
2601 let cfg = Config {
2602 partition: "variable-replay-read-options".into(),
2603 items_per_section: NZU64!(20),
2604 compression: None,
2605 codec_config: (),
2606 page_cache: page_cache.clone(),
2607 write_buffer: NZUsize!(1024),
2608 replay_buffer: NZUsize!(1024),
2609 };
2610 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg)
2611 .await
2612 .unwrap();
2613
2614 for item in 0..39 {
2615 (journal, _) = journal.append(&item).await.unwrap();
2616 }
2617 journal = journal.sync().await.unwrap();
2618
2619 page_cache.clear();
2621 {
2622 let stream = journal
2623 .replay(0, NZUsize!(32), ReadOptions::DONT_CACHE)
2624 .await
2625 .unwrap();
2626 recordings.clear();
2627 futures::pin_mut!(stream);
2628 assert_eq!(stream.next().await.unwrap().unwrap(), (0, 0));
2629
2630 let reads = recordings.snapshot().reads;
2631 assert!(!reads.is_empty());
2632 assert!(
2633 reads
2634 .iter()
2635 .all(|options| *options == ReadOptions::DONT_CACHE)
2636 );
2637 }
2638
2639 page_cache.clear();
2641 {
2642 let stream = journal
2643 .replay(20, NZUsize!(32), ReadOptions::DONT_CACHE)
2644 .await
2645 .unwrap();
2646 recordings.clear();
2647 futures::pin_mut!(stream);
2648 assert_eq!(stream.next().await.unwrap().unwrap(), (20, 20));
2649
2650 let reads = recordings.snapshot().reads;
2651 assert!(!reads.is_empty());
2652 assert!(
2653 reads
2654 .iter()
2655 .all(|options| *options == ReadOptions::DONT_CACHE)
2656 );
2657 }
2658
2659 journal.destroy().await.unwrap();
2660 });
2661 }
2662
2663 #[test]
2664 fn test_start_sync_keeps_predecessor_sync() {
2665 let executor = deterministic::Runner::default();
2666 executor.start(|context| async move {
2667 let cfg = Config {
2668 partition: "variable-start-sync-predecessor".into(),
2669 items_per_section: NZU64!(3),
2670 compression: None,
2671 codec_config: (),
2672 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2)),
2673 write_buffer: NZUsize!(2048),
2674 replay_buffer: NZUsize!(2048),
2675 };
2676 let mut journal = Box::new(Inner::<_, u64>::init(context, cfg).await.unwrap());
2677
2678 journal
2679 .append_many(Many::Flat(&[1, 2, 3, 4]))
2680 .await
2681 .unwrap();
2682 assert!(journal.blobs.has_tail_predecessor_sync());
2683
2684 let (journal, handle) = journal.start_sync().await.unwrap();
2686 assert!(journal.blobs.has_tail_predecessor_sync());
2687 handle.await.unwrap();
2688 assert!(journal.blobs.has_tail_predecessor_sync());
2689
2690 journal.destroy().await.unwrap();
2691 });
2692 }
2693
2694 #[test_traced]
2695 fn test_start_sync_advances_offsets_watermark_lagged() {
2696 let executor = deterministic::Runner::default();
2697 executor.start(|context| async move {
2698 let pending = PendingSyncs::default();
2699 let cfg = Config {
2700 partition: "variable-watermark-lagged".into(),
2701 items_per_section: NZU64!(100),
2702 compression: None,
2703 codec_config: (),
2704 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2)),
2705 write_buffer: NZUsize!(2048),
2706 replay_buffer: NZUsize!(2048),
2707 };
2708 let make = |pending: PendingSyncs| {
2709 Inner::<_, u64>::init(
2710 DelayedSyncContext {
2711 inner: context.child("journal"),
2712 pending,
2713 },
2714 cfg.clone(),
2715 )
2716 };
2717 let mut journal = Box::new(make(pending.clone()).await.unwrap());
2718
2719 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2721 let (mut journal, h1) = journal.start_sync().await.unwrap();
2722 assert_eq!(journal.offsets.recovery_watermark(), 0);
2723
2724 release_pending_syncs(&pending);
2725 drive_pending_syncs(&pending, h1).await.unwrap();
2726
2727 journal.append(&4).await.unwrap();
2730 let (journal, h2) = journal.start_sync().await.unwrap();
2731 assert_eq!(journal.offsets.recovery_watermark(), 3);
2732 drive_pending_syncs(&pending, h2).await.unwrap();
2733
2734 pending.unblock();
2737 drop(journal);
2738 let journal = make(pending.clone()).await.unwrap();
2739 assert_eq!(journal.offsets.recovery_watermark(), 4);
2740 assert_eq!(journal.bounds(), 0..4);
2741 for i in 0..4u64 {
2742 assert_eq!(journal.read(i).await.unwrap(), i + 1);
2743 }
2744 journal.destroy().await.unwrap();
2745 });
2746 }
2747
2748 #[test_traced]
2749 fn test_start_sync_watermark_requires_joint_proof() {
2750 let executor = deterministic::Runner::default();
2751 executor.start(|context| async move {
2752 let pending = PendingSyncs::default();
2753 let cfg = Config {
2754 partition: "variable-watermark-joint".into(),
2755 items_per_section: NZU64!(100),
2756 compression: None,
2757 codec_config: (),
2758 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2)),
2759 write_buffer: NZUsize!(2048),
2760 replay_buffer: NZUsize!(2048),
2761 };
2762 let mut journal = Box::new(
2763 Inner::<_, u64>::init(
2764 DelayedSyncContext {
2765 inner: context.child("journal"),
2766 pending: pending.clone(),
2767 },
2768 cfg,
2769 )
2770 .await
2771 .unwrap(),
2772 );
2773
2774 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2777 let (journal, h1) = journal.start_sync().await.unwrap();
2778 let data = next_pending_sync(&pending);
2779 release_pending_syncs(&pending);
2780 data.release
2781 .send(Err(commonware_runtime::Error::Io(
2782 std::io::Error::other("injected sync failure").into(),
2783 )))
2784 .unwrap();
2785 assert!(h1.await.is_err());
2786
2787 let (journal, h2) = journal.start_sync().await.unwrap();
2788 assert_eq!(journal.offsets.recovery_watermark(), 0);
2789 assert!(h2.await.is_err());
2790 });
2791 }
2792
2793 #[test]
2794 fn test_rewind_truncates_durable_size() {
2795 let executor = deterministic::Runner::default();
2796 executor.start(|context| async move {
2797 let pending = PendingSyncs::default();
2798 let cfg = Config {
2799 partition: "variable-rewind-truncate".into(),
2800 items_per_section: NZU64!(100),
2801 compression: None,
2802 codec_config: (),
2803 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2)),
2804 write_buffer: NZUsize!(2048),
2805 replay_buffer: NZUsize!(2048),
2806 };
2807 let mut journal = Box::new(
2808 Inner::<_, u64>::init(
2809 DelayedSyncContext {
2810 inner: context.child("journal"),
2811 pending: pending.clone(),
2812 },
2813 cfg,
2814 )
2815 .await
2816 .unwrap(),
2817 );
2818
2819 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2820 let journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
2821 assert_eq!(journal.offsets.recovery_watermark(), 3);
2822
2823 let mut journal = drive_pending_syncs(&pending, journal.rewind(2))
2827 .await
2828 .unwrap();
2829 journal.append(&9).await.unwrap();
2830 let (journal, h1) = journal.start_sync().await.unwrap();
2831 let data = next_pending_sync(&pending);
2832 release_pending_syncs(&pending);
2833 data.release
2834 .send(Err(commonware_runtime::Error::Io(
2835 std::io::Error::other("injected sync failure").into(),
2836 )))
2837 .unwrap();
2838 assert!(h1.await.is_err());
2839
2840 let (journal, h2) = journal.start_sync().await.unwrap();
2841 assert_eq!(journal.offsets.recovery_watermark(), 2);
2842 assert!(h2.await.is_err());
2843 });
2844 }
2845
2846 #[test]
2847 fn test_start_sync_watermark_advance_deferred_failure() {
2848 let executor = deterministic::Runner::default();
2849 executor.start(|context| async move {
2850 let pending = PendingSyncs::default();
2851 let cfg = Config {
2852 partition: "variable-watermark-deferred".into(),
2853 items_per_section: NZU64!(100),
2854 compression: None,
2855 codec_config: (),
2856 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2)),
2857 write_buffer: NZUsize!(2048),
2858 replay_buffer: NZUsize!(2048),
2859 };
2860 let mut journal = Box::new(
2861 Inner::<_, u64>::init(
2862 DelayedSyncContext {
2863 inner: context.child("journal"),
2864 pending: pending.clone(),
2865 },
2866 cfg,
2867 )
2868 .await
2869 .unwrap(),
2870 );
2871
2872 journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
2873 let (journal, h1) = journal.start_sync().await.unwrap();
2874 release_pending_syncs(&pending);
2875 h1.await.unwrap();
2876
2877 let (mut journal, h2) = journal.start_sync().await.unwrap();
2880 fail_pending_syncs(&pending);
2881 assert!(h2.await.is_err());
2882
2883 journal.append(&4).await.unwrap();
2886 let journal = drive_pending_syncs(&pending, journal.commit())
2887 .await
2888 .unwrap();
2889 assert!(drive_pending_syncs(&pending, journal.sync()).await.is_err());
2890 });
2891 }
2892
2893 #[test_traced]
2897 fn test_variable_dropped_failed_start_sync_surfaces_after_rollover() {
2898 let executor = deterministic::Runner::default();
2899 executor.start(|context| async move {
2900 let cfg = Config {
2901 partition: "variable-dropped-failed-commit".into(),
2902 items_per_section: NZU64!(3),
2903 compression: None,
2904 codec_config: (),
2905 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2)),
2906 write_buffer: NZUsize!(2048),
2907 replay_buffer: NZUsize!(2048),
2908 };
2909 let mut journal = Box::new(
2910 Inner::<_, u64>::init(context.child("journal"), cfg)
2911 .await
2912 .unwrap(),
2913 );
2914
2915 journal.append(&0).await.unwrap();
2918 *context.storage_fault_config().write() = deterministic::FaultConfig {
2919 write_rate: Some(deterministic::WriteConfig {
2920 failure_rate: probability!(1.0),
2921 retention_rate: probability!(0.0),
2922 mode: deterministic::PartialWriteMode::Prefix,
2923 }),
2924 ..Default::default()
2925 };
2926 let (mut journal, handle) = journal.start_sync().await.unwrap();
2927 drop(handle);
2928 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
2929
2930 assert!(matches!(
2932 journal.append_many(Many::Flat(&[1, 2, 3])).await,
2933 Err(Error::Runtime(_))
2934 ));
2935 });
2936 }
2937
2938 fn counter(buffer: &str, name: &str) -> u64 {
2940 buffer
2941 .lines()
2942 .find(|l| l.contains(name) && !l.starts_with('#'))
2943 .and_then(|l| l.split_whitespace().last())
2944 .and_then(|v| v.parse().ok())
2945 .expect("counter missing")
2946 }
2947
2948 #[test_traced]
2949 fn test_variable_init_syncs_adopted_data_before_offsets_watermark_advance() {
2950 let executor = deterministic::Runner::default();
2951 executor.start(|context| async move {
2952 let cfg = Config {
2953 partition: "init-adopted-variable".into(),
2954 items_per_section: NZU64!(10),
2955 compression: None,
2956 codec_config: (),
2957 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
2958 write_buffer: NZUsize!(1024),
2959 replay_buffer: NZUsize!(1024),
2960 };
2961
2962 let mut journal =
2963 Journal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone())
2964 .await
2965 .unwrap();
2966 (journal, _) = journal.append(&FixedBytes::new([1; 32])).await.unwrap();
2967 (journal, _) = journal.append(&FixedBytes::new([2; 32])).await.unwrap();
2968 let journal = journal.sync().await.unwrap();
2969 let journal = journal
2972 .test_set_offsets_recovery_watermark(1)
2973 .await
2974 .unwrap();
2975 drop(journal);
2976
2977 let data_partition = format!("{}{}", cfg.partition, DATA_SUFFIX);
2982 let context = commonware_runtime::mocks::SyncFaultContext {
2983 inner: context,
2984 fail_partition: data_partition,
2985 };
2986 assert!(
2987 Journal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone())
2988 .await
2989 .is_err(),
2990 "init must sync adopted data before advancing rebuilt offsets"
2991 );
2992 });
2993 }
2994
2995 #[test_traced]
2996 fn test_variable_append_many_compressed() {
2997 let executor = deterministic::Runner::default();
2998 executor.start(|context| async move {
2999 let cfg = Config {
3000 partition: "append-many-compressed".into(),
3001 items_per_section: NZU64!(3),
3002 compression: Some(1),
3003 codec_config: (),
3004 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3005 write_buffer: NZUsize!(1024),
3006 replay_buffer: NZUsize!(1024),
3007 };
3008 let mut journal = Journal::<_, FixedBytes<32>>::init(context.child("journal"), cfg)
3009 .await
3010 .unwrap();
3011 let items = [
3012 FixedBytes::new([0; 32]),
3013 FixedBytes::new([1; 32]),
3014 FixedBytes::new([2; 32]),
3015 FixedBytes::new([3; 32]),
3016 FixedBytes::new([4; 32]),
3017 ];
3018
3019 let last;
3020 (journal, last) = journal.append_many(Many::Flat(&items)).await.unwrap();
3021 assert_eq!(last, 4);
3022 for (pos, item) in items.iter().enumerate() {
3023 assert_eq!(journal.read(pos as u64).await.unwrap(), *item);
3024 }
3025
3026 journal.destroy().await.unwrap();
3027 });
3028 }
3029
3030 #[test_traced]
3031 fn test_variable_append_many_exceeding_write_buffer_reopens_across_sections() {
3032 let executor = deterministic::Runner::default();
3033 executor.start(|context| async move {
3034 let cfg = Config {
3035 partition: "append-many-exceeds-buffer".into(),
3036 items_per_section: NZU64!(5),
3037 compression: None,
3038 codec_config: (),
3039 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3040 write_buffer: NZUsize!(512),
3041 replay_buffer: NZUsize!(512),
3042 };
3043 let items = (0..13)
3044 .map(|i| FixedBytes::new([i as u8; 300]))
3045 .collect::<Vec<_>>();
3046
3047 let mut journal =
3048 Journal::<_, FixedBytes<300>>::init(context.child("first"), cfg.clone())
3049 .await
3050 .unwrap();
3051 let appended;
3052 (journal, appended) = journal.append_many(Many::Flat(&items)).await.unwrap();
3053 assert_eq!(appended, 12);
3054 journal.sync().await.unwrap();
3055
3056 let journal = Journal::<_, FixedBytes<300>>::init(context.child("second"), cfg)
3057 .await
3058 .unwrap();
3059 assert_eq!(journal.bounds(), 0..13);
3060 for (pos, item) in items.iter().enumerate() {
3061 assert_eq!(journal.read(pos as u64).await.unwrap(), *item);
3062 }
3063 assert_eq!(
3064 journal.read_many(&[0, 4, 5, 9, 10, 12]).await.unwrap(),
3065 vec![
3066 items[0].clone(),
3067 items[4].clone(),
3068 items[5].clone(),
3069 items[9].clone(),
3070 items[10].clone(),
3071 items[12].clone(),
3072 ]
3073 );
3074
3075 journal.destroy().await.unwrap();
3076 });
3077 }
3078
3079 #[test_traced]
3080 fn test_variable_init_at_max_size_rejected() {
3081 let executor = deterministic::Runner::default();
3082 executor.start(|context| async move {
3083 let cfg = Config {
3084 partition: "init-at-max".into(),
3085 items_per_section: NZU64!(5),
3086 compression: None,
3087 codec_config: (),
3088 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3089 write_buffer: NZUsize!(1024),
3090 replay_buffer: NZUsize!(1024),
3091 };
3092
3093 assert!(matches!(
3095 Journal::<_, u64>::init_at_size(context.child("max"), cfg, u64::MAX).await,
3096 Err(Error::SizeOverflow)
3097 ));
3098 });
3099 }
3100
3101 #[test_traced]
3102 fn test_variable_append_size_overflow() {
3103 let executor = deterministic::Runner::default();
3104 executor.start(|context| async move {
3105 let cfg = Config {
3106 partition: "append-size-overflow".into(),
3107 items_per_section: NZU64!(5),
3108 compression: None,
3109 codec_config: (),
3110 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3111 write_buffer: NZUsize!(1024),
3112 replay_buffer: NZUsize!(1024),
3113 };
3114
3115 let mut journal =
3117 Journal::<_, u64>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
3118 .await
3119 .unwrap();
3120
3121 let appended;
3123 (journal, appended) = journal.append(&7).await.unwrap();
3124 assert_eq!(appended, u64::MAX - 1);
3125 assert_eq!(journal.size(), u64::MAX);
3126
3127 assert!(matches!(journal.append(&8).await, Err(Error::SizeOverflow)));
3130 });
3131 }
3132
3133 #[test_traced]
3134 fn test_variable_replay_near_max_size() {
3135 let executor = deterministic::Runner::default();
3136 executor.start(|context| async move {
3137 let cfg = Config {
3138 partition: "replay-near-max-size".into(),
3139 items_per_section: NZU64!(10),
3140 compression: None,
3141 codec_config: (),
3142 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3143 write_buffer: NZUsize!(1024),
3144 replay_buffer: NZUsize!(1024),
3145 };
3146
3147 let mut journal =
3148 Journal::<_, u64>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
3149 .await
3150 .unwrap();
3151 let appended;
3152 (journal, appended) = journal.append(&7).await.unwrap();
3153 assert_eq!(appended, u64::MAX - 1);
3154
3155 {
3156 let reader;
3157 (journal, reader) = journal.snapshot().await.unwrap();
3158 let stream = reader
3159 .replay(u64::MAX - 1, NZUsize!(20), ReadOptions::default())
3160 .await
3161 .unwrap();
3162 futures::pin_mut!(stream);
3163 let (pos, item) = stream.next().await.unwrap().unwrap();
3164 assert_eq!(pos, u64::MAX - 1);
3165 assert_eq!(item, 7);
3166 assert!(stream.next().await.is_none());
3167 }
3168
3169 journal.destroy().await.unwrap();
3170 });
3171 }
3172
3173 #[test_traced]
3174 fn test_variable_try_read_many_sync_matches_read_many() {
3175 let executor = deterministic::Runner::default();
3178 executor.start(|context| async move {
3179 let cfg = Config {
3180 partition: "read-many-sync".into(),
3181 items_per_section: NZU64!(5),
3182 compression: None,
3183 codec_config: (),
3184 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(64)),
3185 write_buffer: NZUsize!(1024),
3186 replay_buffer: NZUsize!(1024),
3187 };
3188 let items = (0..13)
3189 .map(|i| FixedBytes::new([i as u8; 300]))
3190 .collect::<Vec<_>>();
3191 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
3192 .await
3193 .unwrap();
3194 (journal, _) = journal.append_many(Many::Flat(&items)).await.unwrap();
3195 journal = journal.sync().await.unwrap();
3196
3197 let positions: Vec<u64> = (0..items.len() as u64).collect();
3198 let reader;
3199 (journal, reader) = journal.snapshot().await.unwrap();
3200 let expected = reader.read_many(&positions).await.unwrap();
3203 let served = reader.try_read_many_sync(&positions);
3204 assert_eq!(served.len(), positions.len());
3205 for (item, expected) in served.iter().zip(&expected) {
3206 assert_eq!(item.as_ref().expect("cached position is served"), expected);
3207 }
3208
3209 let served = reader.try_read_many_sync(&[9, 13]);
3213 assert!(served[0].is_some());
3214 assert!(served[1].is_none());
3215 drop(served);
3216 drop(reader);
3217
3218 journal.destroy().await.unwrap();
3219 });
3220 }
3221
3222 #[test_traced]
3223 #[should_panic(expected = "positions must be strictly increasing")]
3224 fn test_variable_read_many_rejects_unsorted_positions() {
3225 let executor = deterministic::Runner::default();
3226 executor.start(|context| async move {
3227 let cfg = Config {
3228 partition: "read-many-unsorted".into(),
3229 items_per_section: NZU64!(5),
3230 compression: None,
3231 codec_config: (),
3232 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3233 write_buffer: NZUsize!(1024),
3234 replay_buffer: NZUsize!(1024),
3235 };
3236 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
3237 .await
3238 .unwrap();
3239 for i in 0..5u64 {
3240 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3241 }
3242 journal = journal.sync().await.unwrap();
3243
3244 let (_journal, reader) = journal.snapshot().await.unwrap();
3245 let _ = reader.read_many(&[2, 1]).await;
3246 });
3247 }
3248
3249 #[test_traced]
3250 #[should_panic(expected = "positions must be strictly increasing")]
3251 fn test_variable_read_many_rejects_duplicate_positions() {
3252 let executor = deterministic::Runner::default();
3254 executor.start(|context| async move {
3255 let cfg = Config {
3256 partition: "read-many-duplicate".into(),
3257 items_per_section: NZU64!(5),
3258 compression: None,
3259 codec_config: (),
3260 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3261 write_buffer: NZUsize!(1024),
3262 replay_buffer: NZUsize!(1024),
3263 };
3264 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
3265 .await
3266 .unwrap();
3267 for i in 0..5u64 {
3268 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3269 }
3270 journal = journal.sync().await.unwrap();
3271
3272 let (_journal, reader) = journal.snapshot().await.unwrap();
3273 let _ = reader.read_many(&[1, 1]).await;
3274 });
3275 }
3276
3277 #[test_traced]
3278 fn test_variable_probe_then_read_many_matches_read_many() {
3279 let executor = deterministic::Runner::default();
3282 executor.start(|context| async move {
3283 let cfg = Config {
3284 partition: "read-many-probe-complete".into(),
3285 items_per_section: NZU64!(5),
3286 compression: None,
3287 codec_config: (),
3288 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3289 write_buffer: NZUsize!(1024),
3290 replay_buffer: NZUsize!(1024),
3291 };
3292 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
3293 .await
3294 .unwrap();
3295 for i in 0..12u64 {
3296 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3297 }
3298 journal = journal.sync().await.unwrap();
3299
3300 let positions: Vec<u64> = (0..12).collect();
3301 let expected: Vec<u64> = (0..12).map(|i| i * 100).collect();
3302 let reader;
3303 (journal, reader) = journal.snapshot().await.unwrap();
3304 for _ in 0..2 {
3305 let mut served = reader.try_read_many_sync(&positions);
3306 let misses: Vec<u64> = positions
3307 .iter()
3308 .zip(&served)
3309 .filter_map(|(&pos, item)| item.is_none().then_some(pos))
3310 .collect();
3311 let mut fetched = reader.read_many(&misses).await.unwrap().into_iter();
3312 for item in served.iter_mut().filter(|item| item.is_none()) {
3313 *item = fetched.next();
3314 }
3315 let completed: Vec<_> = served.into_iter().map(Option::unwrap).collect();
3316 assert_eq!(completed, expected);
3317 }
3318 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
3319 drop(reader);
3320
3321 journal.destroy().await.unwrap();
3322 });
3323 }
3324
3325 #[test_traced]
3326 fn test_variable_read_many_reuses_probed_offset() {
3327 let executor = deterministic::Runner::default();
3330 executor.start(|context| async move {
3331 let cfg = Config {
3335 partition: "read-many-offset-reuse".into(),
3336 items_per_section: NZU64!(128),
3337 compression: None,
3338 codec_config: (),
3339 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(4)),
3340 write_buffer: NZUsize!(1024),
3341 replay_buffer: NZUsize!(1024),
3342 };
3343 let appended = (0..140)
3344 .map(|i| FixedBytes::new([i as u8; 300]))
3345 .collect::<Vec<_>>();
3346 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
3347 .await
3348 .unwrap();
3349 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
3350 journal = journal.sync().await.unwrap();
3351 let reader;
3352 (journal, reader) = journal.snapshot().await.unwrap();
3353
3354 reader
3359 .read_many(&(128..136).collect::<Vec<u64>>())
3360 .await
3361 .unwrap();
3362 reader.read(4).await.unwrap();
3363
3364 let (items, misses) = reader.probe_parts(&[0]);
3366 assert!(items[0].is_none());
3367 assert_eq!(misses[0].offset, Some(0));
3368
3369 let before = context.encode();
3373 assert_eq!(
3374 reader.read_many(&[0]).await.unwrap(),
3375 vec![appended[0].clone()]
3376 );
3377 let after = context.encode();
3378 assert_eq!(
3379 counter(&after, "offsets_items_read_total"),
3380 counter(&before, "offsets_items_read_total") + 2
3381 );
3382 drop(reader);
3383
3384 journal.destroy().await.unwrap();
3385 });
3386 }
3387
3388 #[test_traced]
3389 fn test_variable_read_many_consecutive_after_reopen() {
3390 let executor = deterministic::Runner::default();
3391 executor.start(|context| async move {
3392 let cfg = Config {
3393 partition: "read-many-consecutive-after-reopen".into(),
3394 items_per_section: NZU64!(20),
3395 compression: None,
3396 codec_config: (),
3397 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3398 write_buffer: NZUsize!(1024),
3399 replay_buffer: NZUsize!(1024),
3400 };
3401
3402 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3403 .await
3404 .unwrap();
3405 for i in 0..20u64 {
3406 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3407 }
3408 let journal = journal.sync().await.unwrap();
3409 drop(journal);
3410
3411 let cfg = Config {
3412 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
3413 ..cfg
3414 };
3415 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
3416 .await
3417 .unwrap();
3418 let reader;
3419 (journal, reader) = journal.snapshot().await.unwrap();
3420 let positions: Vec<u64> = (3..10).collect();
3421 let items = reader.read_many(&positions).await.unwrap();
3422 assert_eq!(items, vec![300, 400, 500, 600, 700, 800, 900]);
3423 drop(reader);
3424
3425 journal.destroy().await.unwrap();
3426 });
3427 }
3428
3429 #[test_traced]
3430 fn test_variable_read_many_scattered_single_runs_across_blobs() {
3431 let executor = deterministic::Runner::default();
3437 executor.start(|context| async move {
3438 let cfg = Config {
3439 partition: "read-many-scattered-runs".into(),
3440 items_per_section: NZU64!(5),
3441 compression: None,
3442 codec_config: (),
3443 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(16)),
3444 write_buffer: NZUsize!(1024),
3445 replay_buffer: NZUsize!(1024),
3446 };
3447
3448 let items = (0..30)
3452 .map(|i| FixedBytes::new([i as u8; 300]))
3453 .collect::<Vec<_>>();
3454 let mut journal =
3455 Journal::<_, FixedBytes<300>>::init(context.child("first"), cfg.clone())
3456 .await
3457 .unwrap();
3458 for item in &items {
3459 (journal, _) = journal.append(item).await.unwrap();
3460 }
3461 let journal = journal.sync().await.unwrap();
3462 drop(journal);
3463
3464 let cfg = Config {
3466 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(16)),
3467 ..cfg
3468 };
3469 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("second"), cfg)
3470 .await
3471 .unwrap();
3472 let reader;
3473 (journal, reader) = journal.snapshot().await.unwrap();
3474
3475 reader.read(6).await.unwrap();
3478 reader.read(16).await.unwrap();
3479
3480 for hit in [5, 6, 15, 17] {
3484 assert!(reader.try_read_sync(hit).is_some(), "position {hit}");
3485 }
3486 let misses = [0, 3, 10, 12, 20, 21, 23];
3487 for miss in misses {
3488 assert!(reader.try_read_sync(miss).is_none(), "position {miss}");
3489 }
3490
3491 let positions = [0, 3, 5, 6, 10, 12, 15, 17, 20, 21, 23];
3494 let expected: Vec<_> = positions
3495 .iter()
3496 .map(|&p| items[p as usize].clone())
3497 .collect();
3498 let before = context.encode();
3499 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
3500
3501 let after = context.encode();
3503 assert_eq!(
3504 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
3505 4
3506 );
3507 assert_eq!(
3508 counter(&after, "second_cache_misses") - counter(&before, "second_cache_misses"),
3509 misses.len() as u64
3510 );
3511
3512 let before = context.encode();
3514 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
3515 let after = context.encode();
3516 assert_eq!(
3517 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
3518 positions.len() as u64
3519 );
3520 assert_eq!(
3521 counter(&after, "second_cache_misses"),
3522 counter(&before, "second_cache_misses")
3523 );
3524 drop(reader);
3525
3526 journal.destroy().await.unwrap();
3527 });
3528 }
3529
3530 #[test_traced]
3536 fn test_variable_offsets_partition_loss_after_prune_unrecoverable() {
3537 let executor = deterministic::Runner::default();
3538 executor.start(|context| async move {
3539 let cfg = Config {
3540 partition: "offsets-loss-after-prune".into(),
3541 items_per_section: NZU64!(10),
3542 compression: None,
3543 codec_config: (),
3544 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3545 write_buffer: NZUsize!(1024),
3546 replay_buffer: NZUsize!(1024),
3547 };
3548
3549 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3551 .await
3552 .unwrap();
3553
3554 for i in 0..40u64 {
3556 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3557 }
3558
3559 let (journal, _) = journal.prune(20).await.unwrap();
3561 let bounds = journal.bounds();
3562 assert_eq!(bounds.start, 20);
3563 assert_eq!(bounds.end, 40);
3564
3565 let journal = journal.sync().await.unwrap();
3566 drop(journal);
3567
3568 context
3571 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
3572 .await
3573 .expect("Failed to remove offsets blobs partition");
3574 context
3575 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
3576 .await
3577 .expect("Failed to remove offsets metadata partition");
3578
3579 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
3581 assert!(matches!(result, Err(Error::Corruption(_))));
3582 });
3583 }
3584
3585 #[test_traced]
3593 fn test_variable_align_data_offsets_mismatch() {
3594 let executor = deterministic::Runner::default();
3595 executor.start(|context| async move {
3596 let cfg = Config {
3597 partition: "data-loss-test".into(),
3598 items_per_section: NZU64!(10),
3599 compression: None,
3600 codec_config: (),
3601 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3602 write_buffer: NZUsize!(1024),
3603 replay_buffer: NZUsize!(1024),
3604 };
3605
3606 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
3608 .await
3609 .unwrap();
3610
3611 for i in 0..20u64 {
3613 (variable, _) = variable.append(&(i * 100)).await.unwrap();
3614 }
3615
3616 let variable = variable.sync().await.unwrap();
3617 drop(variable);
3618
3619 context
3621 .remove(&cfg.data_partition(), None)
3622 .await
3623 .expect("Failed to remove data partition");
3624
3625 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
3627 .await
3628 .expect("Should align offsets to match empty data");
3629
3630 assert_eq!(journal.size(), 20);
3632
3633 assert!(journal.bounds().is_empty());
3635
3636 for i in 0..20 {
3638 assert!(matches!(
3639 journal.read(i).await,
3640 Err(crate::journal::Error::ItemPruned(_))
3641 ));
3642 }
3643
3644 let pos;
3646 (journal, pos) = journal.append(&999).await.unwrap();
3647 assert_eq!(pos, 20);
3648 assert_eq!(journal.read(20).await.unwrap(), 999);
3649
3650 journal.destroy().await.unwrap();
3651 });
3652 }
3653
3654 #[test_traced]
3656 fn test_variable_replay() {
3657 let executor = deterministic::Runner::default();
3658 executor.start(|context| async move {
3659 let cfg = Config {
3660 partition: "replay".into(),
3661 items_per_section: NZU64!(10),
3662 compression: None,
3663 codec_config: (),
3664 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3665 write_buffer: NZUsize!(1024),
3666 replay_buffer: NZUsize!(1024),
3667 };
3668
3669 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
3671
3672 for i in 0..40u64 {
3674 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3675 }
3676
3677 {
3679 let reader;
3680 (journal, reader) = journal.snapshot().await.unwrap();
3681 let stream = reader
3682 .replay(0, NZUsize!(20), ReadOptions::default())
3683 .await
3684 .unwrap();
3685 futures::pin_mut!(stream);
3686 for i in 0..40u64 {
3687 let (pos, item) = stream.next().await.unwrap().unwrap();
3688 assert_eq!(pos, i);
3689 assert_eq!(item, i * 100);
3690 }
3691 assert!(stream.next().await.is_none());
3692 }
3693
3694 {
3696 let reader;
3697 (journal, reader) = journal.snapshot().await.unwrap();
3698 let stream = reader
3699 .replay(15, NZUsize!(20), ReadOptions::default())
3700 .await
3701 .unwrap();
3702 futures::pin_mut!(stream);
3703 for i in 15..40u64 {
3704 let (pos, item) = stream.next().await.unwrap().unwrap();
3705 assert_eq!(pos, i);
3706 assert_eq!(item, i * 100);
3707 }
3708 assert!(stream.next().await.is_none());
3709 }
3710
3711 {
3713 let reader;
3714 (journal, reader) = journal.snapshot().await.unwrap();
3715 let stream = reader
3716 .replay(20, NZUsize!(20), ReadOptions::default())
3717 .await
3718 .unwrap();
3719 futures::pin_mut!(stream);
3720 for i in 20..40u64 {
3721 let (pos, item) = stream.next().await.unwrap().unwrap();
3722 assert_eq!(pos, i);
3723 assert_eq!(item, i * 100);
3724 }
3725 assert!(stream.next().await.is_none());
3726 }
3727
3728 (journal, _) = journal.prune(20).await.unwrap();
3730 {
3731 let reader;
3732 (journal, reader) = journal.snapshot().await.unwrap();
3733 let res = reader.replay(0, NZUsize!(20), ReadOptions::default()).await;
3734 assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_))));
3735 }
3736 {
3737 let reader;
3738 (journal, reader) = journal.snapshot().await.unwrap();
3739 let res = reader
3740 .replay(19, NZUsize!(20), ReadOptions::default())
3741 .await;
3742 assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_))));
3743 }
3744
3745 {
3747 let reader;
3748 (journal, reader) = journal.snapshot().await.unwrap();
3749 let stream = reader
3750 .replay(20, NZUsize!(20), ReadOptions::default())
3751 .await
3752 .unwrap();
3753 futures::pin_mut!(stream);
3754 for i in 20..40u64 {
3755 let (pos, item) = stream.next().await.unwrap().unwrap();
3756 assert_eq!(pos, i);
3757 assert_eq!(item, i * 100);
3758 }
3759 assert!(stream.next().await.is_none());
3760 }
3761
3762 {
3764 let reader;
3765 (journal, reader) = journal.snapshot().await.unwrap();
3766 let stream = reader
3767 .replay(40, NZUsize!(20), ReadOptions::default())
3768 .await
3769 .unwrap();
3770 futures::pin_mut!(stream);
3771 assert!(stream.next().await.is_none());
3772 }
3773
3774 {
3776 let reader;
3777 (journal, reader) = journal.snapshot().await.unwrap();
3778 let res = reader
3779 .replay(41, NZUsize!(20), ReadOptions::default())
3780 .await;
3781 assert!(matches!(
3782 res,
3783 Err(crate::journal::Error::ItemOutOfRange(41))
3784 ));
3785 }
3786
3787 journal.destroy().await.unwrap();
3788 });
3789 }
3790
3791 #[test_traced]
3792 fn test_variable_replay_stops_after_error() {
3793 let executor = deterministic::Runner::default();
3794 executor.start(|context| async move {
3795 let cfg = Config {
3796 partition: "replay-stops-after-error".into(),
3797 items_per_section: NZU64!(10),
3798 compression: None,
3799 codec_config: (),
3800 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3801 write_buffer: NZUsize!(1024),
3802 replay_buffer: NZUsize!(1024),
3803 };
3804
3805 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
3806 .await
3807 .unwrap();
3808 for i in 0..30u64 {
3809 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3810 }
3811 let journal = journal.sync().await.unwrap();
3812
3813 let (blob, _) = context
3814 .open(&cfg.data_partition(), &1u64.to_be_bytes())
3815 .await
3816 .unwrap();
3817 blob.write_at(0, vec![0xFF; 1], WriteOptions::SYNC)
3818 .await
3819 .unwrap();
3820
3821 {
3822 let cache = CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10));
3823 let mut writers = Vec::new();
3824 for blob_index in 0..3u64 {
3825 let (blob, size) = context
3826 .open(&cfg.data_partition(), &blob_index.to_be_bytes())
3827 .await
3828 .unwrap();
3829 writers.push(
3830 Writer::new(blob, size, cfg.write_buffer.get(), cache.clone())
3831 .await
3832 .unwrap(),
3833 );
3834 }
3835
3836 let mut states = Vec::new();
3837 for (blob_index, writer) in writers.iter().enumerate() {
3838 let blob = blob_index as u64;
3839 states.push(ReplayState::<_, u64> {
3840 blob,
3841 replay: Blob::Writer(writer)
3842 .replay_from(0, NZUsize!(1024), ReadOptions::default())
3843 .unwrap(),
3844 budget: 1024,
3845 pos: blob * 10,
3846 end_pos: (blob + 1) * 10,
3847 offset: 0,
3848 codec_config: (),
3849 compressed: false,
3850 _marker: PhantomData,
3851 });
3852 }
3853
3854 let stream = crate::journal::contiguous::replay_stream_from_states(states);
3855 futures::pin_mut!(stream);
3856
3857 for i in 0..10u64 {
3858 let (pos, item) = stream.next().await.unwrap().unwrap();
3859 assert_eq!(pos, i);
3860 assert_eq!(item, i * 100);
3861 }
3862 assert!(matches!(
3863 stream.next().await.unwrap(),
3864 Err(Error::Corruption(_))
3865 ));
3866 assert!(stream.next().await.is_none());
3867 }
3868
3869 journal.destroy().await.unwrap();
3870 });
3871 }
3872
3873 #[test_traced]
3874 fn test_variable_contiguous() {
3875 let executor = deterministic::Runner::default();
3876 executor.start(|context| async move {
3877 run_contiguous_tests(move |test_name: String, idx: usize| {
3878 let label = test_name.replace('-', "_");
3879 let context = context
3880 .child("test")
3881 .with_attribute("name", &label)
3882 .with_attribute("index", idx);
3883 async move {
3884 let cfg = Config {
3885 partition: format!("generic-test-{test_name}"),
3886 items_per_section: NZU64!(10),
3887 compression: None,
3888 codec_config: (),
3889 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3890 write_buffer: NZUsize!(1024),
3891 replay_buffer: NZUsize!(1024),
3892 };
3893 Journal::<_, u64>::init(context, cfg).await
3894 }
3895 .boxed()
3896 })
3897 .await;
3898 });
3899 }
3900
3901 #[test_traced]
3904 fn test_variable_prune_durability_survives_crash() {
3905 fn cfg(pooler: &impl BufferPooler) -> Config<()> {
3906 Config {
3907 partition: "variable-prune-durability".into(),
3908 items_per_section: NZU64!(3),
3909 compression: None,
3910 codec_config: (),
3911 page_cache: CacheRef::from_pooler(pooler, LARGE_PAGE_SIZE, NZUsize!(10)),
3912 write_buffer: NZUsize!(1024),
3913 replay_buffer: NZUsize!(1024),
3914 }
3915 }
3916
3917 let executor = deterministic::Runner::default();
3918 let (_, checkpoint) = executor.start_and_recover(|context| async move {
3919 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg(&context))
3920 .await
3921 .unwrap();
3922
3923 for i in 0..8u64 {
3927 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3928 }
3929 let (journal, pruned) = journal.prune(3).await.unwrap();
3930 assert!(pruned);
3931 drop(journal);
3932 });
3933
3934 deterministic::Runner::from(checkpoint).start(|context| async move {
3935 let journal = Journal::<_, u64>::init(context.child("recover"), cfg(&context))
3936 .await
3937 .unwrap();
3938 assert_eq!(
3939 journal.bounds(),
3940 3..8,
3941 "pruned journal lost acknowledged items"
3942 );
3943 for i in 3..8u64 {
3944 assert_eq!(journal.read(i).await.unwrap(), i * 100);
3945 }
3946 journal.destroy().await.unwrap();
3947 });
3948 }
3949
3950 #[test_traced]
3952 fn test_variable_multiple_sequential_prunes() {
3953 let executor = deterministic::Runner::default();
3954 executor.start(|context| async move {
3955 let cfg = Config {
3956 partition: "sequential-prunes".into(),
3957 items_per_section: NZU64!(10),
3958 compression: None,
3959 codec_config: (),
3960 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
3961 write_buffer: NZUsize!(1024),
3962 replay_buffer: NZUsize!(1024),
3963 };
3964
3965 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
3966
3967 for i in 0..40u64 {
3969 (journal, _) = journal.append(&(i * 100)).await.unwrap();
3970 }
3971
3972 let bounds = journal.bounds();
3974 assert_eq!(bounds.start, 0);
3975 assert_eq!(bounds.end, 40);
3976
3977 let pruned;
3979 (journal, pruned) = journal.prune(10).await.unwrap();
3980 assert!(pruned);
3981
3982 assert_eq!(journal.bounds().start, 10);
3984
3985 assert!(matches!(
3987 journal.read(0).await,
3988 Err(crate::journal::Error::ItemPruned(_))
3989 ));
3990 assert_eq!(journal.read(10).await.unwrap(), 1000);
3991 assert_eq!(journal.read(19).await.unwrap(), 1900);
3992
3993 let pruned;
3995 (journal, pruned) = journal.prune(20).await.unwrap();
3996 assert!(pruned);
3997
3998 assert_eq!(journal.bounds().start, 20);
4000
4001 assert!(matches!(
4003 journal.read(10).await,
4004 Err(crate::journal::Error::ItemPruned(_))
4005 ));
4006 assert!(matches!(
4007 journal.read(19).await,
4008 Err(crate::journal::Error::ItemPruned(_))
4009 ));
4010 assert_eq!(journal.read(20).await.unwrap(), 2000);
4011 assert_eq!(journal.read(29).await.unwrap(), 2900);
4012
4013 let pruned;
4015 (journal, pruned) = journal.prune(30).await.unwrap();
4016 assert!(pruned);
4017
4018 assert_eq!(journal.bounds().start, 30);
4020
4021 assert!(matches!(
4023 journal.read(20).await,
4024 Err(crate::journal::Error::ItemPruned(_))
4025 ));
4026 assert!(matches!(
4027 journal.read(29).await,
4028 Err(crate::journal::Error::ItemPruned(_))
4029 ));
4030 assert_eq!(journal.read(30).await.unwrap(), 3000);
4031 assert_eq!(journal.read(39).await.unwrap(), 3900);
4032
4033 assert_eq!(journal.size(), 40);
4035
4036 journal.destroy().await.unwrap();
4037 });
4038 }
4039
4040 #[test_traced]
4042 fn test_variable_prune_all_then_reinit() {
4043 let executor = deterministic::Runner::default();
4044 executor.start(|context| async move {
4045 let cfg = Config {
4046 partition: "prune-all-reinit".into(),
4047 items_per_section: NZU64!(10),
4048 compression: None,
4049 codec_config: (),
4050 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4051 write_buffer: NZUsize!(1024),
4052 replay_buffer: NZUsize!(1024),
4053 };
4054
4055 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4057 .await
4058 .unwrap();
4059
4060 for i in 0..100u64 {
4061 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4062 }
4063
4064 let bounds = journal.bounds();
4065 assert_eq!(bounds.end, 100);
4066 assert_eq!(bounds.start, 0);
4067
4068 let pruned;
4070 (journal, pruned) = journal.prune(100).await.unwrap();
4071 assert!(pruned);
4072
4073 let bounds = journal.bounds();
4075 assert_eq!(bounds.end, 100);
4076 assert!(bounds.is_empty());
4077
4078 for i in 0..100 {
4080 assert!(matches!(
4081 journal.read(i).await,
4082 Err(crate::journal::Error::ItemPruned(_))
4083 ));
4084 }
4085
4086 journal.sync().await.unwrap();
4087
4088 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4090 .await
4091 .unwrap();
4092
4093 let bounds = journal.bounds();
4095 assert_eq!(bounds.end, 100);
4096 assert!(bounds.is_empty());
4097
4098 for i in 0..100 {
4100 assert!(matches!(
4101 journal.read(i).await,
4102 Err(crate::journal::Error::ItemPruned(_))
4103 ));
4104 }
4105
4106 (journal, _) = journal.append(&10000).await.unwrap();
4109 let bounds = journal.bounds();
4110 assert_eq!(bounds.end, 101);
4111 assert_eq!(bounds.start, 100);
4113
4114 assert_eq!(journal.read(100).await.unwrap(), 10000);
4116
4117 assert!(matches!(
4119 journal.read(99).await,
4120 Err(crate::journal::Error::ItemPruned(_))
4121 ));
4122
4123 journal.destroy().await.unwrap();
4124 });
4125 }
4126
4127 #[test_traced]
4129 fn test_variable_recovery_prune_crash_offsets_behind() {
4130 let executor = deterministic::Runner::default();
4131 executor.start(|context| async move {
4132 let cfg = Config {
4134 partition: "recovery-prune-crash".into(),
4135 items_per_section: NZU64!(10),
4136 compression: None,
4137 codec_config: (),
4138 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4139 write_buffer: NZUsize!(1024),
4140 replay_buffer: NZUsize!(1024),
4141 };
4142
4143 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4144 .await
4145 .unwrap();
4146
4147 for i in 0..40u64 {
4149 (variable, _) = variable.append(&(i * 100)).await.unwrap();
4150 }
4151
4152 let (mut variable, _) = variable.prune(10).await.unwrap();
4154 assert_eq!(variable.bounds().start, 10);
4155
4156 variable.test_prune_data(2).await.unwrap();
4159 variable.sync().await.unwrap();
4162
4163 let (recovery_context, recordings) = RecordingContext::new(context.child("second"));
4165 let variable = Journal::<_, u64>::init(recovery_context, cfg.clone())
4166 .await
4167 .unwrap();
4168
4169 let reads = recordings.snapshot().reads;
4172 assert!(reads.len() > 2);
4173 let (metadata_reads, recovery_reads) = reads.split_at(2);
4174 assert_eq!(metadata_reads, [ReadOptions::DONT_CACHE; 2]);
4175
4176 assert!(
4179 recovery_reads
4180 .iter()
4181 .all(|options| *options == ReadOptions::default())
4182 );
4183
4184 let bounds = variable.bounds();
4186 assert_eq!(bounds.start, 20);
4187 assert_eq!(bounds.end, 40);
4188
4189 assert!(matches!(
4191 variable.read(10).await,
4192 Err(crate::journal::Error::ItemPruned(_))
4193 ));
4194
4195 assert_eq!(variable.read(20).await.unwrap(), 2000);
4197 assert_eq!(variable.read(39).await.unwrap(), 3900);
4198
4199 variable.destroy().await.unwrap();
4200 });
4201 }
4202
4203 #[test_traced]
4206 fn test_variable_recovery_prune_crash_offsets_end_behind() {
4207 let executor = deterministic::Runner::default();
4208 executor.start(|context| async move {
4209 let cfg = Config {
4210 partition: "recovery-prune-offsets-end-behind".into(),
4211 items_per_section: NZU64!(10),
4212 compression: None,
4213 codec_config: (),
4214 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4215 write_buffer: NZUsize!(1024),
4216 replay_buffer: NZUsize!(1024),
4217 };
4218
4219 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4220 .await
4221 .unwrap();
4222
4223 for i in 0..7u64 {
4226 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4227 }
4228 let mut journal = journal.sync().await.unwrap();
4229 for i in 7..12u64 {
4230 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4231 }
4232
4233 journal.0.halt_before_offsets_prune = true;
4237 {
4238 let fut = journal.prune(10);
4239 futures::pin_mut!(fut);
4240 assert!(
4241 futures::poll!(fut.as_mut()).is_pending(),
4242 "prune must park before offsets.prune"
4243 );
4244 }
4245
4246 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4247 .await
4248 .expect("prune crash must leave a recoverable journal");
4249 assert_eq!(journal.bounds(), 10..12);
4250 for i in 10..12u64 {
4251 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4252 }
4253 journal.destroy().await.unwrap();
4254 });
4255 }
4256
4257 #[test_traced]
4262 fn test_variable_recovery_offsets_ahead_corruption() {
4263 let executor = deterministic::Runner::default();
4264 executor.start(|context| async move {
4265 let cfg = Config {
4267 partition: "recovery-offsets-ahead".into(),
4268 items_per_section: NZU64!(10),
4269 compression: None,
4270 codec_config: (),
4271 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4272 write_buffer: NZUsize!(1024),
4273 replay_buffer: NZUsize!(1024),
4274 };
4275
4276 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4277 .await
4278 .unwrap();
4279
4280 for i in 0..40u64 {
4282 (variable, _) = variable.append(&(i * 100)).await.unwrap();
4283 }
4284
4285 let (mut variable, _) = variable.test_prune_offsets(20).await.unwrap(); variable.test_prune_data(1).await.unwrap(); let variable = variable.sync().await.unwrap();
4290 drop(variable);
4291
4292 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
4294 assert!(matches!(result, Err(Error::Corruption(_))));
4295 });
4296 }
4297
4298 #[test_traced]
4301 fn test_variable_recovery_offsets_empty_different_blob_is_corruption() {
4302 let executor = deterministic::Runner::default();
4303 executor.start(|context| async move {
4304 let cfg = Config {
4305 partition: "offsets-empty-diff-blob".into(),
4306 items_per_section: NZU64!(10),
4307 compression: None,
4308 codec_config: (),
4309 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4310 write_buffer: NZUsize!(1024),
4311 replay_buffer: NZUsize!(1024),
4312 };
4313
4314 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4315 .await
4316 .unwrap();
4317
4318 for i in 0..15u64 {
4319 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4320 }
4321 let mut journal = journal.sync().await.unwrap();
4322
4323 journal.0.offsets = journal.0.offsets.clear_to_size(20).await.unwrap();
4326 drop(journal);
4327
4328 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
4329 assert!(matches!(result, Err(Error::Corruption(_))));
4330 });
4331 }
4332
4333 #[test_traced]
4336 fn test_variable_recovery_offsets_end_behind_data_oldest_is_corruption() {
4337 let executor = deterministic::Runner::default();
4338 executor.start(|context| async move {
4339 let cfg = Config {
4340 partition: "offsets-end-behind-data-oldest".into(),
4341 items_per_section: NZU64!(10),
4342 compression: None,
4343 codec_config: (),
4344 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4345 write_buffer: NZUsize!(1024),
4346 replay_buffer: NZUsize!(1024),
4347 };
4348
4349 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4350 .await
4351 .unwrap();
4352
4353 for i in 0..15u64 {
4354 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4355 }
4356 let mut journal = journal.sync().await.unwrap();
4357
4358 journal.test_prune_data(1).await.unwrap();
4361 let journal = journal.test_rewind_offsets(5).await.unwrap();
4362 drop(journal);
4363
4364 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
4365 assert!(matches!(result, Err(Error::Corruption(_))));
4366 });
4367 }
4368
4369 #[test_traced]
4372 fn test_variable_recovery_offsets_start_mid_blob_ahead_of_data() {
4373 let executor = deterministic::Runner::default();
4374 executor.start(|context| async move {
4375 let cfg = Config {
4376 partition: "offsets-mid-blob-ahead".into(),
4377 items_per_section: NZU64!(10),
4378 compression: None,
4379 codec_config: (),
4380 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4381 write_buffer: NZUsize!(1024),
4382 replay_buffer: NZUsize!(1024),
4383 };
4384
4385 let mut journal =
4389 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
4390 .await
4391 .unwrap();
4392 for i in 0..5u64 {
4393 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4394 }
4395 journal.sync().await.unwrap();
4396
4397 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4398 .await
4399 .unwrap();
4400 assert_eq!(journal.bounds(), 7..12);
4401 assert_eq!(journal.read(7).await.unwrap(), 0);
4402 assert_eq!(journal.read(11).await.unwrap(), 400);
4403 journal.destroy().await.unwrap();
4404 });
4405 }
4406
4407 #[test_traced]
4411 fn test_variable_recovery_watermark_below_offsets_start() {
4412 let executor = deterministic::Runner::default();
4413 executor.start(|context| async move {
4414 let cfg = Config {
4415 partition: "watermark-below-start".into(),
4416 items_per_section: NZU64!(10),
4417 compression: None,
4418 codec_config: (),
4419 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4420 write_buffer: NZUsize!(1024),
4421 replay_buffer: NZUsize!(1024),
4422 };
4423
4424 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4425 .await
4426 .unwrap();
4427 for i in 0..25u64 {
4428 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4429 }
4430 let journal = journal.sync().await.unwrap();
4431
4432 let (journal, _) = journal.prune(10).await.unwrap();
4434 let journal = journal
4435 .test_set_offsets_recovery_watermark(5)
4436 .await
4437 .unwrap();
4438 drop(journal);
4439
4440 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4442 .await
4443 .unwrap();
4444 assert_eq!(journal.bounds(), 10..25);
4445 assert_eq!(journal.read(10).await.unwrap(), 1000);
4446 assert_eq!(journal.read(24).await.unwrap(), 2400);
4447 journal.destroy().await.unwrap();
4448 });
4449 }
4450
4451 #[test_traced]
4453 fn test_variable_recovery_append_crash_offsets_behind() {
4454 let executor = deterministic::Runner::default();
4455 executor.start(|context| async move {
4456 let cfg = Config {
4458 partition: "recovery-append-crash".into(),
4459 items_per_section: NZU64!(10),
4460 compression: None,
4461 codec_config: (),
4462 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4463 write_buffer: NZUsize!(1024),
4464 replay_buffer: NZUsize!(1024),
4465 };
4466
4467 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4468 .await
4469 .unwrap();
4470
4471 for i in 0..15u64 {
4473 (variable, _) = variable.append(&(i * 100)).await.unwrap();
4474 }
4475
4476 assert_eq!(variable.size(), 15);
4477
4478 for i in 15..20u64 {
4480 variable.test_append_data(1, i * 100).await.unwrap();
4481 }
4482 variable.sync().await.unwrap();
4485
4486 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4488 .await
4489 .unwrap();
4490
4491 let bounds = variable.bounds();
4493 assert_eq!(bounds.end, 20);
4494 assert_eq!(bounds.start, 0);
4495
4496 for i in 0..20u64 {
4498 assert_eq!(variable.read(i).await.unwrap(), i * 100);
4499 }
4500
4501 assert_eq!(variable.test_offsets_size(), 20);
4503
4504 variable.destroy().await.unwrap();
4505 });
4506 }
4507
4508 #[test_traced]
4509 fn test_variable_recovery_rejects_overlong_data_blob() {
4510 let executor = deterministic::Runner::default();
4511 executor.start(|context| async move {
4512 let cfg = Config {
4513 partition: "recovery-overlong-data-blob".into(),
4514 items_per_section: NZU64!(10),
4515 compression: None,
4516 codec_config: (),
4517 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4518 write_buffer: NZUsize!(1024),
4519 replay_buffer: NZUsize!(1024),
4520 };
4521
4522 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4523 .await
4524 .unwrap();
4525
4526 for i in 0..11u64 {
4527 journal.test_append_data(0, i * 100).await.unwrap();
4528 }
4529 journal.0.blobs.start_sync().await.await.unwrap();
4530 drop(journal);
4531
4532 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
4533 assert!(matches!(result, Err(Error::Corruption(_))));
4534 });
4535 }
4536
4537 #[test_traced]
4542 fn test_variable_recovery_rejects_over_capacity_non_newest_blob() {
4543 let executor = deterministic::Runner::default();
4544 executor.start(|context| async move {
4545 let cfg = Config {
4546 partition: "recovery-over-capacity-non-newest".into(),
4547 items_per_section: NZU64!(10),
4548 compression: None,
4549 codec_config: (),
4550 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4551 write_buffer: NZUsize!(1024),
4552 replay_buffer: NZUsize!(1024),
4553 };
4554
4555 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4556 .await
4557 .unwrap();
4558
4559 for i in 0..11u64 {
4561 journal.test_append_data(0, i * 100).await.unwrap();
4562 }
4563 journal.0.blobs.start_sync().await.await.unwrap();
4566 journal.test_append_data(1, 9999).await.unwrap();
4567 drop(journal);
4569
4570 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
4571 assert!(matches!(result, Err(Error::Corruption(_))));
4572 });
4573 }
4574
4575 #[test_traced]
4576 fn test_variable_recovery_preserves_rolled_predecessors_without_commit() {
4577 let executor = deterministic::Runner::default();
4578 executor.start(|context| async move {
4579 let cfg = Config::<()> {
4580 partition: "recovery-empty-data-tail".into(),
4581 items_per_section: NZU64!(1),
4582 compression: None,
4583 codec_config: (),
4584 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4585 write_buffer: NZUsize!(1024),
4586 replay_buffer: NZUsize!(1024),
4587 };
4588 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
4589 .await
4590 .unwrap();
4591
4592 let appended;
4596 (journal, appended) = journal.append(&10).await.unwrap();
4597 assert_eq!(appended, 0);
4598 journal = journal.sync().await.unwrap();
4599 let appended;
4600 (journal, appended) = journal.append(&20).await.unwrap();
4601 assert_eq!(appended, 1);
4602 let appended;
4603 (journal, appended) = journal.append(&30).await.unwrap();
4604 assert_eq!(appended, 2);
4605 drop(journal);
4606
4607 let data_partition = cfg.data_partition();
4608 let mut data_blobs = context.scan(&data_partition).await.unwrap();
4609 data_blobs.sort();
4610 assert_eq!(data_blobs.len(), 4);
4611 for name in &data_blobs[..3] {
4612 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
4613 assert!(size > 0);
4614 }
4615 let (_blob, size) = context.open(&data_partition, &data_blobs[3]).await.unwrap();
4616 assert_eq!(size, 0);
4617
4618 let cfg = Config {
4620 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4621 ..cfg
4622 };
4623 let mut journal = Journal::<_, u64>::init(context.child("recovered"), cfg.clone())
4624 .await
4625 .unwrap();
4626 assert_eq!(journal.bounds(), 0..3);
4627 assert_eq!(journal.read(0).await.unwrap(), 10);
4628 assert_eq!(journal.read(1).await.unwrap(), 20);
4629 assert_eq!(journal.read(2).await.unwrap(), 30);
4630 let appended;
4631 (journal, appended) = journal.append(&42).await.unwrap();
4632 assert_eq!(appended, 3);
4633 assert_eq!(journal.read(3).await.unwrap(), 42);
4634 drop(journal);
4635
4636 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
4638 assert_eq!(data_blobs.len(), 5);
4639
4640 let journal = Journal::<_, u64>::init(context.child("recovered"), cfg)
4641 .await
4642 .unwrap();
4643 journal.destroy().await.unwrap();
4644 });
4645 }
4646
4647 #[test_traced]
4648 fn test_variable_recovery_preserves_first_rollover_without_commit() {
4649 let executor = deterministic::Runner::default();
4650 executor.start(|context| async move {
4651 let cfg = Config::<()> {
4652 partition: "recovery-empty-data-no-items".into(),
4653 items_per_section: NZU64!(1),
4654 compression: None,
4655 codec_config: (),
4656 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4657 write_buffer: NZUsize!(1024),
4658 replay_buffer: NZUsize!(1024),
4659 };
4660 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
4661 .await
4662 .unwrap();
4663
4664 let appended;
4667 (journal, appended) = journal.append(&10).await.unwrap();
4668 assert_eq!(appended, 0);
4669 let appended;
4670 (journal, appended) = journal.append(&20).await.unwrap();
4671 assert_eq!(appended, 1);
4672 drop(journal);
4673
4674 let data_partition = cfg.data_partition();
4675 let mut data_blobs = context.scan(&data_partition).await.unwrap();
4676 data_blobs.sort();
4677 assert_eq!(data_blobs.len(), 3);
4678 for name in &data_blobs[..2] {
4679 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
4680 assert!(size > 0);
4681 }
4682 let (_blob, size) = context.open(&data_partition, &data_blobs[2]).await.unwrap();
4683 assert_eq!(size, 0);
4684
4685 let cfg = Config {
4686 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
4687 ..cfg
4688 };
4689 let mut journal = Journal::<_, u64>::init(context.child("recovered"), cfg)
4690 .await
4691 .unwrap();
4692 assert_eq!(journal.bounds(), 0..2);
4693 let appended;
4694 (journal, appended) = journal.append(&42).await.unwrap();
4695 assert_eq!(appended, 2);
4696 assert_eq!(journal.read(2).await.unwrap(), 42);
4697 journal.destroy().await.unwrap();
4698 });
4699 }
4700
4701 #[test_traced]
4705 fn test_variable_recovery_truncates_torn_interior_page() {
4706 let executor = deterministic::Runner::default();
4707 executor.start(|context| async move {
4708 let cfg = Config::<()> {
4709 partition: "variable-torn-interior".into(),
4710 items_per_section: NZU64!(30),
4711 compression: None,
4712 codec_config: (),
4713 page_cache: CacheRef::from_pooler(&context, NZU16!(64), NZUsize!(10)),
4714 write_buffer: NZUsize!(2048),
4715 replay_buffer: NZUsize!(2048),
4716 };
4717 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4718 .await
4719 .unwrap();
4720 for i in 0..33u64 {
4721 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4722 }
4723 journal.commit().await.unwrap();
4724
4725 corrupt_page(&context, &cfg.data_partition(), &0u64.to_be_bytes(), 2, 64).await;
4727
4728 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
4731 .await
4732 .unwrap();
4733 assert_eq!(journal.bounds(), 0..14);
4734 for i in 0..14u64 {
4735 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4736 }
4737 let appended;
4738 (journal, appended) = journal.append(&4242).await.unwrap();
4739 assert_eq!(appended, 14);
4740 journal.destroy().await.unwrap();
4741 });
4742 }
4743
4744 #[test_traced]
4747 fn test_variable_recovery_truncates_torn_interior_page_in_tail() {
4748 let executor = deterministic::Runner::default();
4749 executor.start(|context| async move {
4750 let cfg = Config::<()> {
4751 partition: "variable-torn-interior-tail".into(),
4752 items_per_section: NZU64!(30),
4753 compression: None,
4754 codec_config: (),
4755 page_cache: CacheRef::from_pooler(&context, NZU16!(64), NZUsize!(10)),
4756 write_buffer: NZUsize!(2048),
4757 replay_buffer: NZUsize!(2048),
4758 };
4759 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4760 .await
4761 .unwrap();
4762 for i in 0..50u64 {
4763 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4764 }
4765 journal.commit().await.unwrap();
4766
4767 corrupt_page(&context, &cfg.data_partition(), &1u64.to_be_bytes(), 1, 64).await;
4769
4770 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
4772 .await
4773 .unwrap();
4774 assert_eq!(journal.bounds(), 0..37);
4775 for i in 0..37u64 {
4776 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4777 }
4778 let appended;
4779 (journal, appended) = journal.append(&4242).await.unwrap();
4780 assert_eq!(appended, 37);
4781 journal.destroy().await.unwrap();
4782 });
4783 }
4784
4785 #[test_traced]
4790 fn test_variable_recovery_adopts_torn_page_below_watermark() {
4791 let executor = deterministic::Runner::default();
4792 executor.start(|context| async move {
4793 let cfg = Config::<()> {
4794 partition: "variable-torn-below-watermark".into(),
4795 items_per_section: NZU64!(30),
4796 compression: None,
4797 codec_config: (),
4798 page_cache: CacheRef::from_pooler(&context, NZU16!(64), NZUsize!(10)),
4799 write_buffer: NZUsize!(2048),
4800 replay_buffer: NZUsize!(2048),
4801 };
4802 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4803 .await
4804 .unwrap();
4805 for i in 0..33u64 {
4806 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4807 }
4808 journal.sync().await.unwrap();
4810
4811 corrupt_page(&context, &cfg.data_partition(), &0u64.to_be_bytes(), 2, 64).await;
4813 let (_, size_before) = context
4814 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4815 .await
4816 .unwrap();
4817
4818 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
4822 .await
4823 .expect("acknowledged damage must not fail recovery");
4824 let (_, size_after) = context
4825 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4826 .await
4827 .unwrap();
4828 assert_eq!(
4829 size_after, size_before,
4830 "adoption must preserve the evidence"
4831 );
4832 let mut damaged = 0;
4833 for i in 0..30u64 {
4834 match journal.read(i).await {
4835 Ok(item) => assert_eq!(item, i * 100),
4836 Err(_) => damaged += 1,
4837 }
4838 }
4839 assert!(damaged > 0, "the torn page must surface as read errors");
4840 for i in 30..33u64 {
4841 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4842 }
4843 drop(journal);
4844
4845 let _ = Journal::<_, u64>::init(context.child("third"), cfg.clone())
4847 .await
4848 .unwrap();
4849 let (_, size_retry) = context
4850 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4851 .await
4852 .unwrap();
4853 assert_eq!(size_retry, size_before);
4854 });
4855 }
4856
4857 #[test_traced]
4862 fn test_variable_recovery_adopts_shortened_blob_below_watermark() {
4863 let executor = deterministic::Runner::default();
4864 executor.start(|context| async move {
4865 let cfg = Config::<()> {
4866 partition: "variable-short-below-watermark".into(),
4867 items_per_section: NZU64!(30),
4868 compression: None,
4869 codec_config: (),
4870 page_cache: CacheRef::from_pooler(&context, NZU16!(64), NZUsize!(10)),
4871 write_buffer: NZUsize!(2048),
4872 replay_buffer: NZUsize!(2048),
4873 };
4874 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4875 .await
4876 .unwrap();
4877 for i in 0..33u64 {
4878 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4879 }
4880 journal.sync().await.unwrap();
4881
4882 let physical_page_size = 64 + 12;
4885 let (blob, size) = context
4886 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4887 .await
4888 .unwrap();
4889 assert!(size > 2 * physical_page_size);
4890 blob.resize(2 * physical_page_size).await.unwrap();
4891 blob.sync().await.unwrap();
4892
4893 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
4894 .await
4895 .unwrap();
4896 assert_eq!(journal.size(), 33);
4897 for i in 0..14u64 {
4898 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4899 }
4900 assert!(journal.read(20).await.is_err());
4901 for i in 30..33u64 {
4902 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4903 }
4904 });
4905 }
4906
4907 #[test_traced]
4911 fn test_variable_recovery_adopts_shortened_old_blob_below_watermark() {
4912 let executor = deterministic::Runner::default();
4913 executor.start(|context| async move {
4914 let cfg = Config::<()> {
4915 partition: "variable-short-old-below-watermark".into(),
4916 items_per_section: NZU64!(10),
4917 compression: None,
4918 codec_config: (),
4919 page_cache: CacheRef::from_pooler(&context, NZU16!(64), NZUsize!(10)),
4920 write_buffer: NZUsize!(2048),
4921 replay_buffer: NZUsize!(2048),
4922 };
4923 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4924 .await
4925 .unwrap();
4926 for i in 0..25u64 {
4927 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4928 }
4929 journal.sync().await.unwrap();
4930
4931 let physical_page_size = 64 + 12;
4935 let (blob, size) = context
4936 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4937 .await
4938 .unwrap();
4939 assert!(size > physical_page_size);
4940 blob.resize(physical_page_size).await.unwrap();
4941 blob.sync().await.unwrap();
4942
4943 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
4944 .await
4945 .unwrap();
4946 assert_eq!(journal.size(), 25);
4947 for i in 0..7u64 {
4948 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4949 }
4950 assert!(journal.read(8).await.is_err());
4951 for i in 10..25u64 {
4952 assert_eq!(journal.read(i).await.unwrap(), i * 100);
4953 }
4954 });
4955 }
4956
4957 #[test_traced]
4960 fn test_variable_recovery_rejects_torn_page_below_mid_blob_watermark() {
4961 let executor = deterministic::Runner::default();
4962 executor.start(|context| async move {
4963 let cfg = Config::<()> {
4964 partition: "variable-torn-mid-blob-watermark".into(),
4965 items_per_section: NZU64!(30),
4966 compression: None,
4967 codec_config: (),
4968 page_cache: CacheRef::from_pooler(&context, NZU16!(64), NZUsize!(10)),
4969 write_buffer: NZUsize!(2048),
4970 replay_buffer: NZUsize!(2048),
4971 };
4972 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
4973 .await
4974 .unwrap();
4975 for i in 0..20u64 {
4976 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4977 }
4978 journal.sync().await.unwrap();
4980
4981 corrupt_page(&context, &cfg.data_partition(), &0u64.to_be_bytes(), 1, 64).await;
4983 let (_, size_before) = context
4984 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4985 .await
4986 .unwrap();
4987
4988 for child in ["second", "retry"] {
4989 let result = Journal::<_, u64>::init(context.child(child), cfg.clone()).await;
4990 assert!(matches!(result, Err(Error::Corruption(_))));
4991 }
4992
4993 let (_, size_after) = context
4995 .open(&cfg.data_partition(), &0u64.to_be_bytes())
4996 .await
4997 .unwrap();
4998 assert_eq!(size_after, size_before);
4999 });
5000 }
5001
5002 #[test_traced]
5010 fn test_variable_recovery_rolls_back_durable_blob_after_gap() {
5011 let executor = deterministic::Runner::default();
5012 executor.start(|context| async move {
5013 let cfg = Config {
5014 partition: "recovery-rollback-after-gap".into(),
5015 items_per_section: NZU64!(10),
5016 compression: None,
5017 codec_config: (),
5018 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5019 write_buffer: NZUsize!(1024),
5020 replay_buffer: NZUsize!(1024),
5021 };
5022
5023 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5024 .await
5025 .unwrap();
5026
5027 for i in 0..10u64 {
5029 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5030 }
5031 journal = journal.sync().await.unwrap();
5032
5033 for i in 10..30u64 {
5036 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5037 }
5038 journal.test_sync_data_blob(2).await.unwrap();
5039 drop(journal);
5040 let data_partition = cfg.data_partition();
5041 let (blob, _) = context
5042 .open(&data_partition, &1u64.to_be_bytes())
5043 .await
5044 .unwrap();
5045 blob.resize(0).await.unwrap();
5046 blob.sync().await.unwrap();
5047
5048 let mut names = context.scan(&data_partition).await.unwrap();
5051 names.sort();
5052 assert_eq!(names.len(), 4);
5053 let sizes = {
5054 let mut sizes = Vec::new();
5055 for name in &names {
5056 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
5057 sizes.push(size);
5058 }
5059 sizes
5060 };
5061 assert!(sizes[0] > 0, "blob 0 should be durable");
5062 assert_eq!(sizes[1], 0, "blob 1 should be the gap");
5063 assert!(sizes[2] > 0, "blob 2 should be incidentally durable");
5064 assert_eq!(sizes[3], 0, "blob 3 should be the empty tail");
5065
5066 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5069 .await
5070 .unwrap();
5071 assert_eq!(journal.bounds(), 0..10);
5072 for i in 0..10u64 {
5073 assert_eq!(journal.read(i).await.unwrap(), i * 100);
5074 }
5075 assert!(matches!(
5076 journal.read(10).await,
5077 Err(Error::ItemOutOfRange(10))
5078 ));
5079
5080 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
5083 assert_eq!(data_blobs.len(), 2);
5084
5085 let appended;
5087 (journal, appended) = journal.append(&1234).await.unwrap();
5088 assert_eq!(appended, 10);
5089 assert_eq!(journal.read(10).await.unwrap(), 1234);
5090
5091 journal.destroy().await.unwrap();
5092 });
5093 }
5094
5095 #[test_traced]
5105 fn test_variable_recovery_empty_oldest_blob_orphaned_newer_blob() {
5106 let executor = deterministic::Runner::default();
5107 executor.start(|context| async move {
5108 let cfg = Config {
5109 partition: "recovery-empty-oldest-blob".into(),
5110 items_per_section: NZU64!(10),
5111 compression: None,
5112 codec_config: (),
5113 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5114 write_buffer: NZUsize!(1024),
5115 replay_buffer: NZUsize!(1024),
5116 };
5117
5118 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5120 .await
5121 .unwrap();
5122 for i in 0..20u64 {
5123 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5124 }
5125 let journal = journal.sync().await.unwrap();
5126 drop(journal);
5127
5128 let data_partition = cfg.data_partition();
5131 let mut names = context.scan(&data_partition).await.unwrap();
5132 names.sort();
5133 assert_eq!(names.len(), 3);
5134 let (blob0, size0) = context.open(&data_partition, &names[0]).await.unwrap();
5135 assert!(size0 > 0, "blob 0 should start durable");
5136 blob0.resize(0).await.unwrap();
5137 blob0.sync().await.unwrap();
5138 context
5139 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
5140 .await
5141 .unwrap();
5142 context
5143 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
5144 .await
5145 .unwrap();
5146
5147 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5149 .await
5150 .unwrap();
5151 assert_eq!(journal.bounds(), 0..0);
5152 assert!(matches!(
5153 journal.read(0).await,
5154 Err(Error::ItemOutOfRange(0))
5155 ));
5156
5157 let appended;
5159 (journal, appended) = journal.append(&42).await.unwrap();
5160 assert_eq!(appended, 0);
5161 assert_eq!(journal.read(0).await.unwrap(), 42);
5162 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
5163 assert_eq!(
5164 data_blobs.len(),
5165 1,
5166 "orphaned newer blob should be truncated away"
5167 );
5168
5169 journal.destroy().await.unwrap();
5170 });
5171 }
5172
5173 #[test_traced]
5179 fn test_variable_recovery_clean_short_oldest_blob_orphaned_newer_blob() {
5180 let executor = deterministic::Runner::default();
5181 executor.start(|context| async move {
5182 let cfg = Config {
5183 partition: "recovery-clean-short-oldest-blob".into(),
5184 items_per_section: NZU64!(64),
5185 compression: None,
5186 codec_config: (),
5187 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5188 write_buffer: NZUsize!(1024),
5189 replay_buffer: NZUsize!(1024),
5190 };
5191
5192 let mut journal =
5195 Journal::<_, FixedBytes<31>>::init(context.child("first"), cfg.clone())
5196 .await
5197 .unwrap();
5198 for i in 0..128u8 {
5199 (journal, _) = journal.append(&FixedBytes::new([i; 31])).await.unwrap();
5200 }
5201 let journal = journal.sync().await.unwrap();
5202 drop(journal);
5203
5204 let physical_page_size = LARGE_PAGE_SIZE.get() as u64 + 12;
5205 let items_in_page = LARGE_PAGE_SIZE.get() as u64 / 32;
5206 assert!(items_in_page < cfg.items_per_section.get());
5207
5208 let data_partition = cfg.data_partition();
5209 let mut names = context.scan(&data_partition).await.unwrap();
5210 names.sort();
5211 assert_eq!(names.len(), 3);
5212
5213 let (blob0, size0) = context.open(&data_partition, &names[0]).await.unwrap();
5216 assert!(size0 > physical_page_size);
5217 blob0.resize(physical_page_size).await.unwrap();
5218 blob0.sync().await.unwrap();
5219
5220 context
5223 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
5224 .await
5225 .unwrap();
5226 context
5227 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
5228 .await
5229 .unwrap();
5230
5231 let mut journal =
5234 Journal::<_, FixedBytes<31>>::init(context.child("second"), cfg.clone())
5235 .await
5236 .unwrap();
5237 assert_eq!(journal.bounds(), 0..items_in_page);
5238 assert_eq!(
5239 journal.read(items_in_page - 1).await.unwrap(),
5240 FixedBytes::new([(items_in_page - 1) as u8; 31])
5241 );
5242 assert!(matches!(
5243 journal.read(items_in_page).await,
5244 Err(Error::ItemOutOfRange(pos)) if pos == items_in_page
5245 ));
5246
5247 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
5248 assert_eq!(
5249 data_blobs.len(),
5250 1,
5251 "orphaned newer blob should be truncated away"
5252 );
5253
5254 let pos;
5256 (journal, pos) = journal.append(&FixedBytes::new([42; 31])).await.unwrap();
5257 assert_eq!(pos, items_in_page);
5258 assert_eq!(
5259 journal.read(items_in_page).await.unwrap(),
5260 FixedBytes::new([42; 31])
5261 );
5262
5263 journal.destroy().await.unwrap();
5264 });
5265 }
5266
5267 #[test_traced]
5271 fn test_variable_recovery_unsynced_tail_keeps_contiguous_prefix() {
5272 let executor = deterministic::Runner::default();
5273 executor.start(|context| async move {
5274 let cfg = Config {
5275 partition: "recovery-partial-sync-loop".into(),
5276 items_per_section: NZU64!(10),
5277 compression: None,
5278 codec_config: (),
5279 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5280 write_buffer: NZUsize!(1024),
5281 replay_buffer: NZUsize!(1024),
5282 };
5283
5284 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5285 .await
5286 .unwrap();
5287
5288 for i in 0..25u64 {
5291 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5292 }
5293
5294 journal.test_sync_data_blob(0).await.unwrap();
5298 journal.test_sync_data_blob(1).await.unwrap();
5299 drop(journal);
5300
5301 let data_partition = cfg.data_partition();
5304 let mut names = context.scan(&data_partition).await.unwrap();
5305 names.sort();
5306 assert_eq!(names.len(), 3);
5307 for (blob, name) in names.iter().enumerate() {
5308 let (_blob, size) = context.open(&data_partition, name).await.unwrap();
5309 if blob < 2 {
5310 assert!(size > 0, "blob {blob} should be durable");
5311 } else {
5312 assert_eq!(size, 0, "blob {blob} should be empty");
5313 }
5314 }
5315
5316 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5319 .await
5320 .unwrap();
5321 assert_eq!(journal.bounds(), 0..20);
5322 for i in 0..20u64 {
5323 assert_eq!(journal.read(i).await.unwrap(), i * 100);
5324 }
5325 assert!(matches!(
5326 journal.read(20).await,
5327 Err(Error::ItemOutOfRange(20))
5328 ));
5329
5330 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
5333 assert_eq!(data_blobs.len(), 3);
5334 let appended;
5335 (journal, appended) = journal.append(&2000).await.unwrap();
5336 assert_eq!(appended, 20);
5337 assert_eq!(journal.read(20).await.unwrap(), 2000);
5338
5339 journal.destroy().await.unwrap();
5340 });
5341 }
5342
5343 #[test_traced]
5345 fn test_variable_recovery_multiple_prunes_crash() {
5346 let executor = deterministic::Runner::default();
5347 executor.start(|context| async move {
5348 let cfg = Config {
5350 partition: "recovery-multiple-prunes".into(),
5351 items_per_section: NZU64!(10),
5352 compression: None,
5353 codec_config: (),
5354 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5355 write_buffer: NZUsize!(1024),
5356 replay_buffer: NZUsize!(1024),
5357 };
5358
5359 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5360 .await
5361 .unwrap();
5362
5363 for i in 0..50u64 {
5365 (variable, _) = variable.append(&(i * 100)).await.unwrap();
5366 }
5367
5368 let (mut variable, _) = variable.prune(10).await.unwrap();
5370 assert_eq!(variable.bounds().start, 10);
5371
5372 variable.test_prune_data(3).await.unwrap();
5375 variable.sync().await.unwrap();
5378
5379 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5381 .await
5382 .unwrap();
5383
5384 let bounds = variable.bounds();
5386 assert_eq!(bounds.start, 30);
5387 assert_eq!(bounds.end, 50);
5388
5389 assert!(matches!(
5391 variable.read(10).await,
5392 Err(crate::journal::Error::ItemPruned(_))
5393 ));
5394 assert!(matches!(
5395 variable.read(20).await,
5396 Err(crate::journal::Error::ItemPruned(_))
5397 ));
5398
5399 assert_eq!(variable.read(30).await.unwrap(), 3000);
5401 assert_eq!(variable.read(49).await.unwrap(), 4900);
5402
5403 variable.destroy().await.unwrap();
5404 });
5405 }
5406
5407 #[test_traced]
5413 fn test_variable_recovery_offsets_behind_data_multi_blob() {
5414 let executor = deterministic::Runner::default();
5415 executor.start(|context| async move {
5416 let cfg = Config {
5418 partition: "recovery-rewind-crash".into(),
5419 items_per_section: NZU64!(10),
5420 compression: None,
5421 codec_config: (),
5422 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5423 write_buffer: NZUsize!(1024),
5424 replay_buffer: NZUsize!(1024),
5425 };
5426
5427 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5428 .await
5429 .unwrap();
5430
5431 for i in 0..25u64 {
5433 (variable, _) = variable.append(&(i * 100)).await.unwrap();
5434 }
5435
5436 assert_eq!(variable.size(), 25);
5437
5438 let variable = variable.test_rewind_offsets(5).await.unwrap();
5440
5441 variable.sync().await.unwrap();
5442
5443 let mut variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5445 .await
5446 .unwrap();
5447
5448 let bounds = variable.bounds();
5450 assert_eq!(bounds.end, 25);
5451 assert_eq!(bounds.start, 0);
5452
5453 for i in 0..25u64 {
5455 assert_eq!(variable.read(i).await.unwrap(), i * 100);
5456 }
5457
5458 assert_eq!(variable.test_offsets_size(), 25);
5460
5461 let pos;
5463 (variable, pos) = variable.append(&2500).await.unwrap();
5464 assert_eq!(pos, 25);
5465 assert_eq!(variable.read(25).await.unwrap(), 2500);
5466
5467 variable.destroy().await.unwrap();
5468 });
5469 }
5470
5471 #[test_traced]
5472 fn test_variable_rebuild_offsets_rejects_anchor_outside_bounds() {
5473 let executor = deterministic::Runner::default();
5474 executor.start(|context| async move {
5475 let offsets_cfg = fixed::Config {
5476 partition: "rebuild-anchor-outside-offsets".into(),
5477 items_per_blob: NZU64!(10),
5478 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5479 write_buffer: NZUsize!(1024),
5480 replay_buffer: NZUsize!(1024),
5481 };
5482
5483 let partition = Partition::new(
5484 context.child("data"),
5485 "rebuild-anchor-outside-data".into(),
5486 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5487 NZUsize!(1024),
5488 );
5489 let mut pending = BTreeMap::new();
5490 pending.insert(0, partition.open(0).await.unwrap());
5491 let mut offsets = fixed::Inner::<_, u64>::init(context.child("offsets"), offsets_cfg)
5492 .await
5493 .unwrap();
5494
5495 let mut encoded = Vec::new();
5496 encode_frame_into(None, &100u64, &mut encoded).unwrap();
5497 pending.get_mut(&0).unwrap().append(&encoded).await.unwrap();
5498 offsets.append(&0).await.unwrap();
5499
5500 let result = Inner::<_, u64>::rebuild_offsets_from_anchor(
5501 &partition,
5502 &mut pending,
5503 Box::new(offsets),
5504 10,
5505 2,
5506 NZUsize!(1024),
5507 &(),
5508 false,
5509 )
5510 .await;
5511 assert!(matches!(result, Err(Error::Corruption(_))));
5512
5513 drop(pending);
5514 Partition::<deterministic::Context>::remove_all(
5515 &context,
5516 "rebuild-anchor-outside-data",
5517 )
5518 .await
5519 .unwrap();
5520 });
5521 }
5522
5523 #[test_traced]
5524 fn test_variable_recovery_rejects_watermark_beyond_retained_data() {
5525 let executor = deterministic::Runner::default();
5526 executor.start(|context| async move {
5527 let cfg = Config {
5528 partition: "recovery-anchor-too-far".into(),
5529 items_per_section: NZU64!(10),
5530 compression: None,
5531 codec_config: (),
5532 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5533 write_buffer: NZUsize!(1024),
5534 replay_buffer: NZUsize!(1024),
5535 };
5536
5537 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5538 .await
5539 .unwrap();
5540
5541 for i in 0..20u64 {
5542 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5543 }
5544 let journal = journal.sync().await.unwrap();
5545
5546 let mut journal = journal
5549 .test_set_offsets_recovery_watermark(15)
5550 .await
5551 .unwrap();
5552 journal.test_rewind_data_to_position(12).await.unwrap();
5553 journal.0.blobs.start_sync().await.await.unwrap();
5554 drop(journal);
5555
5556 for child in ["second", "retry"] {
5558 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
5559 Err(Error::Corruption(message)) => assert_eq!(
5560 message,
5561 "offsets recovery watermark 15 exceeds retained data end 12 \
5562 (offsets bounds 0..20)"
5563 ),
5564 Err(error) => panic!("unexpected error: {error}"),
5565 Ok(_) => panic!("missing acknowledged data was accepted"),
5566 }
5567 }
5568 });
5569 }
5570
5571 #[test_traced]
5572 fn test_variable_recovery_rejects_missing_data_below_watermark() {
5573 let executor = deterministic::Runner::default();
5574 executor.start(|context| async move {
5575 let cfg = Config {
5576 partition: "recovery-short-middle-retry".into(),
5577 items_per_section: NZU64!(10),
5578 compression: None,
5579 codec_config: (),
5580 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5581 write_buffer: NZUsize!(1024),
5582 replay_buffer: NZUsize!(1024),
5583 };
5584
5585 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5586 .await
5587 .unwrap();
5588
5589 for i in 0..30u64 {
5590 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5591 }
5592 let journal = journal.sync().await.unwrap();
5593
5594 let mut journal = journal
5597 .test_set_offsets_recovery_watermark(15)
5598 .await
5599 .unwrap();
5600 journal.test_rewind_data_to_position(12).await.unwrap();
5601 journal.0.blobs.start_sync().await.await.unwrap();
5602 journal.test_append_data(2, 9999).await.unwrap();
5603 journal.0.blobs.start_sync().await.await.unwrap();
5604 drop(journal);
5605
5606 for child in ["second", "retry"] {
5610 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
5611 Err(Error::Corruption(message)) => assert_eq!(
5612 message,
5613 "data blobs shorter than offsets recovery watermark 15"
5614 ),
5615 Err(error) => panic!("unexpected error: {error}"),
5616 Ok(_) => panic!("missing acknowledged data was accepted"),
5617 }
5618 }
5619
5620 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
5621 assert_eq!(
5622 data_blobs.len(),
5623 3,
5624 "corruption evidence should not be removed"
5625 );
5626 });
5627 }
5628
5629 #[test_traced]
5630 fn test_variable_rewind_commit_reopen() {
5631 let executor = deterministic::Runner::default();
5632 executor.start(|context| async move {
5633 let cfg = Config {
5634 partition: "rewind-commit-reopen".into(),
5635 items_per_section: NZU64!(10),
5636 compression: None,
5637 codec_config: (),
5638 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5639 write_buffer: NZUsize!(1024),
5640 replay_buffer: NZUsize!(1024),
5641 };
5642
5643 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5644 .await
5645 .unwrap();
5646
5647 for i in 0..25u64 {
5648 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5649 }
5650 let journal = journal.sync().await.unwrap();
5651
5652 let journal = journal.rewind(12).await.unwrap();
5653 journal.commit().await.unwrap();
5654
5655 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5656 .await
5657 .unwrap();
5658 assert_eq!(journal.bounds(), 0..12);
5659 for i in 0..12u64 {
5660 assert_eq!(journal.read(i).await.unwrap(), i * 100);
5661 }
5662 assert!(matches!(
5663 journal.read(12).await,
5664 Err(Error::ItemOutOfRange(12))
5665 ));
5666
5667 journal.destroy().await.unwrap();
5668 });
5669 }
5670
5671 #[test_traced]
5672 fn test_variable_recovery_rejects_synced_data_rewind_to_boundary() {
5673 let executor = deterministic::Runner::default();
5674 executor.start(|context| async move {
5675 let cfg = Config {
5676 partition: "recovery-boundary-data-rewind".into(),
5677 items_per_section: NZU64!(10),
5678 compression: None,
5679 codec_config: (),
5680 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5681 write_buffer: NZUsize!(1024),
5682 replay_buffer: NZUsize!(1024),
5683 };
5684
5685 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5686 .await
5687 .unwrap();
5688
5689 for i in 0..20u64 {
5690 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5691 }
5692 let mut journal = journal.sync().await.unwrap();
5693
5694 journal.test_rewind_data_to_position(10).await.unwrap();
5695 journal.0.blobs.start_sync().await.await.unwrap();
5696 drop(journal);
5697
5698 for child in ["second", "retry"] {
5703 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
5704 Err(Error::Corruption(message)) => assert_eq!(
5705 message,
5706 "offsets recovery watermark 20 exceeds retained data end 10 (offsets \
5707 bounds 0..20)"
5708 ),
5709 Err(error) => panic!("unexpected error: {error}"),
5710 Ok(_) => panic!("missing acknowledged data was accepted"),
5711 }
5712 }
5713 });
5714 }
5715
5716 #[test_traced]
5717 fn test_variable_recovery_truncates_short_data_blob_after_anchor() {
5718 let executor = deterministic::Runner::default();
5719 executor.start(|context| async move {
5720 let cfg = Config {
5721 partition: "recovery-short-blob-after-anchor".into(),
5722 items_per_section: NZU64!(10),
5723 compression: None,
5724 codec_config: (),
5725 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5726 write_buffer: NZUsize!(1024),
5727 replay_buffer: NZUsize!(1024),
5728 };
5729
5730 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5731 .await
5732 .unwrap();
5733
5734 for i in 0..25u64 {
5735 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5736 }
5737 journal = journal.sync().await.unwrap();
5738
5739 let mut journal = journal
5743 .test_set_offsets_recovery_watermark(10)
5744 .await
5745 .unwrap();
5746 let offset = {
5747 let offsets = journal.0.offsets.snapshot().await.unwrap();
5748 offsets.read(12).await.unwrap()
5749 };
5750 drop(journal);
5751
5752 let (blob, size) = context
5754 .open(&cfg.data_partition(), &1u64.to_be_bytes())
5755 .await
5756 .unwrap();
5757 let mut writer = Writer::new(blob, size, 1024, cfg.page_cache.clone())
5758 .await
5759 .unwrap();
5760 writer.resize(offset).await.unwrap();
5761 writer.sync().await.unwrap();
5762 drop(writer);
5763
5764 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5765 .await
5766 .unwrap();
5767 assert_eq!(journal.bounds(), 0..12);
5768 assert_eq!(journal.test_offsets_size(), 12);
5769 for i in 0..12u64 {
5770 assert_eq!(journal.read(i).await.unwrap(), i * 100);
5771 }
5772 assert!(matches!(
5773 journal.read(12).await,
5774 Err(Error::ItemOutOfRange(12))
5775 ));
5776
5777 journal.destroy().await.unwrap();
5778 });
5779 }
5780
5781 #[test_traced]
5782 fn test_variable_init_persists_offsets_trailing_item_repair() {
5783 let executor = deterministic::Runner::default();
5784 let ((offsets_blob_partition, expected_size), checkpoint) =
5785 executor.start_and_recover(|context| async move {
5786 let cfg = Config {
5787 partition: "offsets-init-repair-sync".into(),
5788 items_per_section: NZU64!(10),
5789 compression: None,
5790 codec_config: (),
5791 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5792 write_buffer: NZUsize!(1024),
5793 replay_buffer: NZUsize!(1024),
5794 };
5795 let offsets_blob_partition = format!("{}-blobs", cfg.offsets_partition());
5796 let expected_size = 2 * std::mem::size_of::<u64>() as u64;
5797
5798 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5799 .await
5800 .unwrap();
5801 (journal, _) = journal.append(&10).await.unwrap();
5802 (journal, _) = journal.append(&20).await.unwrap();
5803 let journal = journal.sync().await.unwrap();
5804 drop(journal);
5805
5806 let (blob, raw_size) = context
5807 .open(&offsets_blob_partition, &0u64.to_be_bytes())
5808 .await
5809 .unwrap();
5810 let mut append = Writer::new(
5811 blob,
5812 raw_size,
5813 2048,
5814 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5815 )
5816 .await
5817 .unwrap();
5818 assert_eq!(append.size(), expected_size);
5819 append.resize(expected_size + 1).await.unwrap();
5820 append.sync().await.unwrap();
5821 drop(append);
5822
5823 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
5824 .await
5825 .unwrap();
5826 assert_eq!(journal.bounds(), 0..2);
5827 drop(journal);
5828
5829 (offsets_blob_partition, expected_size)
5830 });
5831
5832 deterministic::Runner::from(checkpoint).start(move |context| async move {
5833 let (blob, raw_size) = context
5834 .open(&offsets_blob_partition, &0u64.to_be_bytes())
5835 .await
5836 .unwrap();
5837 let append = Writer::new(
5838 blob,
5839 raw_size,
5840 2048,
5841 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5842 )
5843 .await
5844 .unwrap();
5845 assert_eq!(append.size(), expected_size);
5846 });
5847 }
5848
5849 #[test_traced]
5850 fn test_variable_init_persists_data_tail_repair() {
5851 let executor = deterministic::Runner::default();
5852 let ((data_partition, expected_size), checkpoint) =
5853 executor.start_and_recover(|context| async move {
5854 let cfg = Config {
5855 partition: "data-init-repair-sync".into(),
5856 items_per_section: NZU64!(10),
5857 compression: None,
5858 codec_config: (),
5859 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5860 write_buffer: NZUsize!(1024),
5861 replay_buffer: NZUsize!(1024),
5862 };
5863 let data_partition = cfg.data_partition();
5864
5865 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5866 .await
5867 .unwrap();
5868 (journal, _) = journal.append(&10).await.unwrap();
5869 (journal, _) = journal.append(&20).await.unwrap();
5870 let journal = journal.sync().await.unwrap();
5871 drop(journal);
5872
5873 let (blob, raw_size) = context
5874 .open(&data_partition, &0u64.to_be_bytes())
5875 .await
5876 .unwrap();
5877 let mut append = Writer::new(
5878 blob,
5879 raw_size,
5880 2048,
5881 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5882 )
5883 .await
5884 .unwrap();
5885 let expected_size = append.size();
5886 append.append(&[0xFF, 0xFF]).await.unwrap();
5887 append.sync().await.unwrap();
5888 drop(append);
5889
5890 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
5891 .await
5892 .unwrap();
5893 assert_eq!(journal.bounds(), 0..2);
5894 drop(journal);
5895
5896 (data_partition, expected_size)
5897 });
5898
5899 deterministic::Runner::from(checkpoint).start(move |context| async move {
5900 let (blob, raw_size) = context
5901 .open(&data_partition, &0u64.to_be_bytes())
5902 .await
5903 .unwrap();
5904 let append = Writer::new(
5905 blob,
5906 raw_size,
5907 2048,
5908 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5909 )
5910 .await
5911 .unwrap();
5912 assert_eq!(append.size(), expected_size);
5913 });
5914 }
5915
5916 #[test_traced]
5919 fn test_variable_recovery_empty_offsets_after_prune_and_append() {
5920 let executor = deterministic::Runner::default();
5921 executor.start(|context| async move {
5922 let cfg = Config {
5923 partition: "recovery-empty-after-prune".into(),
5924 items_per_section: NZU64!(10),
5925 compression: None,
5926 codec_config: (),
5927 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5928 write_buffer: NZUsize!(1024),
5929 replay_buffer: NZUsize!(1024),
5930 };
5931
5932 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5934 .await
5935 .unwrap();
5936
5937 for i in 0..10u64 {
5939 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5940 }
5941 let bounds = journal.bounds();
5942 assert_eq!(bounds.end, 10);
5943 assert_eq!(bounds.start, 0);
5944
5945 (journal, _) = journal.prune(10).await.unwrap();
5947 let bounds = journal.bounds();
5948 assert_eq!(bounds.end, 10);
5949 assert!(bounds.is_empty()); for i in 10..20u64 {
5955 journal.test_append_data(1, i * 100).await.unwrap();
5956 }
5957 journal.0.blobs.start_sync().await.await.unwrap();
5959 drop(journal);
5963
5964 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5966 .await
5967 .expect("Should recover from crash after data sync but before offsets sync");
5968
5969 let bounds = journal.bounds();
5971 assert_eq!(bounds.end, 20);
5972 assert_eq!(bounds.start, 10);
5973
5974 for i in 10..20u64 {
5976 assert_eq!(journal.read(i).await.unwrap(), i * 100);
5977 }
5978
5979 for i in 0..10 {
5981 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
5982 }
5983
5984 journal.destroy().await.unwrap();
5985 });
5986 }
5987
5988 #[test_traced]
5990 fn test_variable_concurrent_sync_recovery() {
5991 let executor = deterministic::Runner::default();
5992 executor.start(|context| async move {
5993 let cfg = Config {
5994 partition: "concurrent-sync-recovery".into(),
5995 items_per_section: NZU64!(10),
5996 compression: None,
5997 codec_config: (),
5998 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
5999 write_buffer: NZUsize!(1024),
6000 replay_buffer: NZUsize!(1024),
6001 };
6002
6003 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6004 .await
6005 .unwrap();
6006
6007 for i in 0..15u64 {
6009 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6010 }
6011
6012 let journal = journal.commit().await.unwrap();
6014
6015 drop(journal);
6017
6018 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6019 .await
6020 .unwrap();
6021
6022 assert_eq!(journal.size(), 15);
6024 for i in 0..15u64 {
6025 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6026 }
6027
6028 journal.destroy().await.unwrap();
6029 });
6030 }
6031
6032 #[test_traced]
6033 fn test_variable_recovery_from_mid_blob_durable_anchor() {
6034 let executor = deterministic::Runner::default();
6035 executor.start(|context| async move {
6036 let cfg = Config {
6037 partition: "mid-blob-durable-anchor".into(),
6038 items_per_section: NZU64!(5),
6039 compression: None,
6040 codec_config: (),
6041 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6042 write_buffer: NZUsize!(1024),
6043 replay_buffer: NZUsize!(1024),
6044 };
6045
6046 let mut journal =
6047 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
6048 .await
6049 .unwrap();
6050 let appended;
6051 (journal, appended) = journal.append(&700).await.unwrap();
6052 assert_eq!(appended, 7);
6053 journal = journal.sync().await.unwrap();
6054
6055 for i in 1..6u64 {
6056 let appended;
6057 (journal, appended) = journal.append(&(700 + i)).await.unwrap();
6058 assert_eq!(appended, 7 + i);
6059 }
6060 journal.commit().await.unwrap();
6061
6062 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6063 .await
6064 .unwrap();
6065 assert_eq!(journal.bounds(), 7..13);
6066 for i in 0..6u64 {
6067 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
6068 }
6069
6070 journal.destroy().await.unwrap();
6071 });
6072 }
6073
6074 #[test_traced]
6075 fn test_init_at_size_rejects_conflicting_offsets_partitions() {
6076 let executor = deterministic::Runner::default();
6077 executor.start(|context| async move {
6078 let cfg = Config {
6079 partition: "init-at-size-conflicting-offsets".into(),
6080 items_per_section: NZU64!(5),
6081 compression: None,
6082 codec_config: (),
6083 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6084 write_buffer: NZUsize!(1024),
6085 replay_buffer: NZUsize!(1024),
6086 };
6087 let legacy_partition = cfg.offsets_partition();
6088 let blobs_partition = format!("{legacy_partition}-blobs");
6089
6090 for partition in [&legacy_partition, &blobs_partition] {
6091 let (blob, _) = context.open(partition, &0u64.to_be_bytes()).await.unwrap();
6092 blob.write_at(0, vec![0], WriteOptions::SYNC).await.unwrap();
6093 }
6094
6095 let result = Journal::<_, u64>::init_at_size(context.child("storage"), cfg, 7).await;
6096 assert!(matches!(result, Err(Error::Corruption(_))));
6097
6098 assert_eq!(context.scan(&legacy_partition).await.unwrap().len(), 1);
6101 assert_eq!(context.scan(&blobs_partition).await.unwrap().len(), 1);
6102 });
6103 }
6104
6105 #[test_traced]
6106 fn test_init_at_size_zero() {
6107 let executor = deterministic::Runner::default();
6108 executor.start(|context| async move {
6109 let cfg = Config {
6110 partition: "init-at-size-zero".into(),
6111 items_per_section: NZU64!(5),
6112 compression: None,
6113 codec_config: (),
6114 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6115 write_buffer: NZUsize!(1024),
6116 replay_buffer: NZUsize!(1024),
6117 };
6118
6119 let mut journal =
6120 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 0)
6121 .await
6122 .unwrap();
6123
6124 assert_eq!(journal.size(), 0);
6126
6127 assert!(journal.bounds().is_empty());
6129
6130 let pos;
6132 (journal, pos) = journal.append(&100).await.unwrap();
6133 assert_eq!(pos, 0);
6134 assert_eq!(journal.size(), 1);
6135 assert_eq!(journal.read(0).await.unwrap(), 100);
6136
6137 journal.destroy().await.unwrap();
6138 });
6139 }
6140
6141 #[test_traced]
6142 fn test_init_at_size_blob_boundary() {
6143 let executor = deterministic::Runner::default();
6144 executor.start(|context| async move {
6145 let cfg = Config {
6146 partition: "init-at-size-boundary".into(),
6147 items_per_section: NZU64!(5),
6148 compression: None,
6149 codec_config: (),
6150 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6151 write_buffer: NZUsize!(1024),
6152 replay_buffer: NZUsize!(1024),
6153 };
6154
6155 let mut journal =
6157 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 10)
6158 .await
6159 .unwrap();
6160
6161 let bounds = journal.bounds();
6163 assert_eq!(bounds.end, 10);
6164
6165 assert!(bounds.is_empty());
6167
6168 let pos;
6170 (journal, pos) = journal.append(&1000).await.unwrap();
6171 assert_eq!(pos, 10);
6172 assert_eq!(journal.size(), 11);
6173 assert_eq!(journal.read(10).await.unwrap(), 1000);
6174
6175 let pos;
6177 (journal, pos) = journal.append(&1001).await.unwrap();
6178 assert_eq!(pos, 11);
6179 assert_eq!(journal.read(11).await.unwrap(), 1001);
6180
6181 journal.destroy().await.unwrap();
6182 });
6183 }
6184
6185 #[test_traced]
6186 fn test_init_at_size_mid_blob() {
6187 let executor = deterministic::Runner::default();
6188 executor.start(|context| async move {
6189 let cfg = Config {
6190 partition: "init-at-size-mid".into(),
6191 items_per_section: NZU64!(5),
6192 compression: None,
6193 codec_config: (),
6194 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6195 write_buffer: NZUsize!(1024),
6196 replay_buffer: NZUsize!(1024),
6197 };
6198
6199 let mut journal =
6201 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 7)
6202 .await
6203 .unwrap();
6204
6205 let bounds = journal.bounds();
6207 assert_eq!(bounds.end, 7);
6208
6209 assert!(bounds.is_empty());
6211
6212 let pos;
6214 (journal, pos) = journal.append(&700).await.unwrap();
6215 assert_eq!(pos, 7);
6216 assert_eq!(journal.size(), 8);
6217 assert_eq!(journal.read(7).await.unwrap(), 700);
6218
6219 journal.destroy().await.unwrap();
6220 });
6221 }
6222
6223 #[test_traced]
6224 fn test_init_at_size_persistence() {
6225 let executor = deterministic::Runner::default();
6226 executor.start(|context| async move {
6227 let cfg = Config {
6228 partition: "init-at-size-persist".into(),
6229 items_per_section: NZU64!(5),
6230 compression: None,
6231 codec_config: (),
6232 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6233 write_buffer: NZUsize!(1024),
6234 replay_buffer: NZUsize!(1024),
6235 };
6236
6237 let mut journal =
6239 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15)
6240 .await
6241 .unwrap();
6242
6243 for i in 0..5u64 {
6245 let pos;
6246 (journal, pos) = journal.append(&(1500 + i)).await.unwrap();
6247 assert_eq!(pos, 15 + i);
6248 }
6249
6250 assert_eq!(journal.size(), 20);
6251
6252 journal.sync().await.unwrap();
6254
6255 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6256 .await
6257 .unwrap();
6258
6259 let bounds = journal.bounds();
6261 assert_eq!(bounds.end, 20);
6262 assert_eq!(bounds.start, 15);
6263
6264 for i in 0..5u64 {
6266 assert_eq!(journal.read(15 + i).await.unwrap(), 1500 + i);
6267 }
6268
6269 let pos;
6271 (journal, pos) = journal.append(&9999).await.unwrap();
6272 assert_eq!(pos, 20);
6273 assert_eq!(journal.read(20).await.unwrap(), 9999);
6274
6275 journal.destroy().await.unwrap();
6276 });
6277 }
6278
6279 #[test_traced]
6280 fn test_init_at_size_persistence_without_data() {
6281 let executor = deterministic::Runner::default();
6282 executor.start(|context| async move {
6283 let cfg = Config {
6284 partition: "init-at-size-persist-empty".into(),
6285 items_per_section: NZU64!(5),
6286 compression: None,
6287 codec_config: (),
6288 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6289 write_buffer: NZUsize!(1024),
6290 replay_buffer: NZUsize!(1024),
6291 };
6292
6293 let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15)
6295 .await
6296 .unwrap();
6297
6298 let bounds = journal.bounds();
6299 assert_eq!(bounds.end, 15);
6300 assert!(bounds.is_empty());
6301
6302 drop(journal);
6304
6305 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6307 .await
6308 .unwrap();
6309
6310 let bounds = journal.bounds();
6311 assert_eq!(bounds.end, 15);
6312 assert!(bounds.is_empty());
6313
6314 let pos;
6316 (journal, pos) = journal.append(&1500).await.unwrap();
6317 assert_eq!(pos, 15);
6318 assert_eq!(journal.read(15).await.unwrap(), 1500);
6319
6320 journal.destroy().await.unwrap();
6321 });
6322 }
6323
6324 #[test_traced]
6325 fn test_init_at_size_clears_existing_data() {
6326 let executor = deterministic::Runner::default();
6327 executor.start(|context| async move {
6328 let cfg = Config {
6329 partition: "init-at-size-clears-existing".into(),
6330 items_per_section: NZU64!(5),
6331 compression: None,
6332 codec_config: (),
6333 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6334 write_buffer: NZUsize!(1024),
6335 replay_buffer: NZUsize!(1024),
6336 };
6337
6338 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6339 .await
6340 .unwrap();
6341 for i in 0..12u64 {
6342 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6343 }
6344 journal.sync().await.unwrap();
6345
6346 let mut journal =
6347 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 7)
6348 .await
6349 .unwrap();
6350 assert_eq!(journal.bounds(), 7..7);
6351 let appended;
6352 (journal, appended) = journal.append(&700).await.unwrap();
6353 assert_eq!(appended, 7);
6354 journal.sync().await.unwrap();
6355
6356 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6357 .await
6358 .unwrap();
6359 assert_eq!(journal.bounds(), 7..8);
6360 assert_eq!(journal.read(7).await.unwrap(), 700);
6361 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
6362 assert!(matches!(
6363 journal.read(8).await,
6364 Err(Error::ItemOutOfRange(8))
6365 ));
6366
6367 journal.destroy().await.unwrap();
6368 });
6369 }
6370
6371 #[test_traced]
6372 fn test_init_at_size_stages_reset_before_clearing_data() {
6373 let partition = "init-at-size-stage-before-clear-failure".to_string();
6374 let executor = deterministic::Runner::default();
6375 let ((), checkpoint) = executor.start_and_recover({
6376 let partition = partition.clone();
6377 |context| async move {
6378 let cfg = Config {
6379 partition,
6380 items_per_section: NZU64!(5),
6381 compression: None,
6382 codec_config: (),
6383 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6384 write_buffer: NZUsize!(1024),
6385 replay_buffer: NZUsize!(1024),
6386 };
6387
6388 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6389 .await
6390 .unwrap();
6391 for i in 0..12u64 {
6392 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6393 }
6394 let journal = journal.sync().await.unwrap();
6395 drop(journal);
6396
6397 *context.storage_fault_config().write() = deterministic::FaultConfig {
6398 sync_rate: Some(probability!(1.0)),
6399 ..Default::default()
6400 };
6401 assert!(
6402 Journal::<_, u64>::init_at_size(context.child("reset"), cfg, 7)
6403 .await
6404 .is_err()
6405 );
6406 }
6407 });
6408
6409 deterministic::Runner::from(checkpoint).start(move |context| async move {
6410 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
6411 let cfg = Config {
6412 partition,
6413 items_per_section: NZU64!(5),
6414 compression: None,
6415 codec_config: (),
6416 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6417 write_buffer: NZUsize!(1024),
6418 replay_buffer: NZUsize!(1024),
6419 };
6420
6421 let journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
6422 .await
6423 .unwrap();
6424 assert_eq!(journal.bounds(), 0..12);
6425 for i in 0..12u64 {
6426 assert_eq!(journal.read(i).await.unwrap(), 100 + i);
6427 }
6428
6429 journal.destroy().await.unwrap();
6430 });
6431 }
6432
6433 #[test_traced]
6434 fn test_clear_to_size_stages_reset_before_clearing_data() {
6435 let partition = "clear-to-size-stage-before-clear-failure".to_string();
6436 let executor = deterministic::Runner::default();
6437 let ((), checkpoint) = executor.start_and_recover({
6438 let partition = partition.clone();
6439 |context| async move {
6440 let cfg = Config {
6441 partition,
6442 items_per_section: NZU64!(5),
6443 compression: None,
6444 codec_config: (),
6445 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6446 write_buffer: NZUsize!(1024),
6447 replay_buffer: NZUsize!(1024),
6448 };
6449
6450 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6451 .await
6452 .unwrap();
6453 for i in 0..12u64 {
6454 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6455 }
6456 let journal = journal.sync().await.unwrap();
6457
6458 *context.storage_fault_config().write() = deterministic::FaultConfig {
6461 sync_rate: Some(probability!(1.0)),
6462 ..Default::default()
6463 };
6464 assert!(journal.0.clear_to_size(7).await.is_err());
6465 }
6466 });
6467
6468 deterministic::Runner::from(checkpoint).start(move |context| async move {
6469 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
6470 let cfg = Config {
6471 partition,
6472 items_per_section: NZU64!(5),
6473 compression: None,
6474 codec_config: (),
6475 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6476 write_buffer: NZUsize!(1024),
6477 replay_buffer: NZUsize!(1024),
6478 };
6479
6480 let journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
6481 .await
6482 .unwrap();
6483 assert_eq!(journal.bounds(), 0..12);
6484 for i in 0..12u64 {
6485 assert_eq!(journal.read(i).await.unwrap(), 100 + i);
6486 }
6487
6488 journal.destroy().await.unwrap();
6489 });
6490 }
6491
6492 #[test_traced]
6493 fn test_clear_to_size_crash_after_staging_completes_on_init() {
6494 let partition = "clear-to-size-crash-after-staging".to_string();
6495 let executor = deterministic::Runner::default();
6496 let ((), checkpoint) = executor.start_and_recover({
6497 let partition = partition.clone();
6498 |context| async move {
6499 let cfg = Config {
6500 partition,
6501 items_per_section: NZU64!(5),
6502 compression: None,
6503 codec_config: (),
6504 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6505 write_buffer: NZUsize!(1024),
6506 replay_buffer: NZUsize!(1024),
6507 };
6508
6509 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6510 .await
6511 .unwrap();
6512 for i in 0..12u64 {
6513 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6514 }
6515 journal = journal.sync().await.unwrap();
6516
6517 *context.storage_fault_config().write() = deterministic::FaultConfig {
6521 remove_rate: Some(probability!(1.0)),
6522 ..Default::default()
6523 };
6524 assert!(journal.0.clear_to_size(7).await.is_err());
6525 }
6526 });
6527
6528 deterministic::Runner::from(checkpoint).start(move |context| async move {
6529 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
6530 let cfg = Config {
6531 partition,
6532 items_per_section: NZU64!(5),
6533 compression: None,
6534 codec_config: (),
6535 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6536 write_buffer: NZUsize!(1024),
6537 replay_buffer: NZUsize!(1024),
6538 };
6539
6540 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
6542 .await
6543 .unwrap();
6544 assert_eq!(journal.bounds(), 7..7);
6545 let appended;
6546 (journal, appended) = journal.append(&700).await.unwrap();
6547 assert_eq!(appended, 7);
6548 journal.sync().await.unwrap();
6549
6550 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg.clone())
6552 .await
6553 .unwrap();
6554 assert_eq!(journal.bounds(), 7..8);
6555 assert_eq!(journal.read(7).await.unwrap(), 700);
6556
6557 journal.destroy().await.unwrap();
6558 });
6559 }
6560
6561 #[test_traced]
6562 fn test_init_at_size_recovers_staged_reset_crash_points() {
6563 let executor = deterministic::Runner::default();
6564 executor.start(|context| async move {
6565 for (index, clear_data) in [false, true].into_iter().enumerate() {
6566 let cfg = Config {
6567 partition: format!("init-at-size-staged-reset-crash-{index}"),
6568 items_per_section: NZU64!(5),
6569 compression: None,
6570 codec_config: (),
6571 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6572 write_buffer: NZUsize!(1024),
6573 replay_buffer: NZUsize!(1024),
6574 };
6575
6576 let mut journal = Journal::<_, u64>::init(
6577 context.child("first").with_attribute("index", index),
6578 cfg.clone(),
6579 )
6580 .await
6581 .unwrap();
6582 for i in 0..12u64 {
6583 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6584 }
6585 let journal = journal.sync().await.unwrap();
6586 drop(journal);
6587
6588 let offsets_cfg = fixed::Config {
6589 partition: cfg.offsets_partition(),
6590 items_per_blob: cfg.items_per_section,
6591 page_cache: cfg.page_cache.clone(),
6592 write_buffer: cfg.write_buffer,
6593 replay_buffer: cfg.replay_buffer,
6594 };
6595 let intent_ctx = context.child("intent").with_attribute("index", index);
6599 fixed::Journal::<_, u64>::test_stage_clear(
6600 intent_ctx.child("meta"),
6601 &offsets_cfg.partition,
6602 7,
6603 )
6604 .await
6605 .unwrap();
6606
6607 if clear_data {
6608 Partition::<deterministic::Context>::remove_all(
6609 &context,
6610 &cfg.data_partition(),
6611 )
6612 .await
6613 .unwrap();
6614 }
6615
6616 let mut journal = Journal::<_, u64>::init(
6617 context.child("recover").with_attribute("index", index),
6618 cfg.clone(),
6619 )
6620 .await
6621 .unwrap();
6622 assert_eq!(journal.bounds(), 7..7);
6623 let appended;
6624 (journal, appended) = journal.append(&700).await.unwrap();
6625 assert_eq!(appended, 7);
6626 journal.sync().await.unwrap();
6627
6628 let journal = Journal::<_, u64>::init(
6629 context.child("reopen").with_attribute("index", index),
6630 cfg.clone(),
6631 )
6632 .await
6633 .unwrap();
6634 assert_eq!(journal.bounds(), 7..8);
6635 assert_eq!(journal.read(7).await.unwrap(), 700);
6636
6637 journal.destroy().await.unwrap();
6638 }
6639 });
6640 }
6641
6642 #[test_traced]
6643 fn test_init_at_size_overwrites_pending_clear_target() {
6644 let executor = deterministic::Runner::default();
6645 executor.start(|context| async move {
6646 let cfg = Config {
6647 partition: "init-at-size-overwrites-pending-target".into(),
6648 items_per_section: NZU64!(5),
6649 compression: None,
6650 codec_config: (),
6651 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6652 write_buffer: NZUsize!(1024),
6653 replay_buffer: NZUsize!(1024),
6654 };
6655
6656 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6657 .await
6658 .unwrap();
6659 for i in 0..12u64 {
6660 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6661 }
6662 let journal = journal.sync().await.unwrap();
6663 drop(journal);
6664
6665 let offsets_cfg = fixed::Config {
6668 partition: cfg.offsets_partition(),
6669 items_per_blob: cfg.items_per_section,
6670 page_cache: cfg.page_cache.clone(),
6671 write_buffer: cfg.write_buffer,
6672 replay_buffer: cfg.replay_buffer,
6673 };
6674 let stale_ctx = context.child("stale");
6675 fixed::Journal::<_, u64>::test_stage_clear(
6676 stale_ctx.child("meta"),
6677 &offsets_cfg.partition,
6678 5,
6679 )
6680 .await
6681 .unwrap();
6682
6683 let mut journal =
6685 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 10)
6686 .await
6687 .unwrap();
6688 assert_eq!(journal.bounds(), 10..10);
6689 let appended;
6690 (journal, appended) = journal.append(&700).await.unwrap();
6691 assert_eq!(appended, 10);
6692 journal.sync().await.unwrap();
6693
6694 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg.clone())
6696 .await
6697 .unwrap();
6698 assert_eq!(journal.bounds(), 10..11);
6699 assert_eq!(journal.read(10).await.unwrap(), 700);
6700
6701 journal.destroy().await.unwrap();
6702 });
6703 }
6704
6705 #[test_traced]
6706 fn test_init_at_size_discards_same_blob_stale_data() {
6707 let executor = deterministic::Runner::default();
6708 executor.start(|context| async move {
6709 let cfg = Config {
6710 partition: "init-at-size-discards-same-blob-stale-data".into(),
6711 items_per_section: NZU64!(5),
6712 compression: None,
6713 codec_config: (),
6714 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6715 write_buffer: NZUsize!(1024),
6716 replay_buffer: NZUsize!(1024),
6717 };
6718
6719 let mut journal =
6720 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 5)
6721 .await
6722 .unwrap();
6723 for i in 0..4u64 {
6724 let appended;
6725 (journal, appended) = journal.append(&(500 + i)).await.unwrap();
6726 assert_eq!(appended, 5 + i);
6727 }
6728 let journal = journal.sync().await.unwrap();
6729 drop(journal);
6730
6731 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 7)
6732 .await
6733 .unwrap();
6734
6735 let mut journal = Journal::<_, u64>::init(context.child("after_reset"), cfg.clone())
6736 .await
6737 .unwrap();
6738 assert_eq!(journal.bounds(), 7..7);
6739 assert!(matches!(
6740 journal.read(7).await,
6741 Err(Error::ItemOutOfRange(7))
6742 ));
6743
6744 let appended;
6745 (journal, appended) = journal.append(&700).await.unwrap();
6746 assert_eq!(appended, 7);
6747 journal.sync().await.unwrap();
6748
6749 let journal = Journal::<_, u64>::init(context.child("after_append"), cfg.clone())
6750 .await
6751 .unwrap();
6752 assert_eq!(journal.bounds(), 7..8);
6753 assert_eq!(journal.read(7).await.unwrap(), 700);
6754 assert!(matches!(
6755 journal.read(8).await,
6756 Err(Error::ItemOutOfRange(8))
6757 ));
6758
6759 journal.destroy().await.unwrap();
6760 });
6761 }
6762
6763 #[test_traced]
6765 fn test_init_at_size_mid_blob_persistence() {
6766 let executor = deterministic::Runner::default();
6767 executor.start(|context| async move {
6768 let cfg = Config {
6769 partition: "init-at-size-mid-blob".into(),
6770 items_per_section: NZU64!(5),
6771 compression: None,
6772 codec_config: (),
6773 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6774 write_buffer: NZUsize!(1024),
6775 replay_buffer: NZUsize!(1024),
6776 };
6777
6778 let mut journal =
6780 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
6781 .await
6782 .unwrap();
6783
6784 for i in 0..3u64 {
6786 let pos;
6787 (journal, pos) = journal.append(&(700 + i)).await.unwrap();
6788 assert_eq!(pos, 7 + i);
6789 }
6790
6791 let bounds = journal.bounds();
6792 assert_eq!(bounds.end, 10);
6793 assert_eq!(bounds.start, 7);
6794
6795 journal.sync().await.unwrap();
6797
6798 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6800 .await
6801 .unwrap();
6802
6803 let bounds = journal.bounds();
6805 assert_eq!(bounds.end, 10);
6806 assert_eq!(bounds.start, 7);
6807
6808 for i in 0..3u64 {
6810 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
6811 }
6812
6813 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
6815
6816 journal.destroy().await.unwrap();
6817 });
6818 }
6819
6820 #[test_traced]
6822 fn test_init_at_size_mid_blob_multi_blob_persistence() {
6823 let executor = deterministic::Runner::default();
6824 executor.start(|context| async move {
6825 let cfg = Config {
6826 partition: "init-at-size-multi-blob".into(),
6827 items_per_section: NZU64!(5),
6828 compression: None,
6829 codec_config: (),
6830 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6831 write_buffer: NZUsize!(1024),
6832 replay_buffer: NZUsize!(1024),
6833 };
6834
6835 let mut journal =
6837 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
6838 .await
6839 .unwrap();
6840
6841 for i in 0..8u64 {
6843 let pos;
6844 (journal, pos) = journal.append(&(700 + i)).await.unwrap();
6845 assert_eq!(pos, 7 + i);
6846 }
6847
6848 let bounds = journal.bounds();
6849 assert_eq!(bounds.end, 15);
6850 assert_eq!(bounds.start, 7);
6851
6852 journal.sync().await.unwrap();
6854
6855 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6857 .await
6858 .unwrap();
6859
6860 let bounds = journal.bounds();
6862 assert_eq!(bounds.end, 15);
6863 assert_eq!(bounds.start, 7);
6864
6865 for i in 0..8u64 {
6867 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
6868 }
6869
6870 journal.destroy().await.unwrap();
6871 });
6872 }
6873
6874 #[test_traced]
6876 fn test_align_journals_data_empty_mid_blob_pruning_boundary() {
6877 let executor = deterministic::Runner::default();
6878 executor.start(|context| async move {
6879 let cfg = Config {
6880 partition: "align-journals-mid-blob-pruning-boundary".into(),
6881 items_per_section: NZU64!(5),
6882 compression: None,
6883 codec_config: (),
6884 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6885 write_buffer: NZUsize!(1024),
6886 replay_buffer: NZUsize!(1024),
6887 };
6888
6889 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6891 .await
6892 .unwrap();
6893 for i in 0..7u64 {
6894 (journal, _) = journal.append(&(100 + i)).await.unwrap();
6895 }
6896 journal = journal.sync().await.unwrap();
6897
6898 drop(journal);
6900 Partition::<deterministic::Context>::remove_all(&context, &cfg.data_partition())
6901 .await
6902 .unwrap();
6903
6904 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6906 .await
6907 .unwrap();
6908 let bounds = journal.bounds();
6909 assert_eq!(bounds.end, 7);
6910 assert!(bounds.is_empty());
6911
6912 let pos;
6914 (journal, pos) = journal.append(&777).await.unwrap();
6915 assert_eq!(pos, 7);
6916 assert_eq!(journal.size(), 8);
6917 assert_eq!(journal.read(7).await.unwrap(), 777);
6918
6919 journal.0.blobs.start_sync().await.await.unwrap();
6921 drop(journal);
6922
6923 let journal = Journal::<_, u64>::init(context.child("third"), cfg.clone())
6925 .await
6926 .unwrap();
6927 let bounds = journal.bounds();
6928 assert_eq!(bounds.end, 8);
6929 assert_eq!(bounds.start, 7);
6930 assert_eq!(journal.read(7).await.unwrap(), 777);
6931
6932 journal.destroy().await.unwrap();
6933 });
6934 }
6935
6936 #[test_traced]
6938 fn test_init_at_size_crash_data_synced_offsets_not() {
6939 let executor = deterministic::Runner::default();
6940 executor.start(|context| async move {
6941 let cfg = Config {
6942 partition: "init-at-size-crash-recovery".into(),
6943 items_per_section: NZU64!(5),
6944 compression: None,
6945 codec_config: (),
6946 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6947 write_buffer: NZUsize!(1024),
6948 replay_buffer: NZUsize!(1024),
6949 };
6950
6951 let mut journal =
6953 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
6954 .await
6955 .unwrap();
6956
6957 for i in 0..3u64 {
6959 (journal, _) = journal.append(&(700 + i)).await.unwrap();
6960 }
6961
6962 journal.test_sync_data_blob(1).await.unwrap();
6965 drop(journal);
6967
6968 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6970 .await
6971 .unwrap();
6972
6973 let bounds = journal.bounds();
6975 assert_eq!(bounds.end, 10);
6976 assert_eq!(bounds.start, 7);
6977
6978 for i in 0..3u64 {
6980 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
6981 }
6982
6983 journal.destroy().await.unwrap();
6984 });
6985 }
6986
6987 #[test_traced]
6988 fn test_prune_does_not_move_oldest_retained_backwards() {
6989 let executor = deterministic::Runner::default();
6990 executor.start(|context| async move {
6991 let cfg = Config {
6992 partition: "prune-no-backwards".into(),
6993 items_per_section: NZU64!(5),
6994 compression: None,
6995 codec_config: (),
6996 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
6997 write_buffer: NZUsize!(1024),
6998 replay_buffer: NZUsize!(1024),
6999 };
7000
7001 let mut journal =
7002 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
7003 .await
7004 .unwrap();
7005
7006 for i in 0..3u64 {
7008 let pos;
7009 (journal, pos) = journal.append(&(700 + i)).await.unwrap();
7010 assert_eq!(pos, 7 + i);
7011 }
7012 assert_eq!(journal.bounds().start, 7);
7013
7014 (journal, _) = journal.prune(8).await.unwrap();
7016 assert_eq!(journal.bounds().start, 7);
7017 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
7018 assert_eq!(journal.read(7).await.unwrap(), 700);
7019
7020 journal.destroy().await.unwrap();
7021 });
7022 }
7023
7024 #[test_traced]
7025 fn test_variable_recovery_near_max_data_synced_offsets_not() {
7026 let executor = deterministic::Runner::default();
7027 executor.start(|context| async move {
7028 let cfg = Config {
7029 partition: "near-max-data-synced-offsets-not".into(),
7030 items_per_section: NZU64!(10),
7031 compression: None,
7032 codec_config: (),
7033 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
7034 write_buffer: NZUsize!(1024),
7035 replay_buffer: NZUsize!(1024),
7036 };
7037
7038 let mut journal =
7039 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), u64::MAX - 1)
7040 .await
7041 .unwrap();
7042 let appended;
7043 (journal, appended) = journal.append(&7).await.unwrap();
7044 assert_eq!(appended, u64::MAX - 1);
7045 journal
7046 .test_sync_data_blob(position_to_blob(u64::MAX - 1, cfg.items_per_section.get()))
7047 .await
7048 .unwrap();
7049 drop(journal);
7050
7051 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7052 .await
7053 .unwrap();
7054 assert_eq!(journal.bounds(), (u64::MAX - 1)..u64::MAX);
7055 assert_eq!(journal.read(u64::MAX - 1).await.unwrap(), 7);
7056
7057 journal.destroy().await.unwrap();
7058 });
7059 }
7060
7061 #[test_traced]
7062 fn test_init_at_size_large_offset() {
7063 let executor = deterministic::Runner::default();
7064 executor.start(|context| async move {
7065 let cfg = Config {
7066 partition: "init-at-size-large".into(),
7067 items_per_section: NZU64!(5),
7068 compression: None,
7069 codec_config: (),
7070 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
7071 write_buffer: NZUsize!(1024),
7072 replay_buffer: NZUsize!(1024),
7073 };
7074
7075 let mut journal =
7077 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 1000)
7078 .await
7079 .unwrap();
7080
7081 let bounds = journal.bounds();
7082 assert_eq!(bounds.end, 1000);
7083 assert!(bounds.is_empty());
7085
7086 let pos;
7088 (journal, pos) = journal.append(&100000).await.unwrap();
7089 assert_eq!(pos, 1000);
7090 assert_eq!(journal.read(1000).await.unwrap(), 100000);
7091
7092 journal.destroy().await.unwrap();
7093 });
7094 }
7095
7096 #[test_traced]
7097 fn test_init_at_size_prune_and_append() {
7098 let executor = deterministic::Runner::default();
7099 executor.start(|context| async move {
7100 let cfg = Config {
7101 partition: "init-at-size-prune".into(),
7102 items_per_section: NZU64!(5),
7103 compression: None,
7104 codec_config: (),
7105 page_cache: CacheRef::from_pooler(&context, SMALL_PAGE_SIZE, NZUsize!(2)),
7106 write_buffer: NZUsize!(1024),
7107 replay_buffer: NZUsize!(1024),
7108 };
7109
7110 let mut journal =
7112 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 20)
7113 .await
7114 .unwrap();
7115
7116 for i in 0..10u64 {
7118 (journal, _) = journal.append(&(2000 + i)).await.unwrap();
7119 }
7120
7121 assert_eq!(journal.size(), 30);
7122
7123 (journal, _) = journal.prune(25).await.unwrap();
7125
7126 let bounds = journal.bounds();
7127 assert_eq!(bounds.end, 30);
7128 assert_eq!(bounds.start, 25);
7129
7130 for i in 25..30u64 {
7132 assert_eq!(journal.read(i).await.unwrap(), 2000 + (i - 20));
7133 }
7134
7135 let pos;
7137 (journal, pos) = journal.append(&3000).await.unwrap();
7138 assert_eq!(pos, 30);
7139
7140 journal.destroy().await.unwrap();
7141 });
7142 }
7143
7144 #[test_traced]
7146 fn test_init_sync_no_existing_data() {
7147 let executor = deterministic::Runner::default();
7148 executor.start(|context| async move {
7149 let cfg = Config {
7150 partition: "test-fresh-start".into(),
7151 items_per_section: NZU64!(5),
7152 compression: None,
7153 codec_config: (),
7154 write_buffer: NZUsize!(1024),
7155 replay_buffer: NZUsize!(1024),
7156 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7157 };
7158
7159 let lower_bound = 10;
7161 let upper_bound = 26;
7162 let mut journal = Journal::init_sync(
7163 context.child("storage"),
7164 cfg.clone(),
7165 lower_bound..upper_bound,
7166 )
7167 .await
7168 .expect("Failed to initialize journal with sync boundaries");
7169
7170 let bounds = journal.bounds();
7171 assert_eq!(bounds.end, lower_bound);
7172 assert!(bounds.is_empty());
7173
7174 let pos1;
7176 (journal, pos1) = journal.append(&42u64).await.unwrap();
7177 assert_eq!(pos1, lower_bound);
7178 assert_eq!(journal.read(pos1).await.unwrap(), 42u64);
7179
7180 let pos2;
7181 (journal, pos2) = journal.append(&43u64).await.unwrap();
7182 assert_eq!(pos2, lower_bound + 1);
7183 assert_eq!(journal.read(pos2).await.unwrap(), 43u64);
7184
7185 journal.destroy().await.unwrap();
7186 });
7187 }
7188
7189 #[test_traced]
7191 fn test_init_sync_existing_data_overlap() {
7192 let executor = deterministic::Runner::default();
7193 executor.start(|context| async move {
7194 let cfg = Config {
7195 partition: "test-overlap".into(),
7196 items_per_section: NZU64!(5),
7197 compression: None,
7198 codec_config: (),
7199 write_buffer: NZUsize!(1024),
7200 replay_buffer: NZUsize!(1024),
7201 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7202 };
7203
7204 let mut journal =
7206 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
7207 .await
7208 .expect("Failed to create initial journal");
7209
7210 for i in 0..20u64 {
7212 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7213 }
7214 let journal = journal.sync().await.unwrap();
7215 drop(journal);
7216
7217 let lower_bound = 8;
7220 let upper_bound = 31;
7221 let mut journal = Journal::<_, u64>::init_sync(
7222 context.child("storage"),
7223 cfg.clone(),
7224 lower_bound..upper_bound,
7225 )
7226 .await
7227 .expect("Failed to initialize journal with overlap");
7228
7229 assert_eq!(journal.size(), 20);
7230
7231 assert_eq!(journal.bounds().start, 5); assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
7236 assert!(matches!(journal.read(4).await, Err(Error::ItemPruned(_))));
7237
7238 assert_eq!(journal.read(5).await.unwrap(), 500);
7240 assert_eq!(journal.read(8).await.unwrap(), 800);
7241 assert_eq!(journal.read(19).await.unwrap(), 1900);
7242
7243 assert!(matches!(
7245 journal.read(20).await,
7246 Err(Error::ItemOutOfRange(_))
7247 ));
7248
7249 let pos;
7251 (journal, pos) = journal.append(&999).await.unwrap();
7252 assert_eq!(pos, 20);
7253 assert_eq!(journal.read(20).await.unwrap(), 999);
7254
7255 journal.destroy().await.unwrap();
7256 });
7257 }
7258
7259 #[should_panic]
7261 #[test_traced]
7262 fn test_init_sync_invalid_parameters() {
7263 let executor = deterministic::Runner::default();
7264 executor.start(|context| async move {
7265 let cfg = Config {
7266 partition: "test-invalid".into(),
7267 items_per_section: NZU64!(5),
7268 compression: None,
7269 codec_config: (),
7270 write_buffer: NZUsize!(1024),
7271 replay_buffer: NZUsize!(1024),
7272 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7273 };
7274
7275 #[allow(clippy::reversed_empty_ranges)]
7276 let _result = Journal::<_, u64>::init_sync(
7277 context.child("storage"),
7278 cfg,
7279 10..5, )
7281 .await;
7282 });
7283 }
7284
7285 #[test_traced]
7287 fn test_init_sync_existing_data_exact_match() {
7288 let executor = deterministic::Runner::default();
7289 executor.start(|context| async move {
7290 let items_per_section = NZU64!(5);
7291 let cfg = Config {
7292 partition: "test-exact-match".into(),
7293 items_per_section,
7294 compression: None,
7295 codec_config: (),
7296 write_buffer: NZUsize!(1024),
7297 replay_buffer: NZUsize!(1024),
7298 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7299 };
7300
7301 let mut journal =
7303 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
7304 .await
7305 .expect("Failed to create initial journal");
7306
7307 for i in 0..20u64 {
7309 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7310 }
7311 let journal = journal.sync().await.unwrap();
7312 drop(journal);
7313
7314 let lower_bound = 5; let upper_bound = 20; let mut journal = Journal::<_, u64>::init_sync(
7318 context.child("storage"),
7319 cfg.clone(),
7320 lower_bound..upper_bound,
7321 )
7322 .await
7323 .expect("Failed to initialize journal with exact match");
7324
7325 assert_eq!(journal.size(), 20);
7326
7327 assert_eq!(journal.bounds().start, 5); assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
7332 assert!(matches!(journal.read(4).await, Err(Error::ItemPruned(_))));
7333
7334 assert_eq!(journal.read(5).await.unwrap(), 500);
7336 assert_eq!(journal.read(10).await.unwrap(), 1000);
7337 assert_eq!(journal.read(19).await.unwrap(), 1900);
7338
7339 assert!(matches!(
7341 journal.read(20).await,
7342 Err(Error::ItemOutOfRange(_))
7343 ));
7344
7345 let pos;
7347 (journal, pos) = journal.append(&999).await.unwrap();
7348 assert_eq!(pos, 20);
7349 assert_eq!(journal.read(20).await.unwrap(), 999);
7350
7351 journal.destroy().await.unwrap();
7352 });
7353 }
7354
7355 #[test_traced]
7357 fn test_init_sync_rewinds_data_exceeding_upper_bound() {
7358 let executor = deterministic::Runner::default();
7359 executor.start(|context| async move {
7360 let items_per_section = NZU64!(5);
7361 let cfg = Config {
7362 partition: "test-unexpected-data".into(),
7363 items_per_section,
7364 compression: None,
7365 codec_config: (),
7366 write_buffer: NZUsize!(1024),
7367 replay_buffer: NZUsize!(1024),
7368 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7369 };
7370
7371 let mut journal =
7373 Journal::<deterministic::Context, u64>::init(context.child("initial"), cfg.clone())
7374 .await
7375 .expect("Failed to create initial journal");
7376
7377 for i in 0..30u64 {
7379 (journal, _) = journal.append(&(i * 1000)).await.unwrap();
7380 }
7381 let journal = journal.sync().await.unwrap();
7382 drop(journal);
7383
7384 let lower_bound = 8; let upper_bound = 20;
7387 let journal = Journal::<_, u64>::init_sync(
7388 context.child("sync"),
7389 cfg.clone(),
7390 lower_bound..upper_bound,
7391 )
7392 .await
7393 .expect("Failed to rewind journal to the older sync range");
7394
7395 assert_eq!(journal.bounds(), 5..upper_bound);
7396 for i in lower_bound..upper_bound {
7397 assert_eq!(journal.read(i).await.unwrap(), i * 1000);
7398 }
7399 journal.destroy().await.unwrap();
7400 });
7401 }
7402
7403 #[test_traced]
7405 fn test_init_sync_empty_stale_position_beyond_upper_bound() {
7406 let executor = deterministic::Runner::default();
7407 executor.start(|context| async move {
7408 let cfg = Config {
7409 partition: "test-empty-stale-position".into(),
7410 items_per_section: NZU64!(5),
7411 compression: None,
7412 codec_config: (),
7413 write_buffer: NZUsize!(1024),
7414 replay_buffer: NZUsize!(1024),
7415 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7416 };
7417
7418 let stale_size = 30;
7419 let journal = Journal::<deterministic::Context, u64>::init_at_size(
7420 context.child("first"),
7421 cfg.clone(),
7422 stale_size,
7423 )
7424 .await
7425 .expect("Failed to create stale empty journal");
7426 assert_eq!(journal.size(), stale_size);
7427 assert!(journal.bounds().is_empty());
7428 drop(journal);
7429
7430 let lower_bound = 10;
7431 let upper_bound = 26;
7432 let mut journal = Journal::<_, u64>::init_sync(
7433 context.child("second"),
7434 cfg.clone(),
7435 lower_bound..upper_bound,
7436 )
7437 .await
7438 .expect("Failed to repair stale empty journal");
7439
7440 assert_eq!(journal.size(), lower_bound);
7441 assert!(journal.bounds().is_empty());
7442
7443 let pos;
7444 (journal, pos) = journal.append(&999).await.unwrap();
7445 assert_eq!(pos, lower_bound);
7446 assert_eq!(journal.read(pos).await.unwrap(), 999);
7447
7448 journal.destroy().await.unwrap();
7449 });
7450 }
7451
7452 #[test_traced]
7454 fn test_init_sync_recovers_from_stale_clear_to_size() {
7455 let executor = deterministic::Runner::default();
7456 executor.start(|context| async move {
7457 let cfg = Config {
7458 partition: "test-stale-clear-to-size".into(),
7459 items_per_section: NZU64!(5),
7460 compression: None,
7461 codec_config: (),
7462 write_buffer: NZUsize!(1024),
7463 replay_buffer: NZUsize!(1024),
7464 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7465 };
7466
7467 let journal = Journal::<deterministic::Context, u64>::init_at_size(
7468 context.child("first"),
7469 cfg.clone(),
7470 9,
7471 )
7472 .await
7473 .expect("Failed to create stale empty journal");
7474 let journal = journal.sync().await.unwrap();
7475 drop(journal);
7476
7477 match context.remove(&cfg.data_partition(), None).await {
7480 Ok(()) | Err(commonware_runtime::Error::PartitionMissing(_)) => {}
7481 Err(error) => panic!("failed to clear data partition: {error}"),
7482 }
7483
7484 let lower_bound = 7;
7485 let upper_bound = 20;
7486 let journal = Journal::<_, u64>::init_sync(
7487 context.child("second"),
7488 cfg.clone(),
7489 lower_bound..upper_bound,
7490 )
7491 .await
7492 .expect("Failed to repair stale empty journal");
7493
7494 assert_eq!(journal.size(), lower_bound);
7495 let bounds = journal.bounds();
7496 assert!(bounds.is_empty());
7497 assert_eq!(bounds.start, lower_bound);
7498
7499 journal.destroy().await.unwrap();
7500 });
7501 }
7502
7503 #[test_traced]
7505 fn test_init_sync_existing_data_stale() {
7506 let executor = deterministic::Runner::default();
7507 executor.start(|context| async move {
7508 let items_per_section = NZU64!(5);
7509 let cfg = Config {
7510 partition: "test-stale".into(),
7511 items_per_section,
7512 compression: None,
7513 codec_config: (),
7514 write_buffer: NZUsize!(1024),
7515 replay_buffer: NZUsize!(1024),
7516 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7517 };
7518
7519 let mut journal =
7521 Journal::<deterministic::Context, u64>::init(context.child("first"), cfg.clone())
7522 .await
7523 .expect("Failed to create initial journal");
7524
7525 for i in 0..10u64 {
7527 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7528 }
7529 let journal = journal.sync().await.unwrap();
7530 drop(journal);
7531
7532 let lower_bound = 15; let upper_bound = 26; let journal = Journal::<_, u64>::init_sync(
7536 context.child("second"),
7537 cfg.clone(),
7538 lower_bound..upper_bound,
7539 )
7540 .await
7541 .expect("Failed to initialize journal with stale data");
7542
7543 assert_eq!(journal.size(), 15);
7544
7545 assert!(journal.bounds().is_empty());
7547
7548 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
7550 assert!(matches!(journal.read(9).await, Err(Error::ItemPruned(_))));
7551 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(_))));
7552
7553 journal.destroy().await.unwrap();
7554 });
7555 }
7556
7557 #[test_traced]
7559 fn test_init_sync_blob_boundaries() {
7560 let executor = deterministic::Runner::default();
7561 executor.start(|context| async move {
7562 let items_per_section = NZU64!(5);
7563 let cfg = Config {
7564 partition: "test-boundaries".into(),
7565 items_per_section,
7566 compression: None,
7567 codec_config: (),
7568 write_buffer: NZUsize!(1024),
7569 replay_buffer: NZUsize!(1024),
7570 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7571 };
7572
7573 let mut journal =
7575 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
7576 .await
7577 .expect("Failed to create initial journal");
7578
7579 for i in 0..25u64 {
7581 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7582 }
7583 let journal = journal.sync().await.unwrap();
7584 drop(journal);
7585
7586 let lower_bound = 15; let upper_bound = 25; let mut journal = Journal::<_, u64>::init_sync(
7590 context.child("storage"),
7591 cfg.clone(),
7592 lower_bound..upper_bound,
7593 )
7594 .await
7595 .expect("Failed to initialize journal at boundaries");
7596
7597 assert_eq!(journal.size(), 25);
7598
7599 assert_eq!(journal.bounds().start, 15);
7601
7602 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
7604 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(_))));
7605
7606 assert_eq!(journal.read(15).await.unwrap(), 1500);
7608 assert_eq!(journal.read(20).await.unwrap(), 2000);
7609 assert_eq!(journal.read(24).await.unwrap(), 2400);
7610
7611 assert!(matches!(
7613 journal.read(25).await,
7614 Err(Error::ItemOutOfRange(_))
7615 ));
7616
7617 let pos;
7619 (journal, pos) = journal.append(&999).await.unwrap();
7620 assert_eq!(pos, 25);
7621 assert_eq!(journal.read(25).await.unwrap(), 999);
7622
7623 journal.destroy().await.unwrap();
7624 });
7625 }
7626
7627 #[test_traced]
7629 fn test_init_sync_same_blob_bounds() {
7630 let executor = deterministic::Runner::default();
7631 executor.start(|context| async move {
7632 let items_per_section = NZU64!(5);
7633 let cfg = Config {
7634 partition: "test-same-blob".into(),
7635 items_per_section,
7636 compression: None,
7637 codec_config: (),
7638 write_buffer: NZUsize!(1024),
7639 replay_buffer: NZUsize!(1024),
7640 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
7641 };
7642
7643 let mut journal =
7645 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
7646 .await
7647 .expect("Failed to create initial journal");
7648
7649 for i in 0..15u64 {
7651 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7652 }
7653 let journal = journal.sync().await.unwrap();
7654 drop(journal);
7655
7656 let lower_bound = 10; let upper_bound = 15; let mut journal = Journal::<_, u64>::init_sync(
7660 context.child("storage"),
7661 cfg.clone(),
7662 lower_bound..upper_bound,
7663 )
7664 .await
7665 .expect("Failed to initialize journal with same-blob bounds");
7666
7667 assert_eq!(journal.size(), 15);
7668
7669 assert_eq!(journal.bounds().start, 10);
7672
7673 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
7675 assert!(matches!(journal.read(9).await, Err(Error::ItemPruned(_))));
7676
7677 assert_eq!(journal.read(10).await.unwrap(), 1000);
7679 assert_eq!(journal.read(11).await.unwrap(), 1100);
7680 assert_eq!(journal.read(14).await.unwrap(), 1400);
7681
7682 assert!(matches!(
7684 journal.read(15).await,
7685 Err(Error::ItemOutOfRange(_))
7686 ));
7687
7688 let pos;
7690 (journal, pos) = journal.append(&999).await.unwrap();
7691 assert_eq!(pos, 15);
7692 assert_eq!(journal.read(15).await.unwrap(), 999);
7693
7694 journal.destroy().await.unwrap();
7695 });
7696 }
7697
7698 #[test_traced]
7703 fn test_single_item_per_blob() {
7704 let executor = deterministic::Runner::default();
7705 executor.start(|context| async move {
7706 let cfg = Config {
7707 partition: "single-item-per-blob".into(),
7708 items_per_section: NZU64!(1),
7709 compression: None,
7710 codec_config: (),
7711 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7712 write_buffer: NZUsize!(1024),
7713 replay_buffer: NZUsize!(1024),
7714 };
7715
7716 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7718 .await
7719 .unwrap();
7720
7721 let bounds = journal.bounds();
7723 assert_eq!(bounds.end, 0);
7724 assert!(bounds.is_empty());
7725
7726 let pos;
7728 (journal, pos) = journal.append(&0).await.unwrap();
7729 assert_eq!(pos, 0);
7730 assert_eq!(journal.size(), 1);
7731
7732 journal = journal.sync().await.unwrap();
7734
7735 let value = journal.read(journal.size() - 1).await.unwrap();
7737 assert_eq!(value, 0);
7738
7739 for i in 1..10u64 {
7741 let pos;
7742 (journal, pos) = journal.append(&(i * 100)).await.unwrap();
7743 assert_eq!(pos, i);
7744 assert_eq!(journal.size(), i + 1);
7745
7746 let value = journal.read(journal.size() - 1).await.unwrap();
7748 assert_eq!(value, i * 100);
7749 }
7750
7751 for i in 0..10u64 {
7753 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7754 }
7755
7756 journal = journal.sync().await.unwrap();
7757
7758 let pruned;
7761 (journal, pruned) = journal.prune(5).await.unwrap();
7762 assert!(pruned);
7763
7764 assert_eq!(journal.size(), 10);
7766
7767 assert_eq!(journal.bounds().start, 5);
7769
7770 let value = journal.read(journal.size() - 1).await.unwrap();
7772 assert_eq!(value, 900);
7773
7774 for i in 0..5 {
7776 assert!(matches!(
7777 journal.read(i).await,
7778 Err(crate::journal::Error::ItemPruned(_))
7779 ));
7780 }
7781
7782 for i in 5..10u64 {
7784 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7785 }
7786
7787 for i in 10..15u64 {
7789 let pos;
7790 (journal, pos) = journal.append(&(i * 100)).await.unwrap();
7791 assert_eq!(pos, i);
7792
7793 let value = journal.read(journal.size() - 1).await.unwrap();
7795 assert_eq!(value, i * 100);
7796 }
7797
7798 journal.sync().await.unwrap();
7799
7800 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7802 .await
7803 .unwrap();
7804
7805 assert_eq!(journal.size(), 15);
7807
7808 assert_eq!(journal.bounds().start, 5);
7810
7811 let value = journal.read(journal.size() - 1).await.unwrap();
7813 assert_eq!(value, 1400);
7814
7815 for i in 5..15u64 {
7817 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7818 }
7819
7820 journal.destroy().await.unwrap();
7821
7822 let mut journal = Journal::<_, u64>::init(context.child("third"), cfg.clone())
7825 .await
7826 .unwrap();
7827
7828 for i in 0..10u64 {
7830 (journal, _) = journal.append(&(i * 1000)).await.unwrap();
7831 }
7832
7833 (journal, _) = journal.prune(5).await.unwrap();
7835 let bounds = journal.bounds();
7836 assert_eq!(bounds.end, 10);
7837 assert_eq!(bounds.start, 5);
7838
7839 journal.sync().await.unwrap();
7841
7842 let journal = Journal::<_, u64>::init(context.child("fourth"), cfg.clone())
7844 .await
7845 .unwrap();
7846
7847 let bounds = journal.bounds();
7849 assert_eq!(bounds.end, 10);
7850 assert_eq!(bounds.start, 5);
7851
7852 let value = journal.read(journal.size() - 1).await.unwrap();
7854 assert_eq!(value, 9000);
7855
7856 for i in 5..10u64 {
7858 assert_eq!(journal.read(i).await.unwrap(), i * 1000);
7859 }
7860
7861 journal.destroy().await.unwrap();
7862
7863 let mut journal = Journal::<_, u64>::init(context.child("fifth"), cfg.clone())
7867 .await
7868 .unwrap();
7869
7870 for i in 0..5u64 {
7871 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7872 }
7873 journal = journal.sync().await.unwrap();
7874
7875 (journal, _) = journal.prune(5).await.unwrap();
7877 let bounds = journal.bounds();
7878 assert_eq!(bounds.end, 5); assert!(bounds.is_empty()); let result = journal.read(journal.size() - 1).await;
7883 assert!(matches!(result, Err(crate::journal::Error::ItemPruned(4))));
7884
7885 (journal, _) = journal.append(&500).await.unwrap();
7887 let bounds = journal.bounds();
7888 assert_eq!(bounds.start, 5);
7889 assert_eq!(journal.read(bounds.end - 1).await.unwrap(), 500);
7890
7891 journal.destroy().await.unwrap();
7892 });
7893 }
7894
7895 #[test_traced]
7896 fn test_variable_journal_clear_to_size() {
7897 let executor = deterministic::Runner::default();
7898 executor.start(|context| async move {
7899 let cfg = Config {
7900 partition: "clear-test".into(),
7901 items_per_section: NZU64!(10),
7902 compression: None,
7903 codec_config: (),
7904 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
7905 write_buffer: NZUsize!(1024),
7906 replay_buffer: NZUsize!(1024),
7907 };
7908
7909 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
7910 .await
7911 .unwrap();
7912
7913 for i in 0..25u64 {
7915 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7916 }
7917 let bounds = journal.bounds();
7918 assert_eq!(bounds.end, 25);
7919 assert_eq!(bounds.start, 0);
7920 journal = journal.sync().await.unwrap();
7921
7922 journal.0 = journal.0.clear_to_size(100).await.unwrap();
7924 let bounds = journal.bounds();
7925 assert_eq!(bounds.end, 100);
7926 assert!(bounds.is_empty());
7927
7928 for i in 0..25 {
7930 assert!(matches!(
7931 journal.read(i).await,
7932 Err(crate::journal::Error::ItemPruned(_))
7933 ));
7934 }
7935
7936 drop(journal);
7938 let mut journal =
7939 Journal::<_, u64>::init(context.child("journal_after_clear"), cfg.clone())
7940 .await
7941 .unwrap();
7942 let bounds = journal.bounds();
7943 assert_eq!(bounds.end, 100);
7944 assert!(bounds.is_empty());
7945
7946 for i in 100..105u64 {
7948 let pos;
7949 (journal, pos) = journal.append(&(i * 100)).await.unwrap();
7950 assert_eq!(pos, i);
7951 }
7952 let bounds = journal.bounds();
7953 assert_eq!(bounds.end, 105);
7954 assert_eq!(bounds.start, 100);
7955
7956 for i in 100..105u64 {
7958 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7959 }
7960
7961 journal.sync().await.unwrap();
7963
7964 let journal = Journal::<_, u64>::init(context.child("journal_reopened"), cfg)
7965 .await
7966 .unwrap();
7967
7968 let bounds = journal.bounds();
7969 assert_eq!(bounds.end, 105);
7970 assert_eq!(bounds.start, 100);
7971 for i in 100..105u64 {
7972 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7973 }
7974
7975 journal.destroy().await.unwrap();
7976 });
7977 }
7978
7979 #[test_traced]
7980 fn test_variable_journal_metrics() {
7981 let executor = deterministic::Runner::default();
7982 executor.start(|context| async move {
7983 let cfg = Config {
7984 partition: "metrics".into(),
7985 items_per_section: NZU64!(2),
7986 compression: None,
7987 codec_config: (),
7988 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10)),
7989 write_buffer: NZUsize!(1024),
7990 replay_buffer: NZUsize!(1024),
7991 };
7992 let mut journal = Journal::<_, u64>::init(context.child("variable_metrics"), cfg)
7993 .await
7994 .unwrap();
7995
7996 let items = [0, 1, 2, 3, 4];
7997 (journal, _) = journal.append_many(Many::Flat(&items)).await.unwrap();
7998 (journal, _) = journal.append(&5).await.unwrap();
7999 let reader;
8000 (journal, reader) = journal.snapshot().await.unwrap();
8001 reader.read(0).await.unwrap();
8002 reader.read_many(&[1, 2]).await.unwrap();
8003 reader.try_read_sync(3).unwrap();
8004 drop(reader);
8005 journal = journal.commit().await.unwrap();
8006 journal = journal.sync().await.unwrap();
8007 let handle;
8008 (journal, handle) = journal.start_sync().await.unwrap();
8009 handle.await.unwrap();
8010 (journal, _) = journal.prune(2).await.unwrap();
8011 journal = journal.rewind(4).await.unwrap();
8012
8013 let buffer = context.encode();
8014 for expected in [
8015 "variable_metrics_size 4",
8016 "variable_metrics_pruning_boundary 2",
8017 "variable_metrics_retained 2",
8018 "variable_metrics_tail_items 2",
8019 "variable_metrics_append_calls_total 1",
8020 "variable_metrics_append_many_calls_total 1",
8021 "variable_metrics_read_calls_total 1",
8022 "variable_metrics_read_many_calls_total 1",
8023 "variable_metrics_items_read_total 4",
8024 "variable_metrics_start_sync_calls_total 1",
8025 "variable_metrics_commit_calls_total 1",
8026 "variable_metrics_sync_calls_total 1",
8027 "variable_metrics_append_duration_count 1",
8028 "variable_metrics_append_many_duration_count 1",
8029 "variable_metrics_read_duration_count 0",
8030 "variable_metrics_read_many_duration_count 1",
8031 "variable_metrics_commit_duration_count 1",
8032 "variable_metrics_sync_duration_count 1",
8033 "variable_metrics_cache_hits_total 4",
8034 "variable_metrics_cache_misses_total 0",
8035 "variable_metrics_data_tracked",
8036 "variable_metrics_offsets_size 4",
8037 "variable_metrics_offsets_blobs_tracked",
8038 ] {
8039 assert!(buffer.contains(expected), "{expected}\n{buffer}");
8040 }
8041
8042 journal.destroy().await.unwrap();
8043 });
8044 }
8045
8046 #[test_traced]
8047 fn test_variable_journal_read_miss_timed() {
8048 let executor = deterministic::Runner::default();
8050 executor.start(|context| async move {
8051 let cfg = Config {
8054 partition: "miss".into(),
8055 items_per_section: NZU64!(50),
8056 compression: None,
8057 codec_config: (),
8058 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10)),
8059 write_buffer: NZUsize!(1024),
8060 replay_buffer: NZUsize!(1024),
8061 };
8062 let mut journal = Journal::<_, u64>::init(context.child("miss"), cfg)
8063 .await
8064 .unwrap();
8065 for i in 0..200u64 {
8066 (journal, _) = journal.append(&i).await.unwrap();
8067 }
8068 journal = journal.sync().await.unwrap();
8069
8070 let reader;
8072 (journal, reader) = journal.snapshot().await.unwrap();
8073 let pos = (0..200)
8074 .find(|&pos| reader.try_read_sync(pos).is_none())
8075 .expect("some position should be cold");
8076 assert_eq!(reader.read(pos).await.unwrap(), pos);
8077 drop(reader);
8078
8079 let buffer = context.encode();
8080 assert!(buffer.contains("miss_read_duration_count 1"), "{buffer}");
8081
8082 journal.destroy().await.unwrap();
8083 });
8084 }
8085
8086 #[test_traced]
8087 fn test_variable_snapshot_frozen_across_roll() {
8088 let executor = deterministic::Runner::default();
8089 executor.start(|context| async move {
8090 let cfg = Config {
8091 partition: "snapshot-frozen".into(),
8092 items_per_section: NZU64!(5),
8093 compression: None,
8094 codec_config: (),
8095 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8096 write_buffer: NZUsize!(1024),
8097 replay_buffer: NZUsize!(1024),
8098 };
8099 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
8100 .await
8101 .unwrap();
8102 for i in 0..7u64 {
8103 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8104 }
8105
8106 let snapshot;
8107 (journal, snapshot) = journal.snapshot().await.unwrap();
8108 assert_eq!(snapshot.bounds(), 0..7);
8109
8110 for i in 7..23u64 {
8113 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8114 }
8115 assert_eq!(snapshot.bounds(), 0..7);
8116 for i in 0..7u64 {
8117 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
8118 }
8119 assert!(matches!(
8120 snapshot.read(7).await,
8121 Err(Error::ItemOutOfRange(7))
8122 ));
8123
8124 let fresh;
8125 (journal, fresh) = journal.snapshot().await.unwrap();
8126 assert_eq!(fresh.bounds(), 0..23);
8127 assert_eq!(fresh.read(22).await.unwrap(), 2200);
8128
8129 drop(snapshot);
8130 drop(fresh);
8131 journal.destroy().await.unwrap();
8132 });
8133 }
8134
8135 #[test_traced]
8136 fn test_variable_prune_under_snapshot() {
8137 let executor = deterministic::Runner::default();
8138 executor.start(|context| async move {
8139 let cfg = Config {
8140 partition: "snapshot-prune".into(),
8141 items_per_section: NZU64!(5),
8142 compression: Some(3),
8143 codec_config: (),
8144 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8145 write_buffer: NZUsize!(1024),
8146 replay_buffer: NZUsize!(1024),
8147 };
8148 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
8149 .await
8150 .unwrap();
8151 for i in 0..17u64 {
8152 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8153 }
8154 journal = journal.sync().await.unwrap();
8155
8156 let snapshot;
8157 (journal, snapshot) = journal.snapshot().await.unwrap();
8158 let pruned;
8159 (journal, pruned) = journal.prune(12).await.unwrap();
8160 assert!(pruned);
8161
8162 assert_eq!(snapshot.bounds(), 0..17);
8164 for i in 0..17u64 {
8165 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
8166 }
8167 assert_eq!(
8168 snapshot.read_many(&[1, 2, 3, 11, 16]).await.unwrap(),
8169 vec![100, 200, 300, 1100, 1600]
8170 );
8171
8172 let fresh;
8173 (journal, fresh) = journal.snapshot().await.unwrap();
8174 assert_eq!(fresh.bounds(), 10..17);
8175 assert!(matches!(fresh.read(3).await, Err(Error::ItemPruned(3))));
8176
8177 drop(snapshot);
8178 drop(fresh);
8179 journal.destroy().await.unwrap();
8180 });
8181 }
8182
8183 #[test_traced]
8184 fn test_variable_snapshots_readable_during_concurrent_appends() {
8185 let executor = deterministic::Runner::default();
8186 executor.start(|context| async move {
8187 let cfg = Config {
8188 partition: "snapshot-concurrent".into(),
8189 items_per_section: NZU64!(5),
8190 compression: None,
8191 codec_config: (),
8192 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8193 write_buffer: NZUsize!(1024),
8194 replay_buffer: NZUsize!(1024),
8195 };
8196 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
8197 .await
8198 .unwrap();
8199
8200 let (mut tx, mut rx) =
8201 futures::channel::mpsc::channel::<Reader<'static, deterministic::Context, u64>>(8);
8202 let validator = context.child("validator").spawn(|_| async move {
8203 let mut validated = 0usize;
8204 while let Some(snapshot) = rx.next().await {
8205 let bounds = snapshot.bounds();
8206 for i in bounds.clone() {
8207 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
8208 }
8209 validated += (bounds.end - bounds.start) as usize;
8210 }
8211 validated
8212 });
8213
8214 for i in 0..40u64 {
8215 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8216 if i % 7 == 0 {
8217 let snapshot;
8218 (journal, snapshot) = journal.snapshot().await.unwrap();
8219 if tx.try_send(snapshot).is_err() {
8220 break;
8221 }
8222 }
8223 }
8224 drop(tx);
8225 assert!(validator.await.unwrap() > 0);
8226
8227 journal.destroy().await.unwrap();
8228 });
8229 }
8230
8231 #[test_traced]
8232 fn test_variable_replay_from_stale_snapshot() {
8233 let executor = deterministic::Runner::default();
8234 executor.start(|context| async move {
8235 let cfg = Config {
8236 partition: "snapshot-replay".into(),
8237 items_per_section: NZU64!(5),
8238 compression: None,
8239 codec_config: (),
8240 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8241 write_buffer: NZUsize!(1024),
8242 replay_buffer: NZUsize!(1024),
8243 };
8244 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
8245 .await
8246 .unwrap();
8247 for i in 0..7u64 {
8248 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8249 }
8250
8251 let snapshot;
8253 (journal, snapshot) = journal.snapshot().await.unwrap();
8254 assert_eq!(snapshot.bounds(), 0..7);
8255
8256 for i in 7..23u64 {
8258 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8259 }
8260 let pruned;
8261 (journal, pruned) = journal.prune(12).await.unwrap();
8262 assert!(pruned);
8263
8264 {
8265 let stream = snapshot
8266 .replay(0, NZUsize!(1024), ReadOptions::default())
8267 .await
8268 .unwrap();
8269 futures::pin_mut!(stream);
8270 let mut expected = 0u64;
8271 while let Some(result) = stream.next().await {
8272 let (pos, item) = result.unwrap();
8273 assert_eq!(pos, expected);
8274 assert_eq!(item, pos * 100);
8275 expected += 1;
8276 }
8277 assert_eq!(expected, 7);
8278 }
8279
8280 drop(snapshot);
8281 journal.destroy().await.unwrap();
8282 });
8283 }
8284
8285 #[test_traced]
8288 fn test_variable_recovery_full_newest_blob_without_successor() {
8289 let executor = deterministic::Runner::default();
8290 executor.start(|context| async move {
8291 let cfg = Config {
8292 partition: "recovery-full-newest-no-successor".into(),
8293 items_per_section: NZU64!(10),
8294 compression: None,
8295 codec_config: (),
8296 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8297 write_buffer: NZUsize!(1024),
8298 replay_buffer: NZUsize!(1024),
8299 };
8300
8301 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8302 .await
8303 .unwrap();
8304 for i in 0..10u64 {
8305 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8306 }
8307 let journal = journal.sync().await.unwrap();
8308 drop(journal);
8309
8310 let data_partition = cfg.data_partition();
8313 context
8314 .remove(&data_partition, Some(&1u64.to_be_bytes()))
8315 .await
8316 .unwrap();
8317
8318 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8319 .await
8320 .unwrap();
8321 assert_eq!(journal.bounds(), 0..10);
8322 for i in 0..10u64 {
8323 assert_eq!(journal.read(i).await.unwrap(), i * 100);
8324 }
8325 let appended;
8326 (journal, appended) = journal.append(&1000).await.unwrap();
8327 assert_eq!(appended, 10);
8328 assert_eq!(journal.read(10).await.unwrap(), 1000);
8329
8330 journal.destroy().await.unwrap();
8331 });
8332 }
8333
8334 #[test_traced]
8338 fn test_variable_rewind_crash_before_data_truncation() {
8339 let executor = deterministic::Runner::default();
8340 executor.start(|context| async move {
8341 let cfg = Config {
8342 partition: "rewind-crash-offsets-only".into(),
8343 items_per_section: NZU64!(10),
8344 compression: None,
8345 codec_config: (),
8346 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8347 write_buffer: NZUsize!(1024),
8348 replay_buffer: NZUsize!(1024),
8349 };
8350
8351 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8352 .await
8353 .unwrap();
8354 for i in 0..25u64 {
8355 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8356 }
8357 let journal = journal.sync().await.unwrap();
8358
8359 let journal = journal.test_rewind_offsets(12).await.unwrap();
8362 drop(journal);
8363
8364 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8365 .await
8366 .unwrap();
8367 assert_eq!(journal.bounds(), 0..25);
8368 for i in 0..25u64 {
8369 assert_eq!(journal.read(i).await.unwrap(), i * 100);
8370 }
8371
8372 journal.destroy().await.unwrap();
8373 });
8374 }
8375
8376 #[test_traced]
8378 fn test_variable_recovery_rejects_gap_in_retained_blobs() {
8379 let executor = deterministic::Runner::default();
8380 executor.start(|context| async move {
8381 let cfg = Config {
8382 partition: "recovery-gap-in-retained-blobs".into(),
8383 items_per_section: NZU64!(10),
8384 compression: None,
8385 codec_config: (),
8386 page_cache: CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(10)),
8387 write_buffer: NZUsize!(1024),
8388 replay_buffer: NZUsize!(1024),
8389 };
8390
8391 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8392 .await
8393 .unwrap();
8394 for i in 0..25u64 {
8395 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8396 }
8397 let journal = journal.sync().await.unwrap();
8398 drop(journal);
8399
8400 context
8402 .remove(&cfg.data_partition(), Some(&1u64.to_be_bytes()))
8403 .await
8404 .unwrap();
8405
8406 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
8407 assert!(matches!(result, Err(Error::Corruption(_))));
8408 });
8409 }
8410}