1#[commonware_macros::stability(ALPHA)]
15use super::checkpoint::Checkpoint;
16use super::{
17 Batch, Contiguous, Decoded, Many, Mutable, blob_first_position,
18 blobs::{Blob, Blobs, Partition, Replay as BlobReplay, Writable},
19 fixed,
20 metrics::Metrics,
21 position_to_blob,
22};
23#[commonware_macros::stability(ALPHA)]
24use crate::journal::authenticated;
25use crate::{
26 Context, SyncCompletion,
27 journal::{
28 Error,
29 durability::Barrier,
30 frame::{
31 FrameInfo, Limited, decode_item, decode_length_prefix, encode_frame_into, find_frame,
32 read_frame_at,
33 },
34 },
35};
36use bytes::{Bytes, BytesMut};
37use commonware_codec::{Codec, CodecShared, Copying, varint::MAX_U32_VARINT_SIZE};
38use commonware_macros::boxed;
39use commonware_runtime::{
40 Blob as RBlob, Buf, Handle, IoBuf, ReadOptions,
41 buffer::paged::{CacheRef, Recovery as PagedRecovery, Replay},
42};
43use commonware_utils::{MAX_ALLOCATION_BYTES, futures::try_join_all};
44use futures::{FutureExt as _, Stream, future::try_join};
45#[cfg(test)]
46use std::future::pending;
47use std::{
48 collections::BTreeMap,
49 iter::once,
50 marker::PhantomData,
51 num::{NonZeroU64, NonZeroUsize},
52 ops::Range,
53 sync::Arc,
54};
55use tracing::warn;
56
57const DATA_SUFFIX: &str = "_data";
59
60const OFFSETS_SUFFIX: &str = "_offsets";
62
63const PREPARED_SPARE_LIMIT: usize = 64 * 1024;
70
71pub struct PreparedAppend<V> {
74 encoded: Vec<u8>,
75 item_starts: Vec<usize>,
76 compressed: bool,
77 _marker: PhantomData<V>,
78}
79
80fn with_bytes<T>(scratch: &mut BytesMut, f: impl FnOnce(&Bytes) -> T) -> T {
82 let bytes = std::mem::take(scratch).split().freeze();
84 let result = f(&bytes);
85 if let Ok(reclaimed) = bytes.try_into_mut() {
86 *scratch = reclaimed;
87 }
88 result
89}
90
91fn read_buffer_len(lengths: impl IntoIterator<Item = usize>) -> Option<usize> {
93 let total = lengths
94 .into_iter()
95 .try_fold(0usize, |total, len| total.checked_add(len))?;
96 (total <= MAX_ALLOCATION_BYTES).then_some(total)
97}
98
99fn decode_frame_span<V: CodecShared>(
103 mut span: Bytes,
104 blob: u64,
105 offset: u64,
106 codec_config: &V::Cfg,
107 compressed: bool,
108) -> Result<V, Error> {
109 let expected_len = span.len();
110 let (size, varint_len) = decode_length_prefix(&mut span)?;
111 let actual_len = size.checked_add(varint_len).ok_or(Error::OffsetOverflow)?;
112 if actual_len != expected_len {
113 return Err(Error::OffsetDataMismatch {
114 section: blob,
115 offset,
116 expected_len,
117 actual_len,
118 });
119 }
120 decode_item::<V>(span, codec_config, compressed)
121}
122
123enum Frame {
125 Item { offset: u64 },
127 End { valid_size: u64, torn: bool },
130}
131
132struct FrameScanner<'a, B: RBlob, V: Codec> {
134 replay: Replay<B>,
135 offset: u64,
137 codec_config: &'a V::Cfg,
138 compressed: bool,
139}
140
141impl<'a, B: RBlob, V: CodecShared> FrameScanner<'a, B, V> {
142 const fn new(replay: Replay<B>, codec_config: &'a V::Cfg, compressed: bool) -> Self {
143 Self {
144 replay,
145 offset: 0,
146 codec_config,
147 compressed,
148 }
149 }
150
151 async fn next(&mut self) -> Result<Frame, Error> {
156 match self.replay.ensure(MAX_U32_VARINT_SIZE).await {
157 Ok(true) => {}
158 Ok(false) if self.replay.remaining() == 0 => {
159 return Ok(Frame::End {
160 valid_size: self.offset,
161 torn: false,
162 });
163 }
164 Ok(false) => {}
166 Err(err) => return Err(err.into()),
167 }
168
169 let before_remaining = self.replay.remaining();
170 let (item_size, varint_len) = match decode_length_prefix(&mut self.replay) {
171 Ok(result) => result,
172 Err(err) => {
173 if self.replay.is_exhausted() || before_remaining < MAX_U32_VARINT_SIZE {
176 return Ok(Frame::End {
177 valid_size: self.offset,
178 torn: true,
179 });
180 }
181 return Err(err);
182 }
183 };
184
185 match self.replay.ensure(item_size).await {
186 Ok(true) => {}
187 Ok(false) => {
188 return Ok(Frame::End {
189 valid_size: self.offset,
190 torn: true,
191 });
192 }
193 Err(err) => return Err(err.into()),
194 }
195
196 let item_offset = self.offset;
197 let next_offset = item_offset
198 .checked_add(varint_len as u64)
199 .and_then(|offset| offset.checked_add(item_size as u64))
200 .ok_or(Error::OffsetOverflow)?;
201 decode_item::<V>(
202 Limited::new(&mut self.replay, item_size),
203 self.codec_config,
204 self.compressed,
205 )?;
206 self.offset = next_offset;
207 Ok(Frame::Item {
208 offset: item_offset,
209 })
210 }
211}
212
213#[derive(Clone, Copy)]
215struct BlobScan {
216 items: u64,
218 valid_size: u64,
220 torn: bool,
222}
223
224struct ReplayState<'a, B: RBlob, V: Codec> {
229 blob: u64,
231 replay: BlobReplay<'a, B>,
233 budget: u64,
235 pos: u64,
237 end_pos: u64,
239 offset: u64,
241 codec_config: V::Cfg,
243 compressed: bool,
245 _marker: PhantomData<V>,
246}
247
248impl<B: RBlob, V: CodecShared> super::BatchDecoder for ReplayState<'_, B, V> {
249 type Item = V;
250
251 async fn next(mut self) -> Option<(Decoded<V>, Self)> {
253 if self.pos == self.end_pos {
254 return None;
255 }
256
257 let start = self.pos;
258 let mut items = Vec::new();
259 let start_offset = self.offset;
260 loop {
261 if self.pos == self.end_pos {
262 break;
263 }
264
265 let buffered = if self.replay.remaining() < MAX_U32_VARINT_SIZE {
270 self.replay.ensure(MAX_U32_VARINT_SIZE).await
271 } else {
272 Ok(true)
273 };
274 match buffered {
275 Ok(true) => {}
276 Ok(false) if self.replay.remaining() == 0 => {
277 let error = Error::Corruption(format!(
278 "data blob {} ended before position {}",
279 self.blob, self.pos
280 ));
281 let decoded = Decoded::failed(start, items, error);
282 return Some((decoded, self));
283 }
284 Ok(false) => {}
285 Err(err) => {
286 let decoded = Decoded::failed(start, items, err);
287 return Some((decoded, self));
288 }
289 }
290
291 let before_remaining = self.replay.remaining();
293 let (item_size, varint_len) = match self.replay.read_length() {
294 Ok(result) => result,
295 Err(err) => {
296 let error =
297 if self.replay.is_exhausted() || before_remaining < MAX_U32_VARINT_SIZE {
298 Error::Corruption(format!(
299 "incomplete frame header in data blob {} at offset {}",
300 self.blob, self.offset
301 ))
302 } else {
303 err
304 };
305 let decoded = Decoded::failed(start, items, error);
306 return Some((decoded, self));
307 }
308 };
309
310 let buffered = if self.replay.remaining() < item_size {
311 self.replay.ensure(item_size).await
312 } else {
313 Ok(true)
314 };
315 match buffered {
316 Ok(true) => {}
317 Ok(false) => {
318 let error = Error::Corruption(format!(
319 "incomplete frame in data blob {} at offset {}",
320 self.blob, self.offset
321 ));
322 let decoded = Decoded::failed(start, items, error);
323 return Some((decoded, self));
324 }
325 Err(err) => {
326 let decoded = Decoded::failed(start, items, err);
327 return Some((decoded, self));
328 }
329 }
330
331 let next_offset = self
332 .offset
333 .checked_add(varint_len as u64)
334 .and_then(|offset| offset.checked_add(item_size as u64));
335 let Some(next_offset) = next_offset else {
336 let decoded = Decoded::failed(start, items, Error::OffsetOverflow);
337 return Some((decoded, self));
338 };
339
340 match self
342 .replay
343 .decode::<V>(item_size, &self.codec_config, self.compressed)
344 {
345 Ok(item) => {
346 self.pos += 1;
347 self.offset = next_offset;
348 items.push(item);
349 }
350 Err(err) => {
351 let decoded = Decoded::failed(start, items, err);
352 return Some((decoded, self));
353 }
354 }
355
356 if self.offset - start_offset >= self.budget
359 || self.replay.remaining() < MAX_U32_VARINT_SIZE
360 {
361 break;
362 }
363 }
364
365 Some((
366 Decoded {
367 batch: Batch { start, items },
368 error: None,
369 },
370 self,
371 ))
372 }
373}
374
375#[derive(Clone)]
377pub struct Config<C> {
378 pub partition: String,
380
381 pub items_per_section: NonZeroU64,
386
387 pub compression: Option<u8>,
392
393 pub codec_config: C,
395
396 pub page_cache: CacheRef,
398
399 pub write_buffer: NonZeroUsize,
401
402 pub replay_buffer: NonZeroUsize,
404}
405
406impl<C> Config<C> {
407 fn data_partition(&self) -> String {
409 format!("{}{}", self.partition, DATA_SUFFIX)
410 }
411
412 fn offsets_partition(&self) -> String {
414 format!("{}{}", self.partition, OFFSETS_SUFFIX)
415 }
416
417 fn offsets_config(&self) -> fixed::Config {
419 fixed::Config {
420 partition: self.offsets_partition(),
421 items_per_blob: self.items_per_section,
422 page_cache: self.page_cache.clone(),
423 write_buffer: self.write_buffer,
424 replay_buffer: self.replay_buffer,
425 }
426 }
427}
428
429struct Inner<E: Context, V: Codec> {
431 blobs: Writable<E>,
433
434 offsets: Box<fixed::Inner<E, u64>>,
437
438 bounds: Range<u64>,
440
441 #[cfg(test)]
444 halt_before_offsets_prune: bool,
445
446 items_per_blob: NonZeroU64,
453
454 compression: Option<u8>,
456
457 codec_config: V::Cfg,
459
460 metrics: Arc<Metrics<E>>,
462
463 barrier: Barrier,
467}
468
469pub struct Reader<'a, E: Context, V: Codec> {
471 data: Blobs<'a, E::Blob>,
473
474 bounds: Range<u64>,
476
477 offsets: fixed::Reader<'a, E, u64>,
479
480 items_per_blob: NonZeroU64,
482
483 codec_config: V::Cfg,
485
486 compressed: bool,
488
489 metrics: Arc<Metrics<E>>,
491}
492
493impl<'a, E: Context, V: CodecShared> Reader<'a, E, V> {
494 const fn validate_readable(&self, position: u64) -> Result<(), Error> {
496 if position >= self.bounds.end {
497 return Err(Error::ItemOutOfRange(position));
498 }
499 if position < self.bounds.start {
500 return Err(Error::ItemPruned(position));
501 }
502 Ok(())
503 }
504
505 async fn read_at_offset(&self, blob: &Blob<'_, E::Blob>, offset: u64) -> Result<V, Error> {
507 read_frame_at(blob, offset, &self.codec_config, self.compressed)
508 .await
509 .map(|(_, _, item)| item)
510 }
511
512 fn try_read_frame_sync(&self, position: u64, offset: u64, buf: &mut BytesMut) -> Option<V> {
515 let blob = self
516 .data
517 .get(position_to_blob(position, self.items_per_blob.get()))?;
518 let remaining = blob.size().checked_sub(offset)?;
519 let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
520 if header_len == 0 {
521 return None;
522 }
523
524 let mut header = [0u8; MAX_U32_VARINT_SIZE];
526 if !blob.try_read_sync_into(&mut header[..header_len], offset) {
527 return None;
528 }
529 let mut cursor = Copying(&header[..header_len]);
530 let (_, item_info) = find_frame(&mut cursor, offset).ok()?;
531
532 let (varint_len, data_len) = match item_info {
533 FrameInfo::Complete {
534 varint_len,
535 data_len,
536 } => (varint_len, data_len),
537 FrameInfo::Incomplete {
538 varint_len,
539 total_len,
540 ..
541 } => (varint_len, total_len),
542 };
543 let item_len = varint_len.checked_add(data_len)?;
544 if item_len > usize::try_from(remaining).ok()? {
545 return None;
546 }
547
548 if item_len <= header_len {
550 return decode_item::<V>(
551 Copying(&header[varint_len..varint_len + data_len]),
552 &self.codec_config,
553 self.compressed,
554 )
555 .ok();
556 }
557
558 buf.resize(item_len, 0);
560 if !blob.try_read_sync_into(buf, offset) {
561 return None;
562 }
563 with_bytes(buf, |bytes| {
564 decode_item::<V>(
565 bytes.slice(varint_len..),
566 &self.codec_config,
567 self.compressed,
568 )
569 .ok()
570 })
571 }
572
573 async fn replay_states(
575 &self,
576 range: Range<u64>,
577 buffer: NonZeroUsize,
578 read_options: ReadOptions,
579 ) -> Result<Vec<ReplayState<'a, E::Blob, V>>, Error> {
580 let bounds = self.bounds();
581 if range.start > range.end || range.end > bounds.end {
582 return Err(Error::ItemOutOfRange(if range.start > range.end {
583 range.start
584 } else {
585 range.end
586 }));
587 }
588 if range.start < bounds.start {
589 return Err(Error::ItemPruned(range.start));
590 }
591
592 let mut states = Vec::new();
593 if range.start < range.end {
594 let items_per_blob = self.items_per_blob.get();
597 let start_pos = range.start;
598 let start_blob = position_to_blob(start_pos, items_per_blob);
599 let end_blob = position_to_blob(range.end - 1, items_per_blob);
600 let start_offset = self.offsets.read(start_pos).await?;
601
602 for blob in start_blob..=end_blob {
603 let blob_handle = self
604 .data
605 .get(blob)
606 .expect("positions in bounds map to a retained blob");
607 let offset = if blob == start_blob { start_offset } else { 0 };
608
609 let first_pos = if blob == start_blob {
610 start_pos
611 } else {
612 blob_first_position(blob, items_per_blob)?
613 };
614 let end_pos = super::blob_end_position(blob, items_per_blob, range.end);
615
616 states.push(ReplayState::<E::Blob, V> {
619 blob,
620 replay: blob_handle.replay_from(offset, buffer, read_options)?,
621 budget: buffer.get() as u64,
622 pos: first_pos,
623 end_pos,
624 offset,
625 codec_config: self.codec_config.clone(),
626 compressed: self.compressed,
627 _marker: PhantomData,
628 });
629 }
630 }
631
632 Ok(states)
633 }
634
635 fn validate_read_many(&self, positions: &[u64]) -> Result<(), Error> {
638 if positions[0] < self.bounds.start {
639 return Err(Error::ItemPruned(positions[0]));
640 }
641 let last_position = *positions.last().expect("positions is not empty");
642 if last_position >= self.bounds.end {
643 return Err(Error::ItemOutOfRange(last_position));
644 }
645 assert!(
646 positions.is_sorted_by(|a, b| a < b),
647 "positions must be strictly increasing"
648 );
649 Ok(())
650 }
651
652 fn next_in_blob(&self, position: u64) -> Option<u64> {
655 let next = position.checked_add(1)?;
656 (next < self.bounds.end && !next.is_multiple_of(self.items_per_blob.get())).then_some(next)
657 }
658
659 fn read_many_sync_pass(&self, positions: &[u64], out: &mut [Option<V>]) -> Vec<Miss> {
663 if positions.is_empty() {
664 return Vec::new();
665 }
666
667 let items_per_blob = self.items_per_blob.get();
672 let mut lookups: Vec<u64> = Vec::with_capacity(positions.len() * 2);
673 for &position in positions {
674 if lookups.last() != Some(&position) {
675 lookups.push(position);
676 }
677 lookups.extend(self.next_in_blob(position));
678 }
679 let offsets = self.offsets.probe_items(&lookups);
680
681 let mut extents: Vec<(usize, u64, usize)> = Vec::with_capacity(positions.len());
686 let mut singles = Vec::new();
687 let mut lookup_idx = 0;
688 let mut next_blob_start = 0;
689 let mut size = None;
690 let mut previous_end = 0;
691 for (idx, &position) in positions.iter().enumerate() {
692 while lookups[lookup_idx] != position {
693 lookup_idx += 1;
694 }
695 if self.validate_readable(position).is_err() {
696 continue;
697 }
698 let Some(offset) = offsets[lookup_idx] else {
699 continue;
700 };
701
702 if position >= next_blob_start {
705 let blob_num = position_to_blob(position, items_per_blob);
706 next_blob_start = (blob_num + 1).saturating_mul(items_per_blob);
707 size = self.data.get(blob_num).map(|blob| blob.size());
708 previous_end = 0;
709 }
710 let Some(size) = size else {
711 continue;
712 };
713
714 let end = match self.next_in_blob(position) {
716 Some(_) => offsets[lookup_idx + 1],
717 None => Some(size),
718 };
719 if offset < previous_end || end.is_some_and(|end| end <= offset || end > size) {
721 extents.clear();
722 singles.clear();
723 break;
724 }
725 let Some(end) = end else {
726 singles.push((idx, offset));
727 continue;
728 };
729 let Ok(len) = usize::try_from(end - offset) else {
730 continue;
731 };
732 previous_end = end;
733 extents.push((idx, offset, len));
734 }
735
736 let mut buf = BytesMut::new();
737 let mut hits = 0u64;
738
739 let mut group_start = 0;
741 while group_start < extents.len() {
742 let blob_num = position_to_blob(positions[extents[group_start].0], items_per_blob);
743 let mut group_end = group_start + 1;
744 while group_end < extents.len()
745 && position_to_blob(positions[extents[group_end].0], items_per_blob) == blob_num
746 {
747 group_end += 1;
748 }
749 let group = &extents[group_start..group_end];
750 group_start = group_end;
751
752 let Some(blob) = self.data.get(blob_num) else {
753 continue;
754 };
755 let ranges: Vec<(u64, usize)> = group
756 .iter()
757 .map(|&(_, offset, len)| (offset, len))
758 .collect();
759 let Some(total) = read_buffer_len(ranges.iter().map(|&(_, len)| len)) else {
760 continue;
761 };
762 buf.resize(total, 0);
763 let missed = blob.try_read_ranges_sync_into(&mut buf, &ranges);
764 with_bytes(&mut buf, |bytes| {
765 let mut missed = missed.into_iter().peekable();
766 let mut local = 0usize;
767 for (range_idx, &(idx, offset, len)) in group.iter().enumerate() {
768 let start = local;
769 local += len;
770 if missed.peek() == Some(&range_idx) {
771 missed.next();
772 continue;
773 }
774
775 let slot = bytes.slice(start..local);
778 if let Ok(item) = decode_frame_span(
779 slot,
780 blob_num,
781 offset,
782 &self.codec_config,
783 self.compressed,
784 ) {
785 out[idx] = Some(item);
786 hits += 1;
787 }
788 }
789 });
790 }
791 for (idx, offset) in singles {
792 if let Some(item) = self.try_read_frame_sync(positions[idx], offset, &mut buf) {
793 out[idx] = Some(item);
794 hits += 1;
795 }
796 }
797 self.metrics.cache_hits.inc_by(hits);
798 self.metrics.items_read.inc_by(hits);
799
800 let mut lookup_idx = 0;
803 let mut misses = Vec::new();
804 for (idx, &position) in positions.iter().enumerate() {
805 if out[idx].is_some() {
806 continue;
807 }
808 while lookups[lookup_idx] != position {
809 lookup_idx += 1;
810 }
811 misses.push(Miss {
812 position,
813 offset: offsets[lookup_idx],
814 end: self
815 .next_in_blob(position)
816 .and_then(|_| offsets[lookup_idx + 1]),
817 });
818 }
819 misses
820 }
821}
822
823#[derive(Clone, Copy)]
825struct Miss {
826 position: u64,
828 offset: Option<u64>,
830 end: Option<u64>,
833}
834
835async fn complete<E: Context, V: CodecShared>(
838 reader: &Reader<'_, E, V>,
839 items: Vec<Option<V>>,
840 misses: Vec<Miss>,
841) -> Result<Vec<V>, Error> {
842 if misses.is_empty() {
843 return Ok(items
844 .into_iter()
845 .map(|item| item.expect("complete probe has no misses"))
846 .collect());
847 }
848
849 let fetched = reader.fetch_misses(&misses).await?;
850 let mut fetched = fetched.into_iter();
851 Ok(items
852 .into_iter()
853 .map(|item| item.unwrap_or_else(|| fetched.next().expect("one fetched item per miss")))
854 .collect())
855}
856
857impl<E: Context, V: CodecShared> Reader<'_, E, V> {
858 fn probe_parts(&self, positions: &[u64]) -> (Vec<Option<V>>, Vec<Miss>) {
861 let mut items: Vec<Option<V>> = (0..positions.len()).map(|_| None).collect();
862 let misses = self.read_many_sync_pass(positions, &mut items);
863 (items, misses)
864 }
865
866 async fn fetch_misses(&self, misses: &[Miss]) -> Result<Vec<V>, Error> {
870 if misses.is_empty() {
871 return Ok(Vec::new());
872 }
873
874 for miss in misses {
877 self.validate_readable(miss.position)?;
878 }
879
880 let mut lookups = Vec::new();
885 for miss in misses {
886 if miss.offset.is_none() {
887 lookups.push(miss.position);
888 }
889 if miss.end.is_none() {
890 lookups.extend(self.next_in_blob(miss.position));
891 }
892 }
893 lookups.dedup();
894
895 let fetched = self
898 .offsets
899 .read_many_inner(&lookups)
900 .await
901 .map_err(|e| match e {
902 Error::ItemOutOfRange(e) | Error::ItemPruned(e) => {
903 Error::Corruption(format!("blob/item should be found, but got: {e}"))
904 }
905 other => other,
906 })?;
907
908 let mut cursor = 0;
911 let mut lookup = |position: u64| {
912 while lookups[cursor] < position {
913 cursor += 1;
914 }
915 assert_eq!(
916 lookups[cursor], position,
917 "every requested position must be in `lookups`"
918 );
919 fetched[cursor]
920 };
921
922 let items_per_blob = self.items_per_blob.get();
926 let mut groups = Vec::new();
927 for misses in misses.chunk_by(|a, b| {
928 position_to_blob(a.position, items_per_blob)
929 == position_to_blob(b.position, items_per_blob)
930 }) {
931 let blob_num = position_to_blob(misses[0].position, items_per_blob);
932 let blob = self
933 .data
934 .get(blob_num)
935 .expect("positions in bounds map to a retained blob");
936 let size = blob.size();
937 let mut ranges = Vec::with_capacity(misses.len());
938 for miss in misses {
939 let offset = miss.offset.unwrap_or_else(|| lookup(miss.position));
940 let end = miss
941 .end
942 .or_else(|| self.next_in_blob(miss.position).map(&mut lookup))
943 .unwrap_or(size);
944
945 let previous_end = ranges
951 .last()
952 .map_or(0, |&(offset, len)| offset + len as u64);
953 if end <= offset || offset < previous_end || end > size {
954 return Err(Error::Corruption(format!(
955 "inconsistent offsets in blob {blob_num} at position {}",
956 miss.position
957 )));
958 }
959 let len = usize::try_from(end - offset).map_err(|_| Error::OffsetOverflow)?;
960 ranges.push((offset, len));
961 }
962 groups.push((blob_num, blob, ranges));
963 }
964 let groups = try_join_all(
965 groups
966 .into_iter()
967 .map(|(blob_num, blob, ranges)| self.read_frames(blob_num, blob, ranges)),
968 )
969 .await?;
970
971 self.metrics.cache_misses.inc_by(misses.len() as u64);
972 self.metrics.items_read.inc_by(misses.len() as u64);
973 Ok(groups.into_iter().flatten().collect())
974 }
975
976 async fn read_frames(
980 &self,
981 blob_num: u64,
982 blob: Blob<'_, E::Blob>,
983 ranges: Vec<(u64, usize)>,
984 ) -> Result<Vec<V>, Error> {
985 let total =
987 read_buffer_len(ranges.iter().map(|&(_, len)| len)).ok_or(Error::OffsetOverflow)?;
988 let mut spans: Vec<(u64, usize)> = Vec::with_capacity(ranges.len());
989 for &(offset, len) in &ranges {
990 match spans.last_mut() {
991 Some((start, span)) if *start + *span as u64 == offset => *span += len,
992 _ => spans.push((offset, len)),
993 }
994 }
995 let mut buf = vec![0u8; total];
996 blob.read_ranges_into(&mut buf, &spans).await?;
997 let mut bytes = Bytes::from(buf);
998 let mut items = Vec::with_capacity(ranges.len());
999 for &(offset, len) in &ranges {
1000 let span = bytes.split_to(len);
1001 items.push(decode_frame_span(
1002 span,
1003 blob_num,
1004 offset,
1005 &self.codec_config,
1006 self.compressed,
1007 )?);
1008 }
1009 Ok(items)
1010 }
1011}
1012
1013impl<E: Context, V: CodecShared> super::Contiguous for Reader<'_, E, V> {
1014 type Item = V;
1015
1016 fn bounds(&self) -> Range<u64> {
1017 self.bounds.clone()
1018 }
1019
1020 async fn read(&self, position: u64) -> Result<V, Error> {
1021 self.metrics.read_calls.inc();
1022 self.validate_readable(position)?;
1023
1024 let cached_offset = self.offsets.try_read_sync(position);
1028 if let Some(offset) = cached_offset
1029 && let Some(item) = self.try_read_frame_sync(position, offset, &mut BytesMut::new())
1030 {
1031 self.metrics.cache_hits.inc();
1032 self.metrics.items_read.inc();
1033 return Ok(item);
1034 }
1035
1036 let _timer = self.metrics.read_timer();
1037 let offset = match cached_offset {
1038 Some(offset) => offset,
1039 None => self.offsets.read_miss(position).await?,
1040 };
1041 let blob = self
1042 .data
1043 .get(position_to_blob(position, self.items_per_blob.get()))
1044 .expect("position in bounds maps to a retained blob");
1045 self.metrics.cache_misses.inc();
1046 let item = self.read_at_offset(&blob, offset).await?;
1047 self.metrics.items_read.inc();
1048 Ok(item)
1049 }
1050
1051 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
1052 if positions.is_empty() {
1053 return Ok(Vec::new());
1054 }
1055 let _timer = self.metrics.read_many_timer();
1056 self.metrics.read_many_calls.inc();
1057 self.validate_read_many(positions)?;
1058 let (items, misses) = self.probe_parts(positions);
1059 complete(self, items, misses).await
1060 }
1061
1062 fn try_read_sync(&self, position: u64) -> Option<V> {
1063 self.validate_readable(position).ok()?;
1064 let offset = self.offsets.try_read_sync(position)?;
1065 let item = self.try_read_frame_sync(position, offset, &mut BytesMut::new())?;
1066 self.metrics.cache_hits.inc();
1067 self.metrics.items_read.inc();
1068 Some(item)
1069 }
1070
1071 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
1072 assert!(
1073 positions.is_sorted_by(|a, b| a < b),
1074 "positions must be strictly increasing"
1075 );
1076 let mut items: Vec<Option<V>> = (0..positions.len()).map(|_| None).collect();
1077 self.read_many_sync_pass(positions, &mut items);
1078 items
1079 }
1080
1081 async fn replay(
1082 &self,
1083 range: Range<u64>,
1084 buffer: NonZeroUsize,
1085 read_options: ReadOptions,
1086 ) -> Result<impl Stream<Item = Result<Batch<V>, Error>> + Send, Error> {
1087 let states = self.replay_states(range, buffer, read_options).await?;
1088
1089 Ok(super::replay_stream_from_states(states))
1090 }
1091}
1092
1093pub struct Recovery<E: Context, V: CodecShared> {
1095 context: E,
1097
1098 cfg: Config<V::Cfg>,
1100
1101 partition: Partition<E>,
1103
1104 pending: BTreeMap<u64, PagedRecovery<E::Blob>>,
1106
1107 discarded: Vec<u64>,
1110
1111 recovered_scans: BTreeMap<u64, BlobScan>,
1113
1114 offsets: Box<fixed::Recovery<E, u64>>,
1117
1118 bounds: Range<u64>,
1120
1121 bounded: bool,
1124
1125 #[cfg(test)]
1128 halt_after_data_removal: bool,
1129}
1130
1131impl<E: Context, V: CodecShared> Recovery<E, V> {
1132 #[boxed]
1134 async fn open(context: E, cfg: Config<V::Cfg>, max_size: Option<u64>) -> Result<Self, Error> {
1135 let items_per_blob = cfg.items_per_section.get();
1136 let data_partition = cfg.data_partition();
1137 let data_context = context.child("data");
1138
1139 let offsets = fixed::Recovery::<E, u64>::init_cleared(
1143 context.child("offsets"),
1144 cfg.offsets_config(),
1145 max_size,
1146 || Partition::<E>::remove_all(&data_context, &data_partition),
1147 )
1148 .await?;
1149
1150 let partition = Partition::new(
1151 data_context,
1152 data_partition,
1153 cfg.page_cache.clone(),
1154 cfg.write_buffer,
1155 );
1156 let (mut pending, discarded) = match max_size {
1157 Some(size) => partition.open_bounded(size, items_per_blob).await?,
1158 None => (partition.open_all().await?, Vec::new()),
1159 };
1160
1161 let floor = offsets.recovery_watermark().max(offsets.pruning_boundary());
1164 let floor_blob = position_to_blob(floor, items_per_blob);
1165
1166 let suspects: Vec<u64> = pending.keys().rev().take(2).copied().collect();
1175 for blob in suspects {
1176 if blob < floor_blob {
1179 continue;
1180 }
1181
1182 let writer = pending.get(&blob).expect("suspect blob is present");
1185 let valid = writer
1186 .recoverable_prefix_len(0, cfg.replay_buffer, ReadOptions::default())
1187 .await?;
1188 let size = writer.size();
1189 if valid == size {
1190 continue;
1191 }
1192
1193 if blob == floor_blob
1198 && floor > blob_first_position(blob, items_per_blob)?
1199 && floor > offsets.pruning_boundary()
1200 && valid <= offsets.item(floor - 1).await?
1201 {
1202 return Err(Error::Corruption(format!(
1203 "blob {blob} no longer backs acknowledged items: well-formed prefix {valid} \
1204 of size {size}"
1205 )));
1206 }
1207 warn!(blob, valid, size, "truncating to last well-formed page");
1208 let writer = pending.remove(&blob).expect("suspect blob is present");
1209 pending.insert(blob, writer.truncate(valid).await?);
1210 }
1211
1212 Self {
1213 context,
1214 cfg,
1215 partition,
1216 pending,
1217 discarded,
1218 recovered_scans: BTreeMap::new(),
1219 offsets: Box::new(offsets),
1220 bounds: 0..0,
1221 bounded: max_size.is_some(),
1222 #[cfg(test)]
1223 halt_after_data_removal: false,
1224 }
1225 .inspect(max_size.unwrap_or(u64::MAX))
1226 .await
1227 }
1228
1229 #[commonware_macros::stability(ALPHA)]
1240 pub(crate) async fn span(context: E, cfg: &Config<V::Cfg>) -> Result<Range<u64>, Error> {
1241 let per_blob = cfg.items_per_section.get();
1242 let offsets_context = context.child("offsets");
1243 let offsets_cfg = cfg.offsets_config();
1244 let checkpoint =
1245 Checkpoint::open(offsets_context.child("meta"), &offsets_cfg.partition).await?;
1246 if let Some(target) = checkpoint.clear_target() {
1247 return Ok(target..target);
1248 }
1249 let offsets =
1250 fixed::Recovery::<E, u64>::span(&offsets_context, &offsets_cfg, &checkpoint).await?;
1251 let names = Partition::<E>::scan_names(&context, &cfg.data_partition()).await?;
1252 let data = Partition::<E>::indices(names)?;
1253 let (Some(&oldest), Some(&newest)) = (data.first(), data.last()) else {
1254 if checkpoint
1255 .watermark()
1256 .is_some_and(|watermark| watermark > offsets.start)
1257 {
1258 return Err(Error::Corruption(
1259 "retained offsets have no data blobs".into(),
1260 ));
1261 }
1262 return Ok(offsets.start..offsets.start);
1263 };
1264 let start = offsets.start.max(blob_first_position(oldest, per_blob)?);
1265 let end = blob_first_position(newest, per_blob)?.saturating_add(per_blob);
1266 Ok(start..end)
1267 }
1268
1269 #[commonware_macros::stability(ALPHA)]
1277 pub(crate) async fn unbounded(self) -> Result<Self, Error> {
1278 let Self {
1279 context,
1280 cfg,
1281 partition,
1282 pending,
1283 offsets,
1284 ..
1285 } = self;
1286
1287 drop(pending);
1290 drop(offsets);
1291 drop(partition);
1292 Self::open(context, cfg, None).await
1293 }
1294
1295 #[commonware_macros::stability(ALPHA)]
1300 fn cleared(
1301 context: E,
1302 cfg: Config<V::Cfg>,
1303 offsets: fixed::Recovery<E, u64>,
1304 size: u64,
1305 ) -> Self {
1306 let partition = Partition::new(
1307 context.child("data"),
1308 cfg.data_partition(),
1309 cfg.page_cache.clone(),
1310 cfg.write_buffer,
1311 );
1312 Self {
1313 context,
1314 cfg,
1315 partition,
1316 pending: BTreeMap::new(),
1317 discarded: Vec::new(),
1318 recovered_scans: BTreeMap::new(),
1319 offsets: Box::new(offsets),
1320 bounds: size..size,
1321 bounded: false,
1322 #[cfg(test)]
1323 halt_after_data_removal: false,
1324 }
1325 }
1326
1327 async fn inspect(mut self, ceiling: u64) -> Result<Self, Error> {
1329 let per_blob = self.cfg.items_per_section.get();
1332 let offsets_start = self.offsets.pruning_boundary();
1333 let oldest = self
1334 .pending
1335 .keys()
1336 .next()
1337 .copied()
1338 .or_else(|| self.discarded.first().copied());
1339 let Some(oldest) = oldest else {
1340 let end = self.offsets.size();
1341 if self.bounded && end > offsets_start {
1342 return Err(Error::Corruption(
1343 "retained offsets have no data blobs".into(),
1344 ));
1345 }
1346 self.bounds = end..end;
1347 return Ok(self);
1348 };
1349 let data_start = blob_first_position(oldest, per_blob)?;
1350 let start = offsets_start.max(data_start);
1351 if ceiling < start {
1352 return Err(Error::ItemPruned(ceiling));
1353 }
1354 if self.offsets.size() < data_start {
1355 return Err(Error::Corruption(
1356 "offsets end precedes retained data".into(),
1357 ));
1358 }
1359 if position_to_blob(offsets_start, per_blob) > oldest {
1360 return Err(Error::Corruption(
1361 "offsets start precedes no retained data blob".into(),
1362 ));
1363 }
1364 if ceiling == start {
1365 self.bounds = start..start;
1366 return Ok(self);
1367 }
1368 let anchor = self.offsets.recovery_watermark().max(start);
1369 let mut blob = position_to_blob(anchor, per_blob);
1370
1371 let mut expected = position_to_blob(start, per_blob);
1373 for (&retained, _) in self.pending.range(expected..blob) {
1374 if retained != expected {
1375 return Err(Error::Corruption(format!(
1376 "missing acknowledged data blob {expected}"
1377 )));
1378 }
1379 expected += 1;
1380 }
1381 if expected != blob
1382 || (anchor > blob_first_position(blob, per_blob)? && !self.pending.contains_key(&blob))
1383 {
1384 return Err(Error::Corruption(format!(
1385 "missing acknowledged data blob {expected}"
1386 )));
1387 }
1388
1389 let offsets_end = self.offsets.size();
1394 if anchor < ceiling {
1395 self.offsets = self.offsets.truncate(anchor).await?;
1396 }
1397 let mut end = anchor;
1398 while let Some(writer) = self.pending.remove(&blob) {
1399 let first = blob_first_position(blob, per_blob)?.max(start);
1402 let limit = super::blob_end_position(blob, per_blob, ceiling);
1403 let physical_size = writer.size();
1404 let (writer, replay) = writer
1405 .replay(self.cfg.replay_buffer, ReadOptions::default())
1406 .await?;
1407 self.pending.insert(blob, writer);
1408 let mut scanner = FrameScanner::<E::Blob, V>::new(
1409 replay,
1410 &self.cfg.codec_config,
1411 self.cfg.compression.is_some(),
1412 );
1413 let mut pos = first;
1414 while pos < limit {
1415 match scanner.next().await? {
1416 Frame::Item { offset } => {
1417 if pos >= anchor {
1418 self.offsets = self.offsets.append(&offset).await?;
1419 }
1420 pos = pos.checked_add(1).ok_or(Error::OffsetOverflow)?;
1421 }
1422 Frame::End { .. } => break,
1423 }
1424 }
1425 if pos < anchor {
1426 let message = if self
1427 .pending
1428 .keys()
1429 .next_back()
1430 .is_some_and(|&last| last > blob)
1431 {
1432 format!("data blobs shorter than offsets recovery watermark {anchor}")
1433 } else {
1434 format!(
1435 "offsets recovery watermark {anchor} exceeds retained data end {pos} (offsets bounds {}..{})",
1436 self.offsets.pruning_boundary(),
1437 offsets_end
1438 )
1439 };
1440 return Err(Error::Corruption(message));
1441 }
1442 end = pos;
1443
1444 if pos == limit && pos != ceiling && matches!(scanner.next().await?, Frame::Item { .. })
1447 {
1448 return Err(Error::Corruption(format!(
1449 "blob {blob} exceeds its item capacity"
1450 )));
1451 }
1452 self.recovered_scans.insert(
1453 blob,
1454 BlobScan {
1455 items: pos - first,
1456 valid_size: scanner.offset,
1457 torn: scanner.offset < physical_size,
1458 },
1459 );
1460 if pos < limit || pos == ceiling {
1461 break;
1462 }
1463 blob = blob.checked_add(1).ok_or(Error::OffsetOverflow)?;
1464 }
1465 self.bounds = start..end;
1466 Ok(self)
1467 }
1468
1469 async fn terminal_offset(&self, size: u64) -> Result<u64, Error> {
1471 let per_blob = self.cfg.items_per_section.get();
1472 let blob = position_to_blob(size, per_blob);
1473 let first = blob_first_position(blob, per_blob)?.max(self.bounds.start);
1474 if size == first {
1475 return Ok(0);
1476 }
1477 if size == self.bounds.end
1478 && let Some(scan) = self.recovered_scans.get(&blob)
1479 {
1480 return Ok(scan.valid_size);
1481 }
1482 self.offsets.item(size).await
1483 }
1484
1485 async fn publish(mut self, max_size: u64) -> Result<Inner<E, V>, Error> {
1487 let size = max_size.min(self.bounds.end);
1488 if size < self.bounds.start {
1489 return Err(Error::ItemPruned(size));
1490 }
1491 let per_blob = self.cfg.items_per_section.get();
1492
1493 let terminal = self.terminal_offset(size).await?;
1496 self.offsets = self.offsets.truncate(size).await?;
1497
1498 (self.offsets, _) = self.offsets.prune(self.bounds.start).await?;
1502
1503 let tail = position_to_blob(size, per_blob);
1506
1507 let retained = || {
1510 self.recovered_scans
1511 .range(..tail)
1512 .map(|(&blob, scan)| (blob, scan.valid_size))
1513 .chain(once((tail, terminal)))
1514 };
1515 let repair_data = !self.discarded.is_empty()
1516 || self
1517 .pending
1518 .keys()
1519 .next_back()
1520 .is_some_and(|&blob| blob > tail)
1521 || retained().any(|(blob, bytes)| {
1522 self.pending
1523 .get(&blob)
1524 .is_some_and(|writer| bytes < writer.size())
1525 });
1526 if repair_data {
1527 for blob in self.discarded.into_iter().rev() {
1529 self.partition.remove(blob).await?;
1530 }
1531 Inner::<E, V>::remove_blobs_after(&self.partition, &mut self.pending, tail).await?;
1532 #[cfg(test)]
1533 if self.halt_after_data_removal {
1534 pending::<()>().await;
1535 }
1536
1537 for (blob, bytes) in retained() {
1539 if self
1540 .pending
1541 .get(&blob)
1542 .is_some_and(|writer| bytes < writer.size())
1543 {
1544 let writer = self.pending.remove(&blob).expect("blob is present");
1545 self.pending.insert(blob, writer.truncate(bytes).await?);
1546 }
1547 }
1548 }
1549
1550 self.recovered_scans.retain(|&blob, _| blob <= tail);
1552 for scan in self.recovered_scans.values_mut() {
1553 scan.torn = false;
1554 }
1555 if self.pending.contains_key(&tail) {
1556 let first = blob_first_position(tail, per_blob)?.max(self.bounds.start);
1557 self.recovered_scans.insert(
1558 tail,
1559 BlobScan {
1560 items: size - first,
1561 valid_size: terminal,
1562 torn: false,
1563 },
1564 );
1565 }
1566
1567 let (offsets, bounds) = Inner::<E, V>::align(
1569 &self.partition,
1570 &mut self.pending,
1571 self.offsets,
1572 per_blob,
1573 self.cfg.replay_buffer,
1574 &self.cfg.codec_config,
1575 self.cfg.compression.is_some(),
1576 &self.recovered_scans,
1577 )
1578 .await?;
1579 if bounds.end > size {
1580 return Err(Error::Corruption(
1581 "recovery exceeded its selected end".into(),
1582 ));
1583 }
1584 let tail = position_to_blob(bounds.end, per_blob);
1585 let blobs = Writable::recover(self.partition, self.pending, tail).await?;
1586 let barrier = Barrier::new(offsets.recovery_watermark());
1587
1588 let offsets = Box::new(offsets.publish(bounds.end).await?);
1590 let metrics = Metrics::new(self.context);
1591 metrics.update(bounds.end, bounds.start, per_blob);
1592 Ok(Inner {
1593 blobs,
1594 offsets,
1595 bounds,
1596 #[cfg(test)]
1597 halt_before_offsets_prune: false,
1598 items_per_blob: self.cfg.items_per_section,
1599 compression: self.cfg.compression,
1600 codec_config: self.cfg.codec_config,
1601 metrics: Arc::new(metrics),
1602 barrier,
1603 })
1604 }
1605}
1606
1607impl<E: Context, V: CodecShared> Inner<E, V> {
1608 #[boxed]
1610 pub(crate) async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
1611 Recovery::<E, V>::open(context, cfg, None)
1612 .await?
1613 .publish(u64::MAX)
1614 .await
1615 }
1616
1617 #[commonware_macros::stability(ALPHA)]
1619 pub(crate) async fn init_at_size(
1620 context: E,
1621 cfg: Config<V::Cfg>,
1622 size: u64,
1623 ) -> Result<Self, Error> {
1624 let offsets_partition = cfg.offsets_partition();
1625 let offsets_context = context.child("offsets");
1626
1627 Partition::select(&offsets_context, &offsets_partition).await?;
1629
1630 if size == u64::MAX {
1633 return Err(Error::SizeOverflow);
1634 }
1635 let checkpoint =
1636 Checkpoint::open(offsets_context.child("meta"), &offsets_partition).await?;
1637
1638 let data_partition = cfg.data_partition();
1641 let offsets = fixed::Recovery::<E, u64>::open_cleared(
1642 offsets_context,
1643 cfg.offsets_config(),
1644 checkpoint,
1645 size,
1646 || Partition::<E>::remove_all(&context, &data_partition),
1647 )
1648 .await?;
1649 Recovery::<E, V>::cleared(context, cfg, offsets, size)
1650 .publish(u64::MAX)
1651 .await
1652 }
1653
1654 pub(crate) async fn append(self: Box<Self>, item: &V) -> Result<(Box<Self>, u64), Error> {
1656 let _timer = self.metrics.append_timer();
1657 self.metrics.append_calls.inc();
1658 self.append_many_inner(Many::Flat(std::slice::from_ref(item)))
1659 .await
1660 }
1661
1662 pub(crate) async fn append_many(
1664 self: Box<Self>,
1665 items: Many<'_, V>,
1666 ) -> Result<(Box<Self>, u64), Error> {
1667 let _timer = self.metrics.append_many_timer();
1668 self.metrics.append_many_calls.inc();
1669 self.append_many_inner(items).await
1670 }
1671
1672 async fn append_many_inner(
1673 self: Box<Self>,
1674 items: Many<'_, V>,
1675 ) -> Result<(Box<Self>, u64), Error> {
1676 let prepared = self.prepare_append::<false>(items)?;
1677 self.write_encoded(prepared).await
1678 }
1679
1680 pub(crate) fn prepare_append<const COMPACT: bool>(
1683 &self,
1684 items: Many<'_, V>,
1685 ) -> Result<PreparedAppend<V>, Error> {
1686 let mut encoded = Vec::new();
1687 let mut item_starts = Vec::with_capacity(items.len());
1688 let mut encode = |item: &V| {
1689 item_starts.push(encoded.len());
1690 encode_frame_into(self.compression, item, &mut encoded)
1691 };
1692 match items {
1693 Many::Flat(items) => {
1694 for item in items {
1695 encode(item)?;
1696 }
1697 }
1698 Many::Nested(nested_items) => {
1699 for items in nested_items {
1700 for item in *items {
1701 encode(item)?;
1702 }
1703 }
1704 }
1705 }
1706
1707 if COMPACT && self.compression.is_some() {
1712 let (len, capacity) = (encoded.len(), encoded.capacity());
1713 if capacity - len > PREPARED_SPARE_LIMIT && len <= capacity / 4 {
1714 encoded.shrink_to_fit();
1715 }
1716 }
1717
1718 Ok(PreparedAppend {
1719 encoded,
1720 item_starts,
1721 compressed: self.compression.is_some(),
1722 _marker: PhantomData,
1723 })
1724 }
1725
1726 pub(crate) async fn append_prepared(
1728 self: Box<Self>,
1729 prepared: PreparedAppend<V>,
1730 ) -> Result<(Box<Self>, u64), Error> {
1731 let _timer = self.metrics.append_prepared_timer();
1732 self.metrics.append_prepared_calls.inc();
1733 self.write_encoded(prepared).await
1734 }
1735
1736 async fn write_encoded(
1738 mut self: Box<Self>,
1739 prepared: PreparedAppend<V>,
1740 ) -> Result<(Box<Self>, u64), Error> {
1741 let PreparedAppend {
1742 encoded,
1743 item_starts,
1744 compressed,
1745 ..
1746 } = prepared;
1747 let items_count = item_starts.len();
1748 if items_count == 0 {
1749 return Err(Error::EmptyAppend);
1750 }
1751 if compressed != self.compression.is_some() {
1752 return Err(Error::InvalidConfiguration(
1753 "prepared append compression setting does not match journal".into(),
1754 ));
1755 }
1756 let encoded = IoBuf::from(encoded);
1757
1758 self.bounds
1761 .end
1762 .checked_add(items_count as u64)
1763 .ok_or(Error::SizeOverflow)?;
1764
1765 let items_per_blob = self.items_per_blob.get();
1767 let mut written = 0;
1768 while written < items_count {
1769 let batch_count = super::batch_count_to_blob_boundary(
1770 self.bounds.end,
1771 items_count - written,
1772 items_per_blob,
1773 );
1774 let batch_start = item_starts[written];
1775 let batch_end = item_starts
1776 .get(written + batch_count)
1777 .copied()
1778 .unwrap_or(encoded.len());
1779
1780 let base_offset = match self
1783 .blobs
1784 .try_append(&encoded.as_ref()[batch_start..batch_end])
1785 {
1786 Some(offset) => offset,
1787 None => {
1788 let (blobs, offset) = self
1789 .blobs
1790 .append_owned(encoded.slice(batch_start..batch_end))
1791 .await?;
1792 self.blobs = blobs;
1793 offset
1794 }
1795 };
1796
1797 let absolute_offsets = item_starts[written..written + batch_count]
1799 .iter()
1800 .map(|&start| {
1801 base_offset
1802 .checked_add((start - batch_start) as u64)
1803 .ok_or(Error::OffsetOverflow)
1804 })
1805 .collect::<Result<Vec<u64>, _>>()?;
1806
1807 let (offsets, last_offsets_pos) = self
1809 .offsets
1810 .append_many(Many::Flat(&absolute_offsets))
1811 .await?;
1812 self.offsets = offsets;
1813 assert_eq!(last_offsets_pos, self.bounds.end + batch_count as u64 - 1);
1814
1815 self.bounds.end += batch_count as u64;
1816 written += batch_count;
1817
1818 if self.bounds.end.is_multiple_of(items_per_blob) {
1820 self.blobs = self.blobs.seal_tail().await?;
1821 }
1822 }
1823
1824 self.metrics.update(
1825 self.bounds.end,
1826 self.bounds.start,
1827 self.items_per_blob.get(),
1828 );
1829 let position = self.bounds.end - 1;
1830 Ok((self, position))
1831 }
1832
1833 pub(crate) async fn snapshot(
1835 mut self: Box<Self>,
1836 ) -> Result<(Box<Self>, Reader<'static, E, V>), Error> {
1837 let (blobs, data) = self.blobs.snapshot().await?;
1838 self.blobs = blobs;
1839 let (offsets, offsets_reader) = self.offsets.snapshot().await?;
1840 self.offsets = offsets;
1841 let reader = Reader {
1842 data,
1843 bounds: self.bounds.clone(),
1844 offsets: offsets_reader,
1845 items_per_blob: self.items_per_blob,
1846 codec_config: self.codec_config.clone(),
1847 compressed: self.compression.is_some(),
1848 metrics: self.metrics.clone(),
1849 };
1850 Ok((self, reader))
1851 }
1852
1853 fn reader(&self) -> Reader<'_, E, V> {
1855 Reader {
1856 data: self.blobs.reader(),
1857 bounds: self.bounds.clone(),
1858 offsets: self.offsets.reader(),
1859 items_per_blob: self.items_per_blob,
1860 codec_config: self.codec_config.clone(),
1861 compressed: self.compression.is_some(),
1862 metrics: self.metrics.clone(),
1863 }
1864 }
1865
1866 pub const fn size(&self) -> u64 {
1869 self.bounds.end
1870 }
1871
1872 pub(crate) async fn prune(
1874 mut self: Box<Self>,
1875 min_position: u64,
1876 ) -> Result<(Box<Self>, bool), Error> {
1877 let items_per_blob = self.items_per_blob.get();
1878
1879 let target_blob = position_to_blob(min_position, items_per_blob);
1882 let tail_blob = position_to_blob(self.bounds.end, items_per_blob);
1883 let min_blob = target_blob.min(tail_blob);
1884
1885 if min_blob <= self.blobs.oldest_blob_index() {
1886 return Ok((self, false));
1887 }
1888
1889 let new_boundary = blob_first_position(min_blob, items_per_blob)?;
1890
1891 let (blobs, data_sync) = self.blobs.start_sync().await?;
1903 self.blobs = blobs;
1904 data_sync.await?;
1905 self.offsets = self.offsets.commit().await?;
1906 self.barrier.mark_durable(self.bounds.end);
1907
1908 self.blobs = self.blobs.prune(min_blob).await?;
1909 self.bounds.start = new_boundary;
1910
1911 #[cfg(test)]
1912 if self.halt_before_offsets_prune {
1913 std::future::pending::<()>().await;
1914 }
1915
1916 let (offsets, _) = self.offsets.prune(new_boundary).await?;
1919 self.offsets = offsets;
1920 self.metrics.update(
1921 self.bounds.end,
1922 self.bounds.start,
1923 self.items_per_blob.get(),
1924 );
1925
1926 Ok((self, true))
1927 }
1928
1929 pub(crate) async fn start_sync(mut self: Box<Self>) -> Result<(Box<Self>, Handle<()>), Error> {
1931 self.metrics.start_sync_calls.inc();
1932
1933 let ((blobs, data), (offsets_journal, offsets)) =
1935 try_join(self.blobs.start_sync(), self.offsets.start_data_sync()).await?;
1936 self.blobs = blobs;
1937
1938 let size = self.barrier.boundary();
1940 let (offsets_journal, watermark_handle) =
1941 offsets_journal.start_watermark_sync(size).await?;
1942 self.offsets = offsets_journal;
1943
1944 let journal_completion: SyncCompletion =
1946 async move { try_join(data, offsets).await.map(|_| ()) }
1947 .boxed()
1948 .shared();
1949 self.barrier
1950 .record(self.bounds.end, journal_completion.clone());
1951
1952 let handle = Handle::from_future(async move {
1954 journal_completion.await?;
1955 watermark_handle.await
1956 });
1957 Ok((self, handle))
1958 }
1959
1960 pub(crate) async fn commit(mut self: Box<Self>) -> Result<Box<Self>, Error> {
1962 let _timer = self.metrics.commit_timer();
1963 self.metrics.commit_calls.inc();
1964 let (blobs, handle) = self.blobs.start_sync().await?;
1965 self.blobs = blobs;
1966 handle.await?;
1967 Ok(self)
1968 }
1969
1970 pub(crate) async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
1972 let _timer = self.metrics.sync_timer();
1973 self.metrics.sync_calls.inc();
1974
1975 let size = self.bounds.end;
1977 let (blobs, handle) = self.blobs.start_sync().await?;
1978 self.blobs = blobs;
1979 handle.await?;
1980 self.offsets = self.offsets.sync().await?;
1981 self.barrier.mark_durable(size);
1982 Ok(self)
1983 }
1984
1985 pub(crate) async fn destroy(self) -> Result<(), Error> {
1987 let offsets = self.offsets.wait_for_sync().await?;
1990 self.blobs.destroy().await?;
1991 offsets.destroy().await
1992 }
1993
1994 #[commonware_macros::stability(ALPHA)]
2002 pub(crate) async fn clear_to_size(
2003 mut self: Box<Self>,
2004 new_size: u64,
2005 ) -> Result<Box<Self>, Error> {
2006 if new_size < self.bounds.end {
2007 return Err(Error::ItemOutOfRange(new_size));
2008 }
2009
2010 self.offsets = self.offsets.stage_clear_intent(new_size).await?;
2013 self.blobs = self
2014 .blobs
2015 .clear(position_to_blob(new_size, self.items_per_blob.get()))
2016 .await?;
2017 self.offsets = self.offsets.clear_to_size(new_size).await?;
2018
2019 self.bounds = new_size..new_size;
2020 self.barrier = Barrier::new(new_size);
2021 self.metrics.update(
2022 self.bounds.end,
2023 self.bounds.start,
2024 self.items_per_blob.get(),
2025 );
2026 Ok(self)
2027 }
2028
2029 async fn scan_blob(
2031 writer: PagedRecovery<E::Blob>,
2032 buffer: NonZeroUsize,
2033 codec_config: &V::Cfg,
2034 compressed: bool,
2035 ) -> Result<(PagedRecovery<E::Blob>, BlobScan), Error> {
2036 let (writer, replay) = writer.replay(buffer, ReadOptions::default()).await?;
2037 let mut scanner = FrameScanner::<E::Blob, V>::new(replay, codec_config, compressed);
2038 let mut items = 0u64;
2039 loop {
2040 match scanner.next().await? {
2041 Frame::Item { .. } => items += 1,
2042 Frame::End { valid_size, torn } => {
2043 let scan = BlobScan {
2044 items,
2045 valid_size,
2046 torn,
2047 };
2048 return Ok((writer, scan));
2049 }
2050 }
2051 }
2052 }
2053
2054 #[allow(clippy::too_many_arguments)]
2063 async fn align(
2064 partition: &Partition<E>,
2065 pending: &mut BTreeMap<u64, PagedRecovery<E::Blob>>,
2066 mut offsets: Box<fixed::Recovery<E, u64>>,
2067 items_per_blob: u64,
2068 buffer: NonZeroUsize,
2069 codec_config: &V::Cfg,
2070 compressed: bool,
2071 scans: &BTreeMap<u64, BlobScan>,
2072 ) -> Result<(Box<fixed::Recovery<E, u64>>, Range<u64>), Error> {
2073 let scanned: Vec<u64> = pending.keys().rev().copied().collect();
2076 let mut items_in_newest = 0;
2077 let mut newest_blob = None;
2078 for &blob in &scanned {
2079 let writer = pending.remove(&blob).expect("blob came from pending");
2080 let (mut writer, scan) = match scans.get(&blob) {
2081 Some(scan) => (writer, *scan),
2082 None => Self::scan_blob(writer, buffer, codec_config, compressed).await?,
2083 };
2084 if scan.items > items_per_blob {
2085 return Err(Error::Corruption(format!(
2086 "blob {blob} has too many items: expected at most {items_per_blob}, got {}",
2087 scan.items
2088 )));
2089 }
2090 if scan.torn {
2091 warn!(
2092 blob,
2093 new_size = scan.valid_size,
2094 "crash repair: truncating trailing bytes"
2095 );
2096 writer = writer.truncate(scan.valid_size).await?;
2097 }
2098 pending.insert(blob, writer);
2099 if scan.items > 0 {
2100 items_in_newest = scan.items;
2101 newest_blob = Some(blob);
2102 break;
2103 }
2104 }
2105
2106 let tail_blob = match newest_blob {
2110 Some(blob) if items_in_newest == items_per_blob => blob.saturating_add(1),
2111 Some(blob) => blob,
2112 None => pending.keys().next().copied().unwrap_or(0),
2113 };
2114 for &blob in &scanned {
2115 if blob <= tail_blob {
2116 break;
2117 }
2118 warn!(blob, "crash repair: removing empty trailing data blob");
2119 pending.remove(&blob);
2120 partition.remove(blob).await?;
2121 }
2122
2123 let Some(newest_blob) = newest_blob else {
2124 return Self::align_empty(partition, pending, offsets, items_per_blob).await;
2125 };
2126
2127 let oldest_blob = *pending.keys().next().expect("pending is non-empty");
2131 let data_oldest_pos = blob_first_position(oldest_blob, items_per_blob)?;
2132 {
2133 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
2134
2135 if offsets_bounds.end < data_oldest_pos {
2138 return Err(Error::Corruption(format!(
2139 "offsets journal size {} is behind data oldest position {data_oldest_pos}",
2140 offsets_bounds.end
2141 )));
2142 }
2143 let offsets_start_blob = position_to_blob(offsets_bounds.start, items_per_blob);
2144 match offsets_start_blob.cmp(&oldest_blob) {
2145 std::cmp::Ordering::Less => {
2146 warn!("crash repair: pruning offsets journal to {data_oldest_pos}");
2147 let (pruned, _) = offsets.prune(data_oldest_pos).await?;
2148 offsets = pruned;
2149 }
2150 std::cmp::Ordering::Equal => {}
2151 std::cmp::Ordering::Greater => {
2152 return Err(Error::Corruption(format!(
2155 "offsets start blob {offsets_start_blob} ahead of \
2156 oldest data blob {oldest_blob}"
2157 )));
2158 }
2159 }
2160 }
2161
2162 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
2164
2165 let data_size = blob_first_position(newest_blob, items_per_blob)?
2168 .max(offsets_bounds.start)
2169 .checked_add(items_in_newest)
2170 .ok_or(Error::OffsetOverflow)?;
2171 let data_sync_start = Self::recovery_anchor(&offsets, &offsets_bounds, data_size)?;
2172
2173 let pruning_boundary = {
2176 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
2177 if offsets_bounds.end != data_size {
2178 return Err(Error::Corruption(format!(
2179 "recovered offsets end {} does not match data size {data_size}",
2180 offsets_bounds.end
2181 )));
2182 }
2183
2184 if !offsets_bounds.is_empty()
2189 && position_to_blob(offsets_bounds.start, items_per_blob) != oldest_blob
2190 {
2191 return Err(Error::Corruption(format!(
2192 "recovered offsets and data start in different blobs: {} != {oldest_blob}",
2193 position_to_blob(offsets_bounds.start, items_per_blob)
2194 )));
2195 }
2196
2197 offsets_bounds.start
2199 };
2200
2201 Self::sync_data_range(pending, data_sync_start, data_size, items_per_blob).await?;
2203 let offsets = offsets.sync().await?;
2204 Ok((offsets, pruning_boundary..data_size))
2205 }
2206
2207 async fn align_empty(
2212 partition: &Partition<E>,
2213 pending: &mut BTreeMap<u64, PagedRecovery<E::Blob>>,
2214 mut offsets: Box<fixed::Recovery<E, u64>>,
2215 items_per_blob: u64,
2216 ) -> Result<(Box<fixed::Recovery<E, u64>>, Range<u64>), Error> {
2217 let offsets_bounds = offsets.pruning_boundary()..offsets.size();
2218
2219 let Some(&blob) = pending.keys().next() else {
2220 let size = offsets_bounds.end;
2224 if !offsets_bounds.is_empty() {
2225 warn!("crash repair: clearing offsets to {size} (prune-all crash)");
2226 offsets = offsets.clear_to_size(size).await?;
2227 }
2228 return Ok((offsets, size..size));
2229 };
2230
2231 let blob_start = blob_first_position(blob, items_per_blob)?;
2234 let target = blob_start.max(offsets_bounds.start);
2235
2236 let aligned = position_to_blob(target, items_per_blob) == blob;
2238 if aligned && offsets_bounds == (target..target) {
2239 return Ok((offsets, target..target));
2240 }
2241
2242 if !aligned {
2245 warn!(blob, "crash repair: removing empty data blob");
2246 pending.remove(&blob);
2247 partition.remove(blob).await?;
2248 }
2249 warn!("crash repair: clearing offsets to {target} (empty data)");
2250 let offsets = offsets.clear_to_size(target).await?;
2251 Ok((offsets, target..target))
2252 }
2253
2254 fn recovery_anchor(
2257 offsets: &fixed::Recovery<E, u64>,
2258 offsets_bounds: &Range<u64>,
2259 retained_data_end_bound: u64,
2260 ) -> Result<u64, Error> {
2261 let recovery_watermark = offsets.recovery_watermark();
2262 if recovery_watermark < offsets_bounds.start {
2263 warn!(
2264 recovery_watermark,
2265 start = offsets_bounds.start,
2266 end = offsets_bounds.end,
2267 retained_data_end_bound,
2268 "crash repair: offsets recovery watermark is unusable, rebuilding from offsets start"
2269 );
2270 return Ok(offsets_bounds.start);
2271 }
2272 if recovery_watermark > retained_data_end_bound {
2273 return Err(Error::Corruption(format!(
2274 "offsets recovery watermark {recovery_watermark} exceeds retained data end \
2275 {retained_data_end_bound} (offsets bounds {}..{})",
2276 offsets_bounds.start, offsets_bounds.end
2277 )));
2278 }
2279 Ok(recovery_watermark)
2280 }
2281
2282 async fn sync_data_range(
2284 pending: &mut BTreeMap<u64, PagedRecovery<E::Blob>>,
2285 start_position: u64,
2286 end_position: u64,
2287 items_per_blob: u64,
2288 ) -> Result<(), Error> {
2289 if start_position >= end_position {
2290 return Ok(());
2291 }
2292
2293 let start_blob = position_to_blob(start_position, items_per_blob);
2295 let end_blob = position_to_blob(end_position - 1, items_per_blob);
2296 let futures: Vec<_> = pending
2297 .extract_if(start_blob..=end_blob, |_, writer| writer.needs_sync())
2298 .map(|(blob, writer)| async move { writer.sync().await.map(|writer| (blob, writer)) })
2299 .collect();
2300 let synced = try_join_all(futures).await?;
2301 pending.extend(synced);
2302 Ok(())
2303 }
2304
2305 async fn remove_blobs_after(
2307 partition: &Partition<E>,
2308 pending: &mut BTreeMap<u64, PagedRecovery<E::Blob>>,
2309 blob: u64,
2310 ) -> Result<(), Error> {
2311 while let Some((&newest, _)) = pending.last_key_value() {
2312 if newest <= blob {
2313 break;
2314 }
2315 drop(pending.remove(&newest));
2316 partition.remove(newest).await?;
2317 }
2318 Ok(())
2319 }
2320}
2321
2322pub struct Journal<E: Context, V: Codec>(Box<Inner<E, V>>);
2372
2373impl<E: Context, V: CodecShared> std::fmt::Debug for Journal<E, V> {
2374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2375 f.debug_struct("Journal")
2376 .field("bounds", &super::Contiguous::bounds(self))
2377 .finish_non_exhaustive()
2378 }
2379}
2380
2381impl<E: Context, V: CodecShared> Journal<E, V> {
2382 pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
2389 Ok(Self(Box::new(Inner::init(context, cfg).await?)))
2390 }
2391
2392 pub async fn init_at_most(
2397 context: E,
2398 cfg: Config<V::Cfg>,
2399 max_size: u64,
2400 ) -> Result<Self, Error> {
2401 let recovery = Recovery::<E, V>::open(context, cfg, Some(max_size)).await?;
2402 Ok(Self(Box::new(recovery.publish(max_size).await?)))
2403 }
2404
2405 #[commonware_macros::stability(ALPHA)]
2418 pub async fn init_at_size(context: E, cfg: Config<V::Cfg>, size: u64) -> Result<Self, Error> {
2419 Ok(Self(Box::new(
2420 Inner::init_at_size(context, cfg, size).await?,
2421 )))
2422 }
2423
2424 #[commonware_macros::stability(ALPHA)]
2426 pub(crate) async fn clear_to_size(mut self, new_size: u64) -> Result<Self, Error> {
2427 self.0 = self.0.clear_to_size(new_size).await?;
2428 Ok(self)
2429 }
2430
2431 pub async fn append(mut self, item: &V) -> Result<(Self, u64), Error> {
2441 let (inner, position) = self.0.append(item).await?;
2442 self.0 = inner;
2443 Ok((self, position))
2444 }
2445
2446 pub async fn append_many(mut self, items: Many<'_, V>) -> Result<(Self, u64), Error> {
2450 let (inner, position) = self.0.append_many(items).await?;
2451 self.0 = inner;
2452 Ok((self, position))
2453 }
2454
2455 pub fn prepare_append(&self, items: Many<'_, V>) -> Result<PreparedAppend<V>, Error> {
2460 self.0.prepare_append::<true>(items)
2461 }
2462
2463 pub async fn append_prepared(
2470 mut self,
2471 prepared: PreparedAppend<V>,
2472 ) -> Result<(Self, u64), Error> {
2473 let (inner, position) = self.0.append_prepared(prepared).await?;
2474 self.0 = inner;
2475 Ok((self, position))
2476 }
2477
2478 pub async fn snapshot(mut self) -> Result<(Self, Reader<'static, E, V>), Error> {
2483 let (inner, reader) = self.0.snapshot().await?;
2484 self.0 = inner;
2485 Ok((self, reader))
2486 }
2487
2488 pub fn size(&self) -> u64 {
2491 self.0.size()
2492 }
2493
2494 pub async fn prune(mut self, min_position: u64) -> Result<(Self, bool), Error> {
2502 let (inner, pruned) = self.0.prune(min_position).await?;
2503 self.0 = inner;
2504 Ok((self, pruned))
2505 }
2506
2507 pub async fn commit(mut self) -> Result<Self, Error> {
2511 self.0 = self.0.commit().await?;
2512 Ok(self)
2513 }
2514
2515 pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
2530 let (inner, handle) = self.0.start_sync().await?;
2531 self.0 = inner;
2532 Ok((self, handle))
2533 }
2534
2535 pub async fn sync(mut self) -> Result<Self, Error> {
2537 self.0 = self.0.sync().await?;
2538 Ok(self)
2539 }
2540
2541 pub async fn destroy(self) -> Result<(), Error> {
2552 self.0.destroy().await
2553 }
2554}
2555
2556impl<E: Context, V: CodecShared> Contiguous for Journal<E, V> {
2557 type Item = V;
2558
2559 fn bounds(&self) -> Range<u64> {
2560 self.0.bounds.clone()
2561 }
2562
2563 async fn read(&self, position: u64) -> Result<V, Error> {
2564 self.0.reader().read(position).await
2565 }
2566
2567 async fn read_many(&self, positions: &[u64]) -> Result<Vec<V>, Error> {
2568 self.0.reader().read_many(positions).await
2569 }
2570
2571 fn try_read_sync(&self, position: u64) -> Option<V> {
2572 self.0.reader().try_read_sync(position)
2573 }
2574
2575 fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<V>> {
2576 self.0.reader().try_read_many_sync(positions)
2577 }
2578
2579 async fn replay(
2580 &self,
2581 range: Range<u64>,
2582 buffer: NonZeroUsize,
2583 read_options: ReadOptions,
2584 ) -> Result<impl Stream<Item = Result<Batch<V>, Error>> + Send, Error> {
2585 let reader = self.0.reader();
2586 let states = reader.replay_states(range, buffer, read_options).await?;
2587
2588 Ok(super::replay_stream_from_states(states))
2589 }
2590}
2591
2592impl<E: Context, V: CodecShared> Mutable for Journal<E, V> {
2593 async fn append(self, item: &Self::Item) -> Result<(Self, u64), Error> {
2594 Self::append(self, item).await
2595 }
2596
2597 async fn append_many(self, items: Many<'_, Self::Item>) -> Result<(Self, u64), Error> {
2598 Self::append_many(self, items).await
2599 }
2600
2601 async fn prune(self, min_position: u64) -> Result<(Self, bool), Error> {
2602 Self::prune(self, min_position).await
2603 }
2604
2605 async fn start_sync(self) -> Result<(Self, Handle<()>), Error> {
2606 Self::start_sync(self).await
2607 }
2608
2609 async fn commit(self) -> Result<Self, Error> {
2610 Self::commit(self).await
2611 }
2612
2613 async fn sync(self) -> Result<Self, Error> {
2614 Self::sync(self).await
2615 }
2616
2617 async fn destroy(self) -> Result<(), Error> {
2618 Self::destroy(self).await
2619 }
2620}
2621
2622#[commonware_macros::stability(ALPHA)]
2623impl<E: Context, V: CodecShared> authenticated::BackingRecovery for Recovery<E, V> {
2624 type Journal = Journal<E, V>;
2625
2626 fn bounds(&self) -> Range<u64> {
2627 self.bounds.clone()
2628 }
2629
2630 async fn read(&self, pos: u64) -> Result<V, Error> {
2632 if pos < self.bounds.start {
2633 return Err(Error::ItemPruned(pos));
2634 }
2635 if pos >= self.bounds.end {
2636 return Err(Error::ItemOutOfRange(pos));
2637 }
2638 let per_blob = self.cfg.items_per_section.get();
2639 let blob = position_to_blob(pos, per_blob);
2640 let writer = self
2641 .pending
2642 .get(&blob)
2643 .ok_or_else(|| Error::Corruption(format!("missing recovery data blob {blob}")))?;
2644 let offset = self.offsets.item(pos).await?;
2645 read_frame_at::<V>(
2646 writer,
2647 offset,
2648 &self.cfg.codec_config,
2649 self.cfg.compression.is_some(),
2650 )
2651 .await
2652 .map(|(_, _, item)| item)
2653 }
2654
2655 async fn reset(mut self, size: u64) -> Result<Self, Error> {
2656 self.pending.clear();
2657 let partition = self.cfg.data_partition();
2658 self.offsets = self
2659 .offsets
2660 .clear_to_size_cleared(size, || {
2661 Partition::<E>::remove_all(&self.context, &partition)
2662 })
2663 .await?;
2664 self.discarded.clear();
2665 self.recovered_scans.clear();
2666 self.bounds = size..size;
2667 Ok(self)
2668 }
2669
2670 async fn finish(self, size: u64) -> Result<Self::Journal, Error> {
2671 Ok(Journal(Box::new(Self::publish(self, size).await?)))
2672 }
2673}
2674
2675#[commonware_macros::stability(ALPHA)]
2676impl<E: Context, V: CodecShared> authenticated::Backing<E> for Journal<E, V> {
2677 type Recovery = Recovery<E, V>;
2678
2679 async fn recover(
2680 context: E,
2681 cfg: Self::Config,
2682 max_size: Option<u64>,
2683 ) -> Result<Self::Recovery, Error> {
2684 Recovery::open(context, cfg, max_size).await
2685 }
2686
2687 async fn span(context: E, cfg: &Self::Config) -> Result<Range<u64>, Error> {
2688 Recovery::<E, V>::span(context, cfg).await
2689 }
2690
2691 async fn clear(context: E, cfg: Self::Config, size: u64) -> Result<Self::Recovery, Error> {
2692 let offsets_partition = cfg.offsets_partition();
2693 let offsets_context = context.child("offsets");
2694 let checkpoint =
2695 Checkpoint::open(offsets_context.child("meta"), &offsets_partition).await?;
2696
2697 if checkpoint.clear_target().is_none() {
2700 Partition::select(&offsets_context, &offsets_partition).await?;
2701 }
2702
2703 let data_partition = cfg.data_partition();
2706 let offsets = fixed::Recovery::<E, u64>::open_cleared(
2707 offsets_context,
2708 cfg.offsets_config(),
2709 checkpoint,
2710 size,
2711 || Partition::<E>::remove_all(&context, &data_partition),
2712 )
2713 .await?;
2714 Ok(Recovery::cleared(context, cfg, offsets, size))
2715 }
2716
2717 type Config = Config<V::Cfg>;
2718}
2719
2720#[cfg(test)]
2721impl<E: Context, V: CodecShared> Journal<E, V> {
2722 pub(crate) async fn test_prune_data(mut self, min_blob: u64) -> Result<(Self, bool), Error> {
2724 let min_blob = min_blob.min(self.0.blobs.tail_blob_index());
2725 if min_blob <= self.0.blobs.oldest_blob_index() {
2726 return Ok((self, false));
2727 }
2728 self.0.blobs = self.0.blobs.prune(min_blob).await?;
2729 Ok((self, true))
2730 }
2731
2732 pub(crate) async fn test_prune_offsets(mut self, position: u64) -> Result<(Self, bool), Error> {
2734 let (offsets, pruned) = self.0.offsets.prune(position).await?;
2735 self.0.offsets = offsets;
2736 Ok((self, pruned))
2737 }
2738
2739 pub(crate) async fn test_set_offsets_recovery_watermark(
2741 mut self,
2742 watermark: u64,
2743 ) -> Result<Self, Error> {
2744 self.0.offsets = self
2745 .0
2746 .offsets
2747 .test_set_recovery_watermark(watermark)
2748 .await?;
2749 Ok(self)
2750 }
2751
2752 pub(crate) fn test_offsets_size(&self) -> u64 {
2754 self.0.offsets.size()
2755 }
2756
2757 pub(crate) async fn test_append_data(
2760 mut self,
2761 blob: u64,
2762 item: V,
2763 ) -> Result<(Self, u64, u32), Error> {
2764 let mut encoded = Vec::new();
2765 encode_frame_into(self.0.compression, &item, &mut encoded)?;
2766 let item_len = encoded.len() as u32;
2767
2768 let tail_blob = self.0.blobs.tail_blob_index();
2769 if blob == tail_blob {
2770 let (blobs, offset) = self.0.blobs.append_owned(encoded.into()).await?;
2771 self.0.blobs = blobs;
2772 return Ok((self, offset, item_len));
2773 }
2774 assert!(blob > tail_blob, "cannot append to a sealed blob");
2775 let writer = self.0.blobs.open_blob(blob).await?;
2776 let (writer, offset) = writer.append(&encoded).await?;
2777 writer.sync().await?;
2778 Ok((self, offset, item_len))
2779 }
2780
2781 pub(crate) async fn test_sync_data_blob(mut self, blob: u64) -> Result<Self, Error> {
2783 self.0.blobs = self.0.blobs.sync_blob(blob).await?;
2784 Ok(self)
2785 }
2786
2787 pub(crate) async fn test_truncate(mut self, cap: u64) -> Result<Self, Error> {
2789 self.0 = self.0.test_truncate(cap).await?;
2790 Ok(self)
2791 }
2792
2793 pub(crate) async fn test_truncate_offsets(mut self, position: u64) -> Result<Self, Error> {
2795 self.0.offsets = self.0.offsets.test_truncate(position).await?;
2796 Ok(self)
2797 }
2798
2799 async fn test_truncate_data(mut self, position: u64) -> Result<Partition<E>, Error> {
2801 let offset = self.0.offsets.reader().read(position).await?;
2802 let blob = position_to_blob(position, self.0.items_per_blob.get());
2803 let (context, name, cache, buffer) = self.0.blobs.test_configuration();
2804 let (blobs, sync) = self.0.blobs.start_sync().await?;
2805 self.0.blobs = blobs;
2806 sync.await?;
2807 drop(self);
2808 let partition = Partition::new(context, name, cache, buffer);
2809 let mut pending = partition.open_all().await?;
2810 Inner::<E, V>::remove_blobs_after(&partition, &mut pending, blob).await?;
2811 pending
2812 .remove(&blob)
2813 .expect("retained data blob")
2814 .truncate(offset)
2815 .await?;
2816 Ok(partition)
2817 }
2818}
2819
2820#[cfg(test)]
2821impl<E: crate::Context, V: CodecShared> Inner<E, V> {
2822 async fn test_truncate(self: Box<Self>, cap: u64) -> Result<Box<Self>, Error> {
2824 let (context, name, page_cache, write_buffer) = self.blobs.test_configuration();
2825 let cfg = Config {
2826 partition: name.strip_suffix(DATA_SUFFIX).unwrap().to_owned(),
2827 items_per_section: self.items_per_blob,
2828 page_cache,
2829 write_buffer,
2830 replay_buffer: write_buffer,
2831 compression: self.compression,
2832 codec_config: self.codec_config.clone(),
2833 };
2834 _ = self.sync().await?;
2835 Ok(Box::new(
2836 Recovery::<E, V>::open(context, cfg, Some(cap))
2837 .await?
2838 .publish(cap)
2839 .await?,
2840 ))
2841 }
2842}
2843
2844#[cfg(test)]
2845mod tests {
2846 use super::*;
2847 use crate::{
2848 journal::{
2849 authenticated::{self, BackingRecovery as _},
2850 contiguous::{
2851 checkpoint::Checkpoint,
2852 tests::{replay_items, run_contiguous_tests, tear_page_rewrite},
2853 },
2854 },
2855 utils::codec::View,
2856 };
2857 use commonware_macros::test_traced;
2858 use commonware_runtime::{
2859 Metrics as _, ReadOptions, Runner, Spawner as _, Storage, Supervisor as _, WriteOptions,
2860 buffer::paged::{CacheRef, Writer, corrupt_page},
2861 deterministic,
2862 mocks::{
2863 DelayedSyncContext, PendingSyncs, RecordingContext, drive_pending_syncs,
2864 fail_pending_syncs, next_pending_sync, release_pending_syncs,
2865 },
2866 };
2867 use commonware_utils::{NZU16, NZU64, NZUsize, probability, sequence::FixedBytes};
2868 use futures::StreamExt as _;
2869 use rand::Rng as _;
2870 use std::num::NonZeroU16;
2871
2872 const PAGE_SIZE: NonZeroU16 = NZU16!(101);
2874 const PAGE_CACHE_SIZE: usize = 2;
2875 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(1024);
2877 const SMALL_PAGE_SIZE: NonZeroU16 = NZU16!(512);
2878
2879 fn initialization_cfg(partition: &str, items_per_section: u64) -> Config<()> {
2880 Config {
2881 partition: partition.into(),
2882 items_per_section: NonZeroU64::new(items_per_section).unwrap(),
2883 compression: None,
2884 codec_config: (),
2885 page_cache: CacheRef::new(NZU16!(16), NZUsize!(4)),
2886 write_buffer: NZUsize!(1),
2887 replay_buffer: NZUsize!(256),
2888 }
2889 }
2890
2891 #[test]
2892 fn test_bounded_inspection_preserves_acknowledged_offsets_until_publication() {
2893 deterministic::Runner::default().start(|context| async move {
2894 let cfg = initialization_cfg("inspection-preserves-offsets", 5);
2896 let offsets_partition = cfg.offsets_partition();
2897 let mut journal = Journal::<_, u64>::init(context.child("seed"), cfg.clone())
2898 .await
2899 .unwrap();
2900 for item in 0..20u64 {
2901 (journal, _) = journal.append(&item).await.unwrap();
2902 }
2903 drop(journal.sync().await.unwrap());
2904 assert_eq!(
2905 fixed::Journal::<_, u64>::persisted_watermark(
2906 context.child("synced"),
2907 &offsets_partition
2908 )
2909 .await
2910 .unwrap(),
2911 Some(20)
2912 );
2913
2914 let offsets_blobs = format!("{offsets_partition}-blobs");
2919 let offsets = context.scan(&offsets_blobs).await.unwrap();
2920 let pending = Recovery::<_, u64>::open(context.child("bounded"), cfg.clone(), Some(10))
2921 .await
2922 .unwrap();
2923 assert_eq!(pending.bounds, 0..10);
2924 drop(pending);
2925 assert_eq!(context.scan(&offsets_blobs).await.unwrap(), offsets);
2926 assert_eq!(
2927 fixed::Journal::<_, u64>::persisted_watermark(
2928 context.child("inspected"),
2929 &offsets_partition
2930 )
2931 .await
2932 .unwrap(),
2933 Some(20)
2934 );
2935
2936 let journal = Journal::<_, u64>::init(context.child("retry"), cfg)
2939 .await
2940 .unwrap();
2941 assert_eq!(journal.bounds(), 0..20);
2942 for item in 0..20 {
2943 assert_eq!(journal.read(item).await.unwrap(), item);
2944 }
2945 drop(journal);
2946
2947 assert_eq!(
2949 fixed::Journal::<_, u64>::persisted_watermark(
2950 context.child("reopened"),
2951 &offsets_partition
2952 )
2953 .await
2954 .unwrap(),
2955 Some(20)
2956 );
2957 });
2958 }
2959
2960 #[test]
2961 fn test_bounded_inspection_preserves_data_above_watermark_until_publication() {
2962 deterministic::Runner::default().start(|context| async move {
2963 let cfg = initialization_cfg("inspection-preserves-data", 5);
2965 let offsets_partition = cfg.offsets_partition();
2966 let data_partition = cfg.data_partition();
2967 let mut journal = Journal::<_, u64>::init(context.child("seed"), cfg.clone())
2968 .await
2969 .unwrap();
2970 for item in 0..7u64 {
2971 (journal, _) = journal.append(&item).await.unwrap();
2972 }
2973 journal = journal.sync().await.unwrap();
2974
2975 for item in 7..20u64 {
2979 (journal, _) = journal.append(&item).await.unwrap();
2980 }
2981 let (journal, handle) = journal.start_sync().await.unwrap();
2982 handle.await.unwrap();
2983 drop(journal);
2984 assert_eq!(
2985 fixed::Journal::<_, u64>::persisted_watermark(
2986 context.child("lagged"),
2987 &offsets_partition
2988 )
2989 .await
2990 .unwrap(),
2991 Some(7)
2992 );
2993
2994 let mut data = Vec::new();
2998 for name in context.scan(&data_partition).await.unwrap() {
2999 let (_, size) = context.open(&data_partition, &name).await.unwrap();
3000 data.push((name, size));
3001 }
3002 data.sort();
3003 assert_eq!(data.len(), 5);
3004
3005 let pending = Recovery::<_, u64>::open(context.child("bounded"), cfg.clone(), Some(12))
3010 .await
3011 .unwrap();
3012 assert_eq!(pending.bounds, 0..12);
3013 assert_eq!(pending.discarded, vec![3, 4]);
3014 drop(pending);
3015 let mut inspected = Vec::new();
3016 for name in context.scan(&data_partition).await.unwrap() {
3017 let (_, size) = context.open(&data_partition, &name).await.unwrap();
3018 inspected.push((name, size));
3019 }
3020 inspected.sort();
3021 assert_eq!(inspected, data);
3022 assert_eq!(
3023 fixed::Journal::<_, u64>::persisted_watermark(
3024 context.child("inspected"),
3025 &offsets_partition
3026 )
3027 .await
3028 .unwrap(),
3029 Some(7)
3030 );
3031
3032 let journal = Journal::<_, u64>::init(context.child("retry"), cfg)
3035 .await
3036 .unwrap();
3037 assert_eq!(journal.bounds(), 0..20);
3038 for item in 0..20 {
3039 assert_eq!(journal.read(item).await.unwrap(), item);
3040 }
3041 drop(journal);
3042
3043 assert_eq!(
3045 fixed::Journal::<_, u64>::persisted_watermark(
3046 context.child("reopened"),
3047 &offsets_partition
3048 )
3049 .await
3050 .unwrap(),
3051 Some(20)
3052 );
3053 });
3054 }
3055
3056 #[test]
3057 fn test_recovery_failure_must_not_clear_durable_data() {
3058 deterministic::Runner::default().start(|context| async move {
3059 let config = initialization_cfg("initialization-recovery-clear", 20);
3060 let mut journal =
3061 Journal::<_, u64>::init_at_size(context.child("seed"), config.clone(), 20)
3062 .await
3063 .unwrap();
3064 for value in 0..8 {
3065 (journal, _) = journal.append(&value).await.unwrap();
3066 }
3067 let (journal, handle) = journal.start_sync().await.unwrap();
3068 handle.await.unwrap();
3069 drop(journal);
3070
3071 let (blob, _) = context
3073 .open(&config.data_partition(), &2u64.to_be_bytes())
3074 .await
3075 .unwrap();
3076 drop(blob);
3077 *context.storage_fault_config().write() = deterministic::FaultConfig {
3078 remove_rate: Some(probability!(1.0)),
3079 ..Default::default()
3080 };
3081
3082 let result =
3083 Journal::<_, u64>::init(context.child("interrupted"), config.clone()).await;
3084 assert!(matches!(result, Err(Error::Runtime(_))), "{result:?}");
3085 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
3086 let journal = Journal::<_, u64>::init(context.child("retry"), config)
3087 .await
3088 .unwrap();
3089 assert_eq!(
3090 journal.bounds(),
3091 20..28,
3092 "ordinary recovery must retain committed data after retry"
3093 );
3094 for pos in 20..28 {
3095 assert_eq!(journal.read(pos).await.unwrap(), pos - 20);
3096 }
3097 });
3098 }
3099
3100 #[test]
3101 fn test_sync_rejects_missing_acknowledged_data() {
3102 deterministic::Runner::default().start(|context| async move {
3103 let config = initialization_cfg("initialization-missing-anchor", 5);
3105 let mut journal = Journal::<_, u64>::init(context.child("seed"), config.clone())
3106 .await
3107 .unwrap();
3108 for value in 0..20 {
3109 (journal, _) = journal.append(&value).await.unwrap();
3110 }
3111 _ = journal.sync().await.unwrap();
3112
3113 for section in 1u64..=4 {
3115 context
3116 .remove(
3117 "initialization-missing-anchor_data",
3118 Some(§ion.to_be_bytes()),
3119 )
3120 .await
3121 .unwrap();
3122 }
3123
3124 let result = <Journal<_, u64> as authenticated::Backing<_>>::recover(
3126 context.child("sync"),
3127 config,
3128 Some(40),
3129 )
3130 .await;
3131 let error = result.err().expect("missing acknowledged data must fail");
3132 assert!(
3133 matches!(error, Error::Corruption(_)),
3134 "must reject missing data instead of authorizing a reset: {error}"
3135 );
3136 });
3137 }
3138
3139 #[test]
3140 fn test_init_sync_rejects_acknowledged_offsets_without_data() {
3141 deterministic::Runner::default().start(|context| async move {
3142 let config = initialization_cfg("initialization-offsets-without-data", 5);
3144 let mut journal = Journal::<_, u64>::init(context.child("seed"), config.clone())
3145 .await
3146 .unwrap();
3147 for value in 0..20u64 {
3148 (journal, _) = journal.append(&value).await.unwrap();
3149 }
3150 drop(journal.sync().await.unwrap());
3151
3152 let offsets_blobs = format!("{}-blobs", config.offsets_partition());
3154 let offsets = context.scan(&offsets_blobs).await.unwrap();
3155
3156 context
3159 .remove(&config.data_partition(), None)
3160 .await
3161 .unwrap();
3162
3163 let result = authenticated::init_sync::<_, Journal<_, u64>>(
3167 context.child("sync"),
3168 config.clone(),
3169 7..27,
3170 )
3171 .await;
3172 assert!(
3173 matches!(
3174 &result,
3175 Err(Error::Corruption(message))
3176 if message == "retained offsets have no data blobs"
3177 ),
3178 "{result:?}"
3179 );
3180
3181 assert_eq!(context.scan(&offsets_blobs).await.unwrap(), offsets);
3183 assert_eq!(
3184 fixed::Journal::<_, u64>::persisted_watermark(
3185 context.child("checkpoint"),
3186 &config.offsets_partition()
3187 )
3188 .await
3189 .unwrap(),
3190 Some(20)
3191 );
3192 });
3193 }
3194
3195 struct Counted(u64);
3196
3197 impl commonware_codec::Write for Counted {
3198 fn write(&self, buf: &mut impl bytes::BufMut) {
3199 commonware_codec::Write::write(&self.0, buf);
3200 }
3201 }
3202
3203 impl commonware_codec::FixedSize for Counted {
3204 const SIZE: usize = 8;
3205 }
3206
3207 impl commonware_codec::Read for Counted {
3208 type Cfg = Arc<std::sync::atomic::AtomicUsize>;
3209
3210 fn read_cfg(
3211 buf: &mut impl commonware_codec::Buf,
3212 count: &Self::Cfg,
3213 ) -> Result<Self, commonware_codec::Error> {
3214 count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3215 Ok(Self(u64::read_cfg(buf, &())?))
3216 }
3217 }
3218
3219 #[test]
3220 fn test_bounded_empty_prefix_at_maximum_position() {
3221 deterministic::Runner::default().start(|context| async move {
3222 let cfg = Config {
3223 partition: "maximum-empty-prefix".into(),
3224 items_per_section: NZU64!(1),
3225 compression: None,
3226 codec_config: (),
3227 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(8)),
3228 write_buffer: NZUsize!(1024),
3229 replay_buffer: NZUsize!(1024),
3230 };
3231 let mut journal =
3232 Journal::<_, u64>::init_at_size(context.child("seed"), cfg.clone(), u64::MAX - 1)
3233 .await
3234 .unwrap();
3235 (journal, _) = journal.append(&99).await.unwrap();
3236 (journal, _) = journal.prune(u64::MAX).await.unwrap();
3237 _ = journal.sync().await.unwrap();
3238 let journal =
3239 Journal::<_, u64>::init_at_most(context.child("capped"), cfg.clone(), u64::MAX)
3240 .await
3241 .unwrap();
3242 assert_eq!(journal.bounds(), u64::MAX..u64::MAX);
3243 drop(journal);
3244 let journal = Journal::<_, u64>::init(context.child("ordinary"), cfg)
3245 .await
3246 .unwrap();
3247 assert_eq!(journal.bounds(), u64::MAX..u64::MAX);
3248 });
3249 }
3250
3251 fn check_bounded_empty_publish_retry(cap: Option<u64>) {
3252 fn config() -> Config<()> {
3253 Config {
3254 partition: "bounded-empty-publish-floor".into(),
3255 items_per_section: NZU64!(10),
3256 compression: None,
3257 codec_config: (),
3258 page_cache: CacheRef::new(NZU16!(16), NZUsize!(16)),
3259 write_buffer: NZUsize!(32),
3260 replay_buffer: NZUsize!(128),
3261 }
3262 }
3263
3264 let ((), checkpoint) =
3267 deterministic::Runner::default().start_and_recover(|context| async move {
3268 let cfg = config();
3269 let mut journal = Journal::<_, u64>::init(context.child("seed"), cfg.clone())
3270 .await
3271 .unwrap();
3272 for value in 0..15u64 {
3273 (journal, _) = journal.append(&value).await.unwrap();
3274 }
3275 journal = journal.sync().await.unwrap();
3276 journal.0.halt_before_offsets_prune = true;
3277 {
3278 let prune = journal.prune(10);
3279 futures::pin_mut!(prune);
3280 assert!(futures::poll!(prune.as_mut()).is_pending());
3281 }
3282 assert_eq!(
3283 context.scan(&cfg.data_partition()).await.unwrap(),
3284 vec![1u64.to_be_bytes().to_vec()]
3285 );
3286 });
3287
3288 let ((), checkpoint) =
3291 deterministic::Runner::from(checkpoint).start_and_recover(|context| async move {
3292 let cfg = config();
3293 let mut recovery =
3294 Recovery::<_, u64>::open(context.child("recover"), cfg.clone(), Some(10))
3295 .await
3296 .unwrap();
3297 assert_eq!(recovery.bounds, 10..10);
3298 assert_eq!(recovery.offsets.bounds(), 0..10);
3299 assert_eq!(recovery.discarded, vec![1]);
3300 assert!(recovery.pending.is_empty());
3301 recovery.offsets = recovery.offsets.truncate(10).await.unwrap();
3302 recovery.halt_after_data_removal = true;
3303 {
3304 let publish = recovery.publish(10);
3305 futures::pin_mut!(publish);
3306 assert!(futures::poll!(publish.as_mut()).is_pending());
3307 }
3308 assert!(
3309 context
3310 .scan(&cfg.data_partition())
3311 .await
3312 .unwrap()
3313 .is_empty()
3314 );
3315 });
3316
3317 deterministic::Runner::from(checkpoint).start(move |context| async move {
3318 let cfg = config();
3319 let metadata = Checkpoint::open(context.child("checkpoint"), &cfg.offsets_partition())
3320 .await
3321 .unwrap();
3322 assert_eq!(metadata.watermark(), Some(10));
3323 assert_eq!(metadata.clear_target(), None);
3324 drop(metadata);
3325 let result = match cap {
3326 Some(cap) => {
3327 Journal::<_, u64>::init_at_most(context.child("retry"), cfg, cap).await
3328 }
3329 None => Journal::<_, u64>::init(context.child("retry"), cfg).await,
3330 };
3331 if cap == Some(9) {
3332 assert!(matches!(result, Err(Error::ItemPruned(9))));
3333 } else {
3334 let journal = result.expect("interrupted publication must remain recoverable");
3335 assert_eq!(journal.bounds(), 10..10);
3336 let (journal, pos) = journal.append(&99).await.unwrap();
3337 assert_eq!(pos, 10);
3338 assert_eq!(journal.read(10).await.unwrap(), 99);
3339 }
3340 });
3341 }
3342
3343 #[test]
3344 fn test_bounded_empty_publish_same_cap() {
3345 check_bounded_empty_publish_retry(Some(10));
3346 }
3347
3348 #[test]
3349 fn test_bounded_empty_publish_larger_cap() {
3350 check_bounded_empty_publish_retry(Some(11));
3351 }
3352
3353 #[test]
3354 fn test_bounded_empty_publish_lower_cap() {
3355 check_bounded_empty_publish_retry(Some(9));
3356 }
3357
3358 #[test]
3359 fn test_bounded_empty_publish_ordinary() {
3360 check_bounded_empty_publish_retry(None);
3361 }
3362
3363 #[test]
3364 fn test_clean_initialization_cap_adds_no_syncs() {
3365 deterministic::Runner::default().start(|context| async move {
3366 let cfg = Config {
3367 partition: "clean-cap-syncs".into(),
3368 items_per_section: NZU64!(5),
3369 compression: None,
3370 codec_config: (),
3371 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(8)),
3372 write_buffer: NZUsize!(1024),
3373 replay_buffer: NZUsize!(1024),
3374 };
3375 let mut journal = Journal::init(context.child("seed"), cfg.clone())
3376 .await
3377 .unwrap();
3378 for value in 0..13u64 {
3379 (journal, _) = journal.append(&value).await.unwrap();
3380 }
3381 _ = journal.sync().await.unwrap();
3382 let mut baseline = None;
3383 for cap in [None, Some(13), Some(u64::MAX)] {
3384 let syncs = PendingSyncs::default();
3385 syncs.unblock();
3386 let recovery_context = DelayedSyncContext {
3387 inner: context.child("recover"),
3388 pending: syncs.clone(),
3389 };
3390 let journal = match cap {
3391 Some(cap) => {
3392 Journal::<_, u64>::init_at_most(recovery_context, cfg.clone(), cap).await
3393 }
3394 None => Journal::<_, u64>::init(recovery_context, cfg.clone()).await,
3395 }
3396 .unwrap();
3397 assert_eq!(journal.bounds(), 0..13);
3398 assert_eq!(syncs.starts(), *baseline.get_or_insert(syncs.starts()));
3399 drop(journal);
3400 }
3401 });
3402 }
3403
3404 #[test]
3405 fn test_selected_prefix_reuses_known_offsets() {
3406 for start in [0, 7] {
3407 for watermark in [start, 10, 13] {
3408 deterministic::Runner::default().start(|context| async move {
3409 let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3410 let cfg = Config {
3411 partition: "selected-offset".into(),
3412 items_per_section: NZU64!(5),
3413 compression: None,
3414 codec_config: count.clone(),
3415 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(8)),
3416 write_buffer: NZUsize!(1024),
3417 replay_buffer: NZUsize!(1024),
3418 };
3419 let mut journal =
3420 Journal::init_at_size(context.child("seed"), cfg.clone(), start)
3421 .await
3422 .unwrap();
3423 for value in start..13 {
3424 (journal, _) = journal.append(&Counted(value)).await.unwrap();
3425 }
3426 let journal = journal.sync().await.unwrap();
3427 drop(
3428 journal
3429 .test_set_offsets_recovery_watermark(watermark)
3430 .await
3431 .unwrap(),
3432 );
3433 let pending =
3434 Recovery::<_, Counted>::open(context.child("recover"), cfg, Some(12))
3435 .await
3436 .unwrap();
3437 count.store(0, std::sync::atomic::Ordering::Relaxed);
3438 for size in start..=12 {
3441 let first = (size / 5 * 5).max(start);
3442 assert_eq!(
3443 pending.terminal_offset(size).await.unwrap(),
3444 (size - first) * 9
3445 );
3446 }
3447 assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 0);
3448 for value in start..12 {
3449 assert_eq!(pending.read(value).await.unwrap().0, value);
3450 }
3451 assert_eq!(
3452 count.load(std::sync::atomic::Ordering::Relaxed),
3453 (12 - start) as usize
3454 );
3455 let journal = Journal(Box::new(pending.publish(7).await.unwrap()));
3456 assert_eq!(journal.bounds(), start..7);
3457 if start < 7 {
3458 assert_eq!(journal.read(6).await.unwrap().0, 6);
3459 }
3460 });
3461 }
3462 }
3463 }
3464
3465 #[test]
3466 fn test_initialization_decodes_each_recovery_frame_once() {
3467 for (cap, watermark, expected) in [
3468 (None, 13, 3),
3469 (Some(13), 13, 3),
3470 (Some(u64::MAX), 13, 3),
3471 (Some(12), 13, 2),
3472 (None, 10, 3),
3473 ] {
3474 deterministic::Runner::default().start(|context| async move {
3475 let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3476 let cfg = Config {
3477 partition: "decode-once".into(),
3478 items_per_section: NZU64!(5),
3479 compression: None,
3480 codec_config: count.clone(),
3481 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(8)),
3482 write_buffer: NZUsize!(1024),
3483 replay_buffer: NZUsize!(1024),
3484 };
3485 let mut journal = Journal::init(context.child("seed"), cfg.clone())
3486 .await
3487 .unwrap();
3488 for value in 0..13 {
3489 (journal, _) = journal.append(&Counted(value)).await.unwrap();
3490 }
3491 let journal = journal.sync().await.unwrap();
3492 drop(
3493 journal
3494 .test_set_offsets_recovery_watermark(watermark)
3495 .await
3496 .unwrap(),
3497 );
3498 count.store(0, std::sync::atomic::Ordering::Relaxed);
3499 let mut journal = match cap {
3500 Some(cap) => {
3501 Journal::<_, Counted>::init_at_most(context.child("recover"), cfg, cap)
3502 .await
3503 }
3504 None => Journal::<_, Counted>::init(context.child("recover"), cfg).await,
3505 }
3506 .unwrap();
3507 assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), expected);
3508 let size = cap.unwrap_or(u64::MAX).min(13);
3509 assert_eq!(journal.bounds(), 0..size);
3510 for value in 0..size {
3511 assert_eq!(journal.read(value).await.unwrap().0, value);
3512 }
3513 (journal, _) = journal.append(&Counted(99)).await.unwrap();
3514 assert_eq!(journal.read(size).await.unwrap().0, 99);
3515 });
3516 }
3517 }
3518
3519 #[test]
3520 fn test_offset_rebuild_with_partial_final_page() {
3521 const PAGE_SIZE: usize = 4096;
3522 const WRITE_BUFFER: usize = 131072;
3523 const SECTIONS: u64 = 16;
3524 const CACHE_PAGES: usize = 4;
3525
3526 fn config(items_per_section: u64) -> Config<()> {
3527 Config {
3528 partition: "offset-rebuild-buffers".into(),
3529 items_per_section: NonZeroU64::new(items_per_section).unwrap(),
3530 compression: None,
3531 codec_config: (),
3532 page_cache: CacheRef::new(NZU16!(PAGE_SIZE as u16), NZUsize!(CACHE_PAGES)),
3533 write_buffer: NZUsize!(WRITE_BUFFER),
3534 replay_buffer: NZUsize!(PAGE_SIZE),
3535 }
3536 }
3537
3538 for items_per_section in [8192, 8193] {
3540 let size = SECTIONS * items_per_section;
3541
3542 let ((), checkpoint) =
3543 deterministic::Runner::default().start_and_recover(move |context| async move {
3544 let mut journal =
3545 Journal::<_, u64>::init(context.child("seed"), config(items_per_section))
3546 .await
3547 .unwrap();
3548 for value in 0..size {
3549 (journal, _) = journal.append(&value).await.unwrap();
3550 }
3551 let journal = journal.commit().await.unwrap();
3552 drop(journal);
3553 });
3554
3555 deterministic::Runner::from(checkpoint).start(move |context| async move {
3556 let pending = Recovery::<_, u64>::open(
3557 context.child("recover"),
3558 config(items_per_section),
3559 None,
3560 )
3561 .await
3562 .unwrap();
3563 assert_eq!(pending.bounds, 0..size);
3564 assert_eq!(pending.offsets.recovery_watermark(), 0);
3565
3566 let journal = Journal(Box::new(pending.publish(size).await.unwrap()));
3567 for section in 0..SECTIONS {
3568 for pos in [
3569 section * items_per_section,
3570 (section + 1) * items_per_section - 1,
3571 ] {
3572 assert_eq!(journal.read(pos).await.unwrap(), pos);
3573 }
3574 }
3575 });
3576 }
3577 }
3578
3579 #[test]
3580 fn test_interrupted_offset_rebuild_preserves_committed_data() {
3581 fn config() -> Config<<Counted as commonware_codec::Read>::Cfg> {
3582 Config {
3583 partition: "interrupted-offset-rebuild".into(),
3584 items_per_section: NZU64!(256),
3585 compression: None,
3586 codec_config: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
3587 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
3588 write_buffer: NZUsize!(128),
3589 replay_buffer: NZUsize!(128),
3590 }
3591 }
3592
3593 for start in [0, 7] {
3594 for retention_rate in [probability!(0.0), probability!(1.0)] {
3595 let ((), checkpoint) =
3596 deterministic::Runner::default().start_and_recover(|context| async move {
3597 let cfg = config();
3598 let mut journal =
3599 Journal::init_at_size(context.child("seed"), cfg.clone(), start)
3600 .await
3601 .unwrap();
3602 for value in start..128 {
3603 (journal, _) = journal.append(&Counted(value)).await.unwrap();
3604 }
3605 drop(journal.commit().await.unwrap());
3606
3607 let count = cfg.codec_config.clone();
3608 count.store(0, std::sync::atomic::Ordering::Relaxed);
3609 *context.storage_fault_config().write() = deterministic::FaultConfig {
3610 write_rate: Some(deterministic::WriteConfig {
3611 failure_rate: probability!(1.0),
3612 retention_rate,
3613 mode: deterministic::PartialWriteMode::Prefix,
3614 }),
3615 ..Default::default()
3616 };
3617 assert!(
3618 Recovery::<_, Counted>::open(context.child("faulted"), cfg, None)
3619 .await
3620 .is_err()
3621 );
3622
3623 let decoded = count.load(std::sync::atomic::Ordering::Relaxed);
3626 assert!(decoded > 0 && decoded < (128 - start) as usize);
3627 });
3628
3629 deterministic::Runner::from(checkpoint).start(|context| async move {
3630 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
3631 let cfg = config();
3632 let checkpoint =
3633 Checkpoint::open(context.child("checkpoint"), &cfg.offsets_partition())
3634 .await
3635 .unwrap();
3636 assert_eq!(checkpoint.watermark(), Some(start));
3637 assert_eq!(checkpoint.clear_target(), None);
3638 drop(checkpoint);
3639
3640 let journal = Journal::<_, Counted>::init(context.child("retry"), cfg)
3641 .await
3642 .unwrap();
3643 assert_eq!(journal.bounds(), start..128);
3644 for value in start..128 {
3645 assert_eq!(journal.read(value).await.unwrap().0, value);
3646 }
3647 });
3648 }
3649 }
3650 }
3651
3652 #[test]
3653 fn test_bounded_initialization_retries_after_storage_faults() {
3654 for start in [0, 7] {
3655 for kind in 0..3 {
3656 for numerator in [1, 3, 7, 10] {
3657 let (succeeded, checkpoint) = deterministic::Runner::default()
3658 .start_and_recover(|context| async move {
3659 let cfg = Config {
3660 partition: "capped-faults".into(),
3661 items_per_section: NZU64!(5),
3662 compression: None,
3663 codec_config: (),
3664 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(4)),
3665 write_buffer: NZUsize!(128),
3666 replay_buffer: NZUsize!(128),
3667 };
3668 let mut journal = Journal::<_, u64>::init_at_size(
3669 context.child("seed"),
3670 cfg.clone(),
3671 start,
3672 )
3673 .await
3674 .unwrap();
3675 for value in start..13 {
3676 (journal, _) = journal.append(&value).await.unwrap();
3677 }
3678 _ = journal.sync().await.unwrap();
3679 let rate = commonware_utils::Probability::new(numerator, 10).unwrap();
3680 *context.storage_fault_config().write() = deterministic::FaultConfig {
3681 sync_rate: (kind == 0).then_some(rate),
3682 remove_rate: (kind == 1).then_some(rate),
3683 resize_rate: (kind == 2).then_some(deterministic::ResizeConfig {
3684 failure_rate: rate,
3685 partial_rate: rate,
3686 }),
3687 ..Default::default()
3688 };
3689
3690 Journal::<_, u64>::init_at_most(context.child("faulted"), cfg, 7)
3693 .await
3694 .is_ok()
3695 });
3696 deterministic::Runner::from(checkpoint).start(|context| async move {
3697 *context.storage_fault_config().write() =
3698 deterministic::FaultConfig::default();
3699 let cfg = Config {
3700 partition: "capped-faults".into(),
3701 items_per_section: NZU64!(5),
3702 compression: None,
3703 codec_config: (),
3704 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(4)),
3705 write_buffer: NZUsize!(128),
3706 replay_buffer: NZUsize!(128),
3707 };
3708 if succeeded {
3709 let journal =
3710 Journal::<_, u64>::init(context.child("ordinary"), cfg.clone())
3711 .await
3712 .unwrap();
3713 assert_eq!(journal.bounds(), start..7);
3714 drop(journal);
3715 }
3716 let journal =
3717 Journal::<_, u64>::init_at_most(context.child("retry"), cfg.clone(), 7)
3718 .await
3719 .unwrap();
3720 assert_eq!(journal.bounds(), start..7);
3721 for value in start..7 {
3722 assert_eq!(journal.read(value).await.unwrap(), value);
3723 }
3724 drop(journal);
3725 let mut journal =
3726 Journal::<_, u64>::init(context.child("restart"), cfg.clone())
3727 .await
3728 .unwrap();
3729 assert_eq!(journal.bounds(), start..7);
3730 (journal, _) = journal.append(&99).await.unwrap();
3731 _ = journal.sync().await.unwrap();
3732 let journal = Journal::<_, u64>::init(context.child("verify"), cfg)
3733 .await
3734 .unwrap();
3735 assert_eq!(journal.bounds(), start..8);
3736 assert_eq!(journal.read(7).await.unwrap(), 99);
3737 });
3738 }
3739 }
3740 }
3741 }
3742
3743 #[test]
3744 fn test_init_at_most_restart_and_append() {
3745 for compression in [None, Some(3)] {
3746 for start in [0, 7] {
3747 for cap in [0, 1, 5, 7, 8, 10, 13, 20, u64::MAX] {
3748 deterministic::Runner::default().start(|context| async move {
3749 let cfg = Config {
3750 partition: "variable-cap".into(),
3751 items_per_section: NZU64!(5),
3752 compression,
3753 codec_config: (),
3754 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(3)),
3755 write_buffer: NZUsize!(2048),
3756 replay_buffer: NZUsize!(2048),
3757 };
3758 let mut journal = Journal::<_, u64>::init_at_size(
3759 context.child("storage"),
3760 cfg.clone(),
3761 start,
3762 )
3763 .await
3764 .unwrap();
3765 for value in start..13 {
3766 (journal, _) = journal.append(&value).await.unwrap();
3767 }
3768 journal = journal.sync().await.unwrap();
3769 drop(journal);
3770
3771 let opened = Journal::<_, u64>::init_at_most(
3772 context.child("storage"),
3773 cfg.clone(),
3774 cap,
3775 )
3776 .await;
3777 if cap < start {
3778 assert!(
3779 matches!(opened, Err(Error::ItemPruned(value)) if value == cap)
3780 );
3781 let journal = Journal::<_, u64>::init(context.child("storage"), cfg)
3782 .await
3783 .unwrap();
3784 assert_eq!(journal.bounds(), start..13);
3785 return;
3786 }
3787 let end = cap.min(13);
3788 let journal = opened.unwrap();
3789 assert_eq!(journal.bounds(), start..end);
3790 for value in start..end {
3791 assert_eq!(journal.read(value).await.unwrap(), value);
3792 }
3793 drop(journal);
3794
3795 let mut journal =
3797 Journal::<_, u64>::init(context.child("storage"), cfg.clone())
3798 .await
3799 .unwrap();
3800 assert_eq!(journal.bounds(), start..end);
3801 for value in end..end + 8 {
3802 let pos;
3803 (journal, pos) = journal.append(&(value + 100)).await.unwrap();
3804 assert_eq!(pos, value);
3805 }
3806 journal = journal.sync().await.unwrap();
3807 drop(journal);
3808 let journal = Journal::<_, u64>::init(context.child("storage"), cfg)
3809 .await
3810 .unwrap();
3811 for value in start..end + 8 {
3812 let expected = if value < end { value } else { value + 100 };
3813 assert_eq!(journal.read(value).await.unwrap(), expected);
3814 }
3815 });
3816 }
3817 }
3818 }
3819 }
3820
3821 #[test]
3822 fn test_init_at_most_skips_discarded_blobs() {
3823 deterministic::Runner::default().start(|context| async move {
3824 let cfg = Config {
3825 partition: "variable-cap".into(),
3826 items_per_section: NZU64!(5),
3827 compression: None,
3828 codec_config: (),
3829 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(3)),
3830 write_buffer: NZUsize!(2048),
3831 replay_buffer: NZUsize!(2048),
3832 };
3833 let mut journal = Journal::<_, u64>::init(context.child("storage"), cfg.clone())
3834 .await
3835 .unwrap();
3836 for value in 0..23 {
3837 (journal, _) = journal.append(&value).await.unwrap();
3838 }
3839 journal = journal.sync().await.unwrap();
3840 drop(journal);
3841 let (blob, _) = context
3842 .open(&cfg.data_partition(), &3u64.to_be_bytes())
3843 .await
3844 .unwrap();
3845 blob.resize(1).await.unwrap();
3846 blob.sync().await.unwrap();
3847 drop(blob);
3848 let journal = Journal::<_, u64>::init_at_most(context.child("storage"), cfg.clone(), 7)
3849 .await
3850 .unwrap();
3851 assert_eq!(journal.bounds(), 0..7);
3852 drop(journal);
3853 let journal = Journal::<_, u64>::init(context.child("storage"), cfg)
3854 .await
3855 .unwrap();
3856 assert_eq!(journal.bounds(), 0..7);
3857 });
3858 }
3859
3860 #[test_traced]
3861 fn test_replay_read_options_reach_history_and_tip() {
3862 const PAGE_SIZE: NonZeroU16 = NZU16!(40);
3863
3864 let executor = deterministic::Runner::default();
3865 executor.start(|context| async move {
3866 let (context, recordings) = RecordingContext::new(context);
3867 let page_cache = CacheRef::new(PAGE_SIZE, NZUsize!(8));
3868 let cfg = Config {
3869 partition: "variable-replay-read-options".into(),
3870 items_per_section: NZU64!(20),
3871 compression: None,
3872 codec_config: (),
3873 page_cache: page_cache.clone(),
3874 write_buffer: NZUsize!(1024),
3875 replay_buffer: NZUsize!(1024),
3876 };
3877 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg)
3878 .await
3879 .unwrap();
3880
3881 for item in 0..35 {
3883 (journal, _) = journal.append(&item).await.unwrap();
3884 }
3885 journal = journal.sync().await.unwrap();
3886
3887 page_cache.clear();
3889 {
3890 let stream = replay_items(
3891 &journal,
3892 0..journal.bounds().end,
3893 NZUsize!(32),
3894 ReadOptions::DONT_CACHE,
3895 )
3896 .await
3897 .unwrap();
3898 recordings.clear();
3899 futures::pin_mut!(stream);
3900 assert_eq!(stream.next().await.unwrap().unwrap(), (0, 0));
3901
3902 let reads = recordings.snapshot().reads;
3903 assert!(!reads.is_empty());
3904 assert!(
3905 reads
3906 .iter()
3907 .all(|options| *options == ReadOptions::DONT_CACHE)
3908 );
3909 }
3910
3911 for options in [ReadOptions::default(), ReadOptions::DONT_CACHE] {
3914 page_cache.clear();
3915 {
3916 let stream =
3917 replay_items(&journal, 20..journal.bounds().end, NZUsize!(2048), options)
3918 .await
3919 .unwrap();
3920 recordings.clear();
3921 futures::pin_mut!(stream);
3922 for item in 20..35 {
3923 assert_eq!(stream.next().await.unwrap().unwrap(), (item, item));
3924 }
3925 assert!(stream.next().await.is_none());
3926 assert_eq!(recordings.snapshot().reads, vec![options]);
3927 }
3928 recordings.clear();
3929 assert_eq!(journal.read(20).await.unwrap(), 20);
3930 assert!(!recordings.snapshot().reads.is_empty());
3931 recordings.clear();
3932 assert_eq!(journal.read(20).await.unwrap(), 20);
3933 assert!(recordings.snapshot().reads.is_empty());
3934 }
3935
3936 for item in 35..39 {
3940 (journal, _) = journal.append(&item).await.unwrap();
3941 }
3942 page_cache.clear();
3943 {
3944 let stream = replay_items(
3945 &journal,
3946 20..journal.bounds().end,
3947 NZUsize!(2048),
3948 ReadOptions::default(),
3949 )
3950 .await
3951 .unwrap();
3952 recordings.clear();
3953 futures::pin_mut!(stream);
3954 assert_eq!(stream.next().await.unwrap().unwrap(), (20, 20));
3955 assert_eq!(
3956 recordings.snapshot().reads,
3957 vec![ReadOptions::DONT_CACHE; 3]
3958 );
3959 }
3960
3961 journal.destroy().await.unwrap();
3962 });
3963 }
3964
3965 #[test]
3966 fn test_start_sync_keeps_predecessor_sync() {
3967 let executor = deterministic::Runner::default();
3968 executor.start(|context| async move {
3969 let cfg = Config {
3970 partition: "variable-start-sync-predecessor".into(),
3971 items_per_section: NZU64!(3),
3972 compression: None,
3973 codec_config: (),
3974 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
3975 write_buffer: NZUsize!(2048),
3976 replay_buffer: NZUsize!(2048),
3977 };
3978 let mut journal = Box::new(Inner::<_, u64>::init(context, cfg).await.unwrap());
3979
3980 (journal, _) = journal
3981 .append_many(Many::Flat(&[1, 2, 3, 4]))
3982 .await
3983 .unwrap();
3984 assert!(journal.blobs.has_tail_predecessor_sync());
3985
3986 let (journal, handle) = journal.start_sync().await.unwrap();
3988 assert!(journal.blobs.has_tail_predecessor_sync());
3989 handle.await.unwrap();
3990 assert!(journal.blobs.has_tail_predecessor_sync());
3991
3992 journal.destroy().await.unwrap();
3993 });
3994 }
3995
3996 #[test_traced]
3997 fn test_start_sync_stages_offsets_while_data_waits() {
3998 for release_data_first in [false, true] {
3999 deterministic::Runner::default().start(|context| async move {
4000 let pending = PendingSyncs::default();
4001 let cfg = Config {
4002 partition: "variable-concurrent-staging".into(),
4003 items_per_section: NZU64!(100),
4004 compression: None,
4005 codec_config: (),
4006 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4007 write_buffer: NZUsize!(2048),
4008 replay_buffer: NZUsize!(2048),
4009 };
4010 let mut journal = Box::new(
4011 Inner::<_, u64>::init(
4012 DelayedSyncContext {
4013 inner: context.child("journal"),
4014 pending: pending.clone(),
4015 },
4016 cfg,
4017 )
4018 .await
4019 .unwrap(),
4020 );
4021 (journal, _) = journal.append(&1).await.unwrap();
4022 let (mut journal, first) = journal.start_sync().await.unwrap();
4023 let prior_data = next_pending_sync(&pending);
4024 next_pending_sync(&pending).release.send(Ok(())).unwrap();
4025
4026 (journal, _) = journal.append(&2).await.unwrap();
4028 let starts = pending.starts();
4029 let mut starting = std::pin::pin!(journal.start_sync());
4030 assert!(futures::poll!(starting.as_mut()).is_pending());
4031 assert_eq!(pending.starts(), starts + 1);
4032 prior_data.blocked.await.unwrap();
4033 let offsets = next_pending_sync(&pending);
4034 prior_data.release.send(Ok(())).unwrap();
4035 let (mut journal, handle) = starting.await.unwrap();
4036 first.await.unwrap();
4037 let data = next_pending_sync(&pending);
4038 release_pending_syncs(&pending);
4039
4040 let (first, last) = if release_data_first {
4041 (data, offsets)
4042 } else {
4043 (offsets, data)
4044 };
4045 first.release.send(Ok(())).unwrap();
4046 let mut handle = std::pin::pin!(handle);
4047 assert!(futures::poll!(handle.as_mut()).is_pending());
4048 assert_eq!(journal.barrier.boundary(), 1);
4049 last.release.send(Ok(())).unwrap();
4050 handle.await.unwrap();
4051 assert_eq!(journal.barrier.boundary(), 2);
4052 journal.destroy().await.unwrap();
4053 });
4054 }
4055 }
4056
4057 #[test_traced]
4058 fn test_start_sync_advances_offsets_watermark_lagged() {
4059 let executor = deterministic::Runner::default();
4060 executor.start(|context| async move {
4061 let pending = PendingSyncs::default();
4062 let cfg = Config {
4063 partition: "variable-watermark-lagged".into(),
4064 items_per_section: NZU64!(100),
4065 compression: None,
4066 codec_config: (),
4067 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4068 write_buffer: NZUsize!(2048),
4069 replay_buffer: NZUsize!(2048),
4070 };
4071 let make = |pending: PendingSyncs| {
4072 Inner::<_, u64>::init(
4073 DelayedSyncContext {
4074 inner: context.child("journal"),
4075 pending,
4076 },
4077 cfg.clone(),
4078 )
4079 };
4080 let mut journal = Box::new(make(pending.clone()).await.unwrap());
4081
4082 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
4084 let (mut journal, h1) = journal.start_sync().await.unwrap();
4085 assert_eq!(journal.offsets.recovery_watermark(), 0);
4086
4087 release_pending_syncs(&pending);
4088 drive_pending_syncs(&pending, h1).await.unwrap();
4089
4090 (journal, _) = journal.append(&4).await.unwrap();
4093 let (journal, h2) = journal.start_sync().await.unwrap();
4094 assert_eq!(journal.offsets.recovery_watermark(), 3);
4095 drive_pending_syncs(&pending, h2).await.unwrap();
4096
4097 pending.unblock();
4100 drop(journal);
4101 let journal = make(pending.clone()).await.unwrap();
4102 assert_eq!(journal.offsets.recovery_watermark(), 4);
4103 assert_eq!(journal.bounds, 0..4);
4104 for i in 0..4u64 {
4105 assert_eq!(journal.reader().read(i).await.unwrap(), i + 1);
4106 }
4107 journal.destroy().await.unwrap();
4108 });
4109 }
4110
4111 #[test_traced]
4112 fn test_start_sync_watermark_requires_joint_proof() {
4113 let executor = deterministic::Runner::default();
4114 executor.start(|context| async move {
4115 let pending = PendingSyncs::default();
4116 let cfg = Config {
4117 partition: "variable-watermark-joint".into(),
4118 items_per_section: NZU64!(3),
4119 compression: None,
4120 codec_config: (),
4121 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4122 write_buffer: NZUsize!(2048),
4123 replay_buffer: NZUsize!(2048),
4124 };
4125 let mut journal = Box::new(
4126 Inner::<_, u64>::init(
4127 DelayedSyncContext {
4128 inner: context.child("journal"),
4129 pending: pending.clone(),
4130 },
4131 cfg,
4132 )
4133 .await
4134 .unwrap(),
4135 );
4136
4137 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
4140 let (mut journal, h1) = journal.start_sync().await.unwrap();
4141
4142 let offsets_sealed = next_pending_sync(&pending);
4144 let data_sealed = next_pending_sync(&pending);
4145 offsets_sealed.release.send(Ok(())).unwrap();
4146 data_sealed
4147 .release
4148 .send(Err(commonware_runtime::Error::Io(
4149 std::io::Error::other("injected sync failure").into(),
4150 )))
4151 .unwrap();
4152 release_pending_syncs(&pending);
4153 assert!(h1.await.is_err());
4154
4155 assert_eq!(journal.barrier.boundary(), 0);
4159 let (journal, h2) = journal.start_sync().await.unwrap();
4160 assert_eq!(journal.offsets.recovery_watermark(), 0);
4161 assert!(h2.await.is_err());
4162 });
4163 }
4164
4165 #[test]
4166 fn test_truncate_truncates_durable_size() {
4167 let executor = deterministic::Runner::default();
4168 executor.start(|context| async move {
4169 let pending = PendingSyncs::default();
4170 let cfg = Config {
4171 partition: "variable-rewind-truncate".into(),
4172 items_per_section: NZU64!(100),
4173 compression: None,
4174 codec_config: (),
4175 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4176 write_buffer: NZUsize!(2048),
4177 replay_buffer: NZUsize!(2048),
4178 };
4179 let mut journal = Box::new(
4180 Inner::<_, u64>::init(
4181 DelayedSyncContext {
4182 inner: context.child("journal"),
4183 pending: pending.clone(),
4184 },
4185 cfg,
4186 )
4187 .await
4188 .unwrap(),
4189 );
4190
4191 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
4192 let journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
4193 assert_eq!(journal.offsets.recovery_watermark(), 3);
4194
4195 let mut journal = drive_pending_syncs(&pending, journal.test_truncate(2))
4198 .await
4199 .unwrap();
4200 (journal, _) = journal.append(&9).await.unwrap();
4201 let (mut journal, h1) = journal.start_sync().await.unwrap();
4202 let data = next_pending_sync(&pending);
4203 release_pending_syncs(&pending);
4204 data.release
4205 .send(Err(commonware_runtime::Error::Io(
4206 std::io::Error::other("injected sync failure").into(),
4207 )))
4208 .unwrap();
4209 assert!(h1.await.is_err());
4210
4211 assert_eq!(journal.barrier.boundary(), 2);
4214 assert_eq!(journal.offsets.recovery_watermark(), 2);
4215 assert!(journal.start_sync().await.is_err());
4216 });
4217 }
4218
4219 #[test]
4220 fn test_start_sync_watermark_advance_deferred_failure() {
4221 let executor = deterministic::Runner::default();
4222 executor.start(|context| async move {
4223 let pending = PendingSyncs::default();
4224 let cfg = Config {
4225 partition: "variable-watermark-deferred".into(),
4226 items_per_section: NZU64!(100),
4227 compression: None,
4228 codec_config: (),
4229 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4230 write_buffer: NZUsize!(2048),
4231 replay_buffer: NZUsize!(2048),
4232 };
4233 let mut journal = Box::new(
4234 Inner::<_, u64>::init(
4235 DelayedSyncContext {
4236 inner: context.child("journal"),
4237 pending: pending.clone(),
4238 },
4239 cfg,
4240 )
4241 .await
4242 .unwrap(),
4243 );
4244
4245 (journal, _) = journal.append_many(Many::Flat(&[1, 2, 3])).await.unwrap();
4246 let (journal, h1) = journal.start_sync().await.unwrap();
4247 release_pending_syncs(&pending);
4248 h1.await.unwrap();
4249
4250 let (mut journal, h2) = journal.start_sync().await.unwrap();
4253 fail_pending_syncs(&pending);
4254 assert!(h2.await.is_err());
4255
4256 (journal, _) = journal.append(&4).await.unwrap();
4259 let journal = drive_pending_syncs(&pending, journal.commit())
4260 .await
4261 .unwrap();
4262 assert!(drive_pending_syncs(&pending, journal.sync()).await.is_err());
4263 });
4264 }
4265
4266 #[test_traced]
4268 fn test_variable_start_sync_flush_failure_returns_err() {
4269 let executor = deterministic::Runner::default();
4270 executor.start(|context| async move {
4271 let cfg = Config {
4272 partition: "variable-start-sync-flush-failure".into(),
4273 items_per_section: NZU64!(3),
4274 compression: None,
4275 codec_config: (),
4276 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4277 write_buffer: NZUsize!(2048),
4278 replay_buffer: NZUsize!(2048),
4279 };
4280 let mut journal = Box::new(
4281 Inner::<_, u64>::init(context.child("journal"), cfg)
4282 .await
4283 .unwrap(),
4284 );
4285
4286 (journal, _) = journal.append(&0).await.unwrap();
4288
4289 *context.storage_fault_config().write() = deterministic::FaultConfig {
4291 write_rate: Some(deterministic::WriteConfig {
4292 failure_rate: probability!(1.0),
4293 retention_rate: probability!(0.0),
4294 mode: deterministic::PartialWriteMode::Prefix,
4295 }),
4296 ..Default::default()
4297 };
4298 assert!(matches!(journal.start_sync().await, Err(Error::Runtime(_))));
4299 });
4300 }
4301
4302 #[test_traced]
4304 fn test_variable_dropped_failed_start_sync_surfaces_after_rollover() {
4305 let executor = deterministic::Runner::default();
4306 executor.start(|context| async move {
4307 let pending = PendingSyncs::default();
4308 let cfg = Config {
4309 partition: "variable-dropped-failed-sync".into(),
4310 items_per_section: NZU64!(3),
4311 compression: None,
4312 codec_config: (),
4313 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4314 write_buffer: NZUsize!(2048),
4315 replay_buffer: NZUsize!(2048),
4316 };
4317 let mut journal = Box::new(
4318 Inner::<_, u64>::init(
4319 DelayedSyncContext {
4320 inner: context.child("journal"),
4321 pending: pending.clone(),
4322 },
4323 cfg,
4324 )
4325 .await
4326 .unwrap(),
4327 );
4328
4329 (journal, _) = journal.append(&0).await.unwrap();
4332 let (journal, handle) = journal.start_sync().await.unwrap();
4333 drop(handle);
4334 next_pending_sync(&pending)
4335 .release
4336 .send(Err(commonware_runtime::Error::Io(
4337 std::io::Error::other("injected sync failure").into(),
4338 )))
4339 .unwrap();
4340 release_pending_syncs(&pending);
4341
4342 assert!(matches!(
4344 journal.append_many(Many::Flat(&[1, 2, 3])).await,
4345 Err(Error::Runtime(_))
4346 ));
4347 });
4348 }
4349
4350 #[test_traced]
4352 fn test_variable_dropped_failed_offsets_sync_fails_next_start_sync() {
4353 let executor = deterministic::Runner::default();
4354 executor.start(|context| async move {
4355 let pending = PendingSyncs::default();
4356 let cfg = Config {
4357 partition: "variable-dropped-failed-offsets-sync".into(),
4358 items_per_section: NZU64!(3),
4359 compression: None,
4360 codec_config: (),
4361 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(2)),
4362 write_buffer: NZUsize!(2048),
4363 replay_buffer: NZUsize!(2048),
4364 };
4365 let mut journal = Box::new(
4366 Inner::<_, u64>::init(
4367 DelayedSyncContext {
4368 inner: context.child("journal"),
4369 pending: pending.clone(),
4370 },
4371 cfg,
4372 )
4373 .await
4374 .unwrap(),
4375 );
4376
4377 (journal, _) = journal.append(&0).await.unwrap();
4380 let (journal, handle) = journal.start_sync().await.unwrap();
4381 drop(handle);
4382 next_pending_sync(&pending).release.send(Ok(())).unwrap();
4383 next_pending_sync(&pending)
4384 .release
4385 .send(Err(commonware_runtime::Error::Io(
4386 std::io::Error::other("injected sync failure").into(),
4387 )))
4388 .unwrap();
4389 release_pending_syncs(&pending);
4390
4391 let journal = journal.commit().await.unwrap();
4393
4394 assert!(matches!(journal.start_sync().await, Err(Error::Runtime(_))));
4396 });
4397 }
4398
4399 fn counter(buffer: &str, name: &str) -> u64 {
4401 buffer
4402 .lines()
4403 .find(|l| l.contains(name) && !l.starts_with('#'))
4404 .and_then(|l| l.split_whitespace().last())
4405 .and_then(|v| v.parse().ok())
4406 .expect("counter missing")
4407 }
4408
4409 #[test_traced]
4410 fn test_variable_init_syncs_adopted_data_before_offsets_watermark_advance() {
4411 let executor = deterministic::Runner::default();
4412 executor.start(|context| async move {
4413 let cfg = Config {
4414 partition: "init-adopted-variable".into(),
4415 items_per_section: NZU64!(10),
4416 compression: None,
4417 codec_config: (),
4418 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4419 write_buffer: NZUsize!(1024),
4420 replay_buffer: NZUsize!(1024),
4421 };
4422
4423 let mut journal =
4424 Journal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone())
4425 .await
4426 .unwrap();
4427 (journal, _) = journal.append(&FixedBytes::new([1; 32])).await.unwrap();
4428 (journal, _) = journal.append(&FixedBytes::new([2; 32])).await.unwrap();
4429 let journal = journal.sync().await.unwrap();
4430 let journal = journal
4433 .test_set_offsets_recovery_watermark(1)
4434 .await
4435 .unwrap();
4436 drop(journal);
4437
4438 let data_partition = format!("{}{}", cfg.partition, DATA_SUFFIX);
4443 let context = commonware_runtime::mocks::SyncFaultContext {
4444 inner: context,
4445 fail_partition: data_partition,
4446 };
4447 assert!(
4448 Journal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone())
4449 .await
4450 .is_err(),
4451 "init must sync adopted data before advancing rebuilt offsets"
4452 );
4453 });
4454 }
4455
4456 #[test_traced]
4457 fn test_variable_append_many_compressed() {
4458 let executor = deterministic::Runner::default();
4459 executor.start(|context| async move {
4460 let cfg = Config {
4461 partition: "append-many-compressed".into(),
4462 items_per_section: NZU64!(3),
4463 compression: Some(1),
4464 codec_config: (),
4465 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4466 write_buffer: NZUsize!(1024),
4467 replay_buffer: NZUsize!(1024),
4468 };
4469 let mut journal = Journal::<_, FixedBytes<32>>::init(context.child("journal"), cfg)
4470 .await
4471 .unwrap();
4472 let items = [
4473 FixedBytes::new([0; 32]),
4474 FixedBytes::new([1; 32]),
4475 FixedBytes::new([2; 32]),
4476 FixedBytes::new([3; 32]),
4477 FixedBytes::new([4; 32]),
4478 ];
4479
4480 let last;
4481 (journal, last) = journal.append_many(Many::Flat(&items)).await.unwrap();
4482 assert_eq!(last, 4);
4483 for (pos, item) in items.iter().enumerate() {
4484 assert_eq!(journal.read(pos as u64).await.unwrap(), *item);
4485 }
4486
4487 journal.destroy().await.unwrap();
4488 });
4489 }
4490
4491 #[test_traced]
4492 fn test_variable_prepared_compressed_capacity() {
4493 let executor = deterministic::Runner::default();
4494 executor.start(|mut context| async move {
4495 let cfg = Config {
4496 partition: "prepared-compressed-capacity".into(),
4497 items_per_section: NZU64!(1024),
4498 compression: Some(3),
4499 codec_config: (..).into(),
4500 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4501 write_buffer: NZUsize!(1024),
4502 replay_buffer: NZUsize!(1024),
4503 };
4504 let journal = Journal::<_, Bytes>::init(context.child("journal"), cfg)
4505 .await
4506 .unwrap();
4507
4508 let large = Bytes::from(vec![0xAB; 1 << 20]);
4511 let small: Vec<_> = (0..4096)
4512 .map(|_| {
4513 let mut record = vec![0; 64];
4514 context.fill_bytes(&mut record);
4515 Bytes::from(record)
4516 })
4517 .collect();
4518 let mixed: Vec<_> = small[..64].iter().cloned().chain([large.clone()]).collect();
4519
4520 let immediate = journal
4522 .0
4523 .prepare_append::<false>(Many::Flat(std::slice::from_ref(&large)))
4524 .unwrap();
4525 let len = immediate.encoded.len();
4526 let spare = immediate.encoded.capacity() - len;
4527 assert!(spare > PREPARED_SPARE_LIMIT.max(len.saturating_mul(3)));
4528
4529 for batch in [vec![large], small, mixed] {
4531 let prepared = journal.prepare_append(Many::Flat(&batch)).unwrap();
4532 let mut expected = Vec::new();
4533 let mut starts = Vec::new();
4534 for record in &batch {
4535 starts.push(expected.len());
4536 let mut frame = Vec::new();
4537 encode_frame_into(Some(3), record, &mut frame).unwrap();
4538 expected.extend_from_slice(&frame);
4539 }
4540 assert_eq!(prepared.encoded, expected);
4541 assert_eq!(prepared.item_starts, starts);
4542
4543 let len = prepared.encoded.len();
4544 let spare = prepared.encoded.capacity() - len;
4545 assert!(
4546 spare <= PREPARED_SPARE_LIMIT.max(len.saturating_mul(3)),
4547 "{spare} unused bytes for {len} encoded bytes"
4548 );
4549 }
4550
4551 journal.destroy().await.unwrap();
4552 });
4553 }
4554
4555 #[test_traced]
4556 fn test_variable_compressed_frames_cross_pages() {
4557 let executor = deterministic::Runner::default();
4558 executor.start(|mut context| async move {
4559 let cfg = Config {
4560 partition: "compressed-cross-pages".into(),
4561 items_per_section: NZU64!(3),
4562 compression: Some(3),
4563 codec_config: ((..=4096).into(), ()),
4564 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4565 write_buffer: NZUsize!(1024),
4566 replay_buffer: NZUsize!(64),
4567 };
4568
4569 let items: Vec<Vec<u8>> = (0..10)
4572 .map(|i| {
4573 let mut item = vec![i as u8; 1500 + i * 97];
4574 if i % 3 != 2 {
4575 context.fill_bytes(&mut item);
4576 }
4577 item
4578 })
4579 .collect();
4580 let mut journal = Journal::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
4581 .await
4582 .unwrap();
4583 for item in &items {
4584 (journal, _) = journal.append(item).await.unwrap();
4585 }
4586 journal = journal.sync().await.unwrap();
4587 drop(journal);
4588
4589 let cfg = Config {
4592 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4593 ..cfg
4594 };
4595 let journal = Journal::<_, Vec<u8>>::init(context.child("second"), cfg)
4596 .await
4597 .unwrap();
4598 {
4599 let stream = replay_items(
4600 &journal,
4601 0..journal.bounds().end,
4602 NZUsize!(64),
4603 ReadOptions::default(),
4604 )
4605 .await
4606 .unwrap();
4607 futures::pin_mut!(stream);
4608 let mut expected = 0u64;
4609 while let Some(result) = stream.next().await {
4610 let (pos, item) = result.unwrap();
4611 assert_eq!(pos, expected);
4612 assert_eq!(item, items[pos as usize]);
4613 expected += 1;
4614 }
4615 assert_eq!(expected, items.len() as u64);
4616 }
4617 for (pos, item) in items.iter().enumerate() {
4618 assert_eq!(&journal.read(pos as u64).await.unwrap(), item);
4619 }
4620 let positions: Vec<u64> = (0..items.len() as u64).collect();
4621 assert_eq!(journal.read_many(&positions).await.unwrap(), items);
4622 journal.destroy().await.unwrap();
4623 });
4624 }
4625
4626 #[test_traced]
4627 fn test_variable_append_many_exceeding_write_buffer_reopens_across_sections() {
4628 let executor = deterministic::Runner::default();
4629 executor.start(|context| async move {
4630 let cfg = Config {
4631 partition: "append-many-exceeds-buffer".into(),
4632 items_per_section: NZU64!(5),
4633 compression: None,
4634 codec_config: (),
4635 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4636 write_buffer: NZUsize!(512),
4637 replay_buffer: NZUsize!(512),
4638 };
4639 let items = (0..13)
4640 .map(|i| FixedBytes::new([i as u8; 300]))
4641 .collect::<Vec<_>>();
4642
4643 let mut journal =
4644 Journal::<_, FixedBytes<300>>::init(context.child("first"), cfg.clone())
4645 .await
4646 .unwrap();
4647 let appended;
4648 (journal, appended) = journal.append_many(Many::Flat(&items)).await.unwrap();
4649 assert_eq!(appended, 12);
4650 journal.sync().await.unwrap();
4651
4652 let journal = Journal::<_, FixedBytes<300>>::init(context.child("second"), cfg)
4653 .await
4654 .unwrap();
4655 assert_eq!(journal.bounds(), 0..13);
4656 for (pos, item) in items.iter().enumerate() {
4657 assert_eq!(journal.read(pos as u64).await.unwrap(), *item);
4658 }
4659 assert_eq!(
4660 journal.read_many(&[0, 4, 5, 9, 10, 12]).await.unwrap(),
4661 vec![
4662 items[0].clone(),
4663 items[4].clone(),
4664 items[5].clone(),
4665 items[9].clone(),
4666 items[10].clone(),
4667 items[12].clone(),
4668 ]
4669 );
4670
4671 journal.destroy().await.unwrap();
4672 });
4673 }
4674
4675 #[test_traced]
4676 fn test_variable_init_at_max_size_rejected() {
4677 let executor = deterministic::Runner::default();
4678 executor.start(|context| async move {
4679 let cfg = Config {
4680 partition: "init-at-max".into(),
4681 items_per_section: NZU64!(5),
4682 compression: None,
4683 codec_config: (),
4684 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4685 write_buffer: NZUsize!(1024),
4686 replay_buffer: NZUsize!(1024),
4687 };
4688
4689 assert!(matches!(
4691 Journal::<_, u64>::init_at_size(context.child("max"), cfg, u64::MAX).await,
4692 Err(Error::SizeOverflow)
4693 ));
4694 });
4695 }
4696
4697 #[test_traced]
4698 fn test_variable_append_size_overflow() {
4699 let executor = deterministic::Runner::default();
4700 executor.start(|context| async move {
4701 let cfg = Config {
4702 partition: "append-size-overflow".into(),
4703 items_per_section: NZU64!(5),
4704 compression: None,
4705 codec_config: (),
4706 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4707 write_buffer: NZUsize!(1024),
4708 replay_buffer: NZUsize!(1024),
4709 };
4710
4711 let mut journal =
4713 Journal::<_, u64>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
4714 .await
4715 .unwrap();
4716
4717 let appended;
4719 (journal, appended) = journal.append(&7).await.unwrap();
4720 assert_eq!(appended, u64::MAX - 1);
4721 assert_eq!(journal.size(), u64::MAX);
4722
4723 assert!(matches!(journal.append(&8).await, Err(Error::SizeOverflow)));
4726 });
4727 }
4728
4729 #[test_traced]
4730 fn test_variable_replay_near_max_size() {
4731 let executor = deterministic::Runner::default();
4732 executor.start(|context| async move {
4733 let cfg = Config {
4734 partition: "replay-near-max-size".into(),
4735 items_per_section: NZU64!(10),
4736 compression: None,
4737 codec_config: (),
4738 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4739 write_buffer: NZUsize!(1024),
4740 replay_buffer: NZUsize!(1024),
4741 };
4742
4743 let mut journal =
4744 Journal::<_, u64>::init_at_size(context.child("near_max"), cfg, u64::MAX - 1)
4745 .await
4746 .unwrap();
4747 let appended;
4748 (journal, appended) = journal.append(&7).await.unwrap();
4749 assert_eq!(appended, u64::MAX - 1);
4750
4751 {
4752 let reader;
4753 (journal, reader) = journal.snapshot().await.unwrap();
4754 let stream = replay_items(
4755 &reader,
4756 u64::MAX - 1..reader.bounds().end,
4757 NZUsize!(20),
4758 ReadOptions::default(),
4759 )
4760 .await
4761 .unwrap();
4762 futures::pin_mut!(stream);
4763 let (pos, item) = stream.next().await.unwrap().unwrap();
4764 assert_eq!(pos, u64::MAX - 1);
4765 assert_eq!(item, 7);
4766 assert!(stream.next().await.is_none());
4767 }
4768
4769 journal.destroy().await.unwrap();
4770 });
4771 }
4772
4773 #[test_traced]
4774 fn test_variable_sync_read_preserves_byte_fields() {
4775 let executor = deterministic::Runner::default();
4776 executor.start(|context| async move {
4777 let cfg = Config {
4778 partition: "sync-read-ownership".into(),
4779 items_per_section: NZU64!(5),
4780 compression: None,
4781 codec_config: ((..).into(), ()),
4782 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(8)),
4783 write_buffer: NZUsize!(1024),
4784 replay_buffer: NZUsize!(1024),
4785 };
4786 let value = vec![View::new(1), View::new(2)];
4787 let mut journal = Journal::<_, Vec<View>>::init(context, cfg).await.unwrap();
4788 (journal, _) = journal.append(&value).await.unwrap();
4789 journal = journal.sync().await.unwrap();
4790 let (journal, reader) = journal.snapshot().await.unwrap();
4791 drop(reader.read(0).await.unwrap());
4792 let offset = reader.offsets.try_read_sync(0).unwrap();
4793 let decoded = reader
4794 .try_read_frame_sync(0, offset, &mut BytesMut::new())
4795 .unwrap();
4796 drop(reader);
4797 journal.destroy().await.unwrap();
4798 assert_eq!(decoded.len(), 2);
4799 for (field, expected) in decoded.iter().zip([1u64, 2]) {
4800 assert_eq!(field.bytes.as_ref(), &expected.to_be_bytes());
4801 field.assert_shared();
4802 }
4803 });
4804 }
4805
4806 #[test_traced]
4807 fn test_variable_read_many_buffer_len() {
4808 assert_eq!(read_buffer_len([]), Some(0));
4809 assert_eq!(read_buffer_len([0, 302, 0, 302]), Some(604));
4810 assert_eq!(
4811 read_buffer_len([MAX_ALLOCATION_BYTES - 1, 1]),
4812 Some(MAX_ALLOCATION_BYTES)
4813 );
4814 assert_eq!(read_buffer_len([MAX_ALLOCATION_BYTES, 1]), None);
4815 assert_eq!(
4816 read_buffer_len([MAX_ALLOCATION_BYTES, MAX_ALLOCATION_BYTES, 2]),
4817 None
4818 );
4819 }
4820
4821 #[test_traced]
4822 fn test_variable_read_many_preserves_byte_fields() {
4823 deterministic::Runner::default().start(|context| async move {
4824 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(8));
4825 let cfg = Config {
4826 partition: "read-many-ownership".into(),
4827 items_per_section: NZU64!(5),
4828 compression: None,
4829 codec_config: ((..).into(), ()),
4830 page_cache: page_cache.clone(),
4831 write_buffer: NZUsize!(1024),
4832 replay_buffer: NZUsize!(1024),
4833 };
4834 let values: Vec<Vec<View>> = (0..6)
4835 .map(|position| {
4836 (0..40)
4837 .map(|field| View::new(position * 40 + field))
4838 .collect()
4839 })
4840 .collect();
4841 let mut frame = Vec::new();
4842 encode_frame_into(None, &values[0], &mut frame).unwrap();
4843 let frame_len = frame.len();
4844 let mut journal = Journal::<_, Vec<View>>::init(context, cfg).await.unwrap();
4845 for value in &values {
4846 (journal, _) = journal.append(value).await.unwrap();
4847 }
4848 journal = journal.sync().await.unwrap();
4849 let (journal, reader) = journal.snapshot().await.unwrap();
4850 let positions = [0, 1, 2];
4851
4852 page_cache.clear();
4854 assert!(
4855 reader
4856 .try_read_many_sync(&positions)
4857 .iter()
4858 .all(Option::is_none)
4859 );
4860 let cold = reader.read_many(&positions).await.unwrap();
4861 let probed: Vec<_> = reader
4862 .try_read_many_sync(&positions)
4863 .into_iter()
4864 .map(|item| item.expect("cached frame is served"))
4865 .collect();
4866 let warm = reader.read_many(&positions).await.unwrap();
4867 drop(reader);
4868 journal.destroy().await.unwrap();
4869
4870 for (decoded, shared) in [(cold, 3), (probed, 3), (warm, 3)] {
4874 assert_eq!(decoded.len(), positions.len());
4875 for (item, expected) in decoded.iter().zip(&values) {
4876 assert_eq!(item.len(), expected.len());
4877 for (field, expected) in item.iter().zip(expected) {
4878 assert_eq!(field.bytes, expected.bytes);
4879 }
4880 }
4881 for field in decoded[..shared].iter().flatten() {
4882 field.assert_shared();
4883 }
4884 for pair in decoded.windows(2) {
4885 assert_eq!(
4886 pair[1][0].bytes.as_ptr(),
4887 pair[0][0].bytes.as_ptr().wrapping_add(frame_len)
4888 );
4889 }
4890 }
4891 });
4892 }
4893
4894 #[test]
4895 fn test_variable_frame_span_preserves_byte_fields() {
4896 let fields = vec![Bytes::from_static(b"hello"), Bytes::from_static(b"world")];
4897 let mut frame = Vec::new();
4898 encode_frame_into(None, &fields, &mut frame).unwrap();
4899 let source = Bytes::from(frame);
4900 let range = source.as_ptr_range();
4901 let decoded =
4902 decode_frame_span::<Vec<Bytes>>(source, 0, 0, &((..).into(), (..).into()), false)
4903 .unwrap();
4904 assert_eq!(decoded, fields);
4905 assert!(decoded.iter().all(|field| range.contains(&field.as_ptr())));
4906 }
4907
4908 #[test_traced]
4909 fn test_variable_try_read_many_sync_matches_read_many() {
4910 let executor = deterministic::Runner::default();
4913 executor.start(|context| async move {
4914 let cfg = Config {
4915 partition: "read-many-sync".into(),
4916 items_per_section: NZU64!(5),
4917 compression: None,
4918 codec_config: (),
4919 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(64)),
4920 write_buffer: NZUsize!(1024),
4921 replay_buffer: NZUsize!(1024),
4922 };
4923 let items = (0..13)
4924 .map(|i| FixedBytes::new([i as u8; 300]))
4925 .collect::<Vec<_>>();
4926 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
4927 .await
4928 .unwrap();
4929 (journal, _) = journal.append_many(Many::Flat(&items)).await.unwrap();
4930 journal = journal.sync().await.unwrap();
4931
4932 let positions: Vec<u64> = (0..items.len() as u64).collect();
4933 let reader;
4934 (journal, reader) = journal.snapshot().await.unwrap();
4935 let expected = reader.read_many(&positions).await.unwrap();
4938 let served = reader.try_read_many_sync(&positions);
4939 assert_eq!(served.len(), positions.len());
4940 for (item, expected) in served.iter().zip(&expected) {
4941 assert_eq!(item.as_ref().expect("cached position is served"), expected);
4942 }
4943
4944 let positions = [0, 4, 6, 9, 11, 12, 13, u64::MAX];
4947 let before = context.encode();
4948 let served = reader.try_read_many_sync(&positions);
4949 assert_eq!(served.len(), positions.len());
4950 for (&position, item) in positions.iter().zip(&served) {
4951 if position < items.len() as u64 {
4952 assert_eq!(item.as_ref(), Some(&items[position as usize]));
4953 } else {
4954 assert!(item.is_none());
4955 }
4956 }
4957 let after = context.encode();
4958 assert_eq!(
4959 counter(&after, "offsets_items_read_total")
4960 - counter(&before, "offsets_items_read_total"),
4961 8
4962 );
4963 assert_eq!(
4964 counter(&after, "j_cache_hits") - counter(&before, "j_cache_hits"),
4965 6
4966 );
4967 drop(served);
4968 drop(reader);
4969
4970 journal.destroy().await.unwrap();
4971 });
4972 }
4973
4974 #[test_traced]
4975 #[should_panic(expected = "positions must be strictly increasing")]
4976 fn test_variable_read_many_rejects_unsorted_positions() {
4977 let executor = deterministic::Runner::default();
4978 executor.start(|context| async move {
4979 let cfg = Config {
4980 partition: "read-many-unsorted".into(),
4981 items_per_section: NZU64!(5),
4982 compression: None,
4983 codec_config: (),
4984 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
4985 write_buffer: NZUsize!(1024),
4986 replay_buffer: NZUsize!(1024),
4987 };
4988 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
4989 .await
4990 .unwrap();
4991 for i in 0..5u64 {
4992 (journal, _) = journal.append(&(i * 100)).await.unwrap();
4993 }
4994 journal = journal.sync().await.unwrap();
4995
4996 let (_journal, reader) = journal.snapshot().await.unwrap();
4997 let _ = reader.read_many(&[2, 1]).await;
4998 });
4999 }
5000
5001 #[test_traced]
5002 #[should_panic(expected = "positions must be strictly increasing")]
5003 fn test_variable_read_many_rejects_duplicate_positions() {
5004 let executor = deterministic::Runner::default();
5006 executor.start(|context| async move {
5007 let cfg = Config {
5008 partition: "read-many-duplicate".into(),
5009 items_per_section: NZU64!(5),
5010 compression: None,
5011 codec_config: (),
5012 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
5013 write_buffer: NZUsize!(1024),
5014 replay_buffer: NZUsize!(1024),
5015 };
5016 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
5017 .await
5018 .unwrap();
5019 for i in 0..5u64 {
5020 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5021 }
5022 journal = journal.sync().await.unwrap();
5023
5024 let (_journal, reader) = journal.snapshot().await.unwrap();
5025 let _ = reader.read_many(&[1, 1]).await;
5026 });
5027 }
5028
5029 #[test_traced]
5030 fn test_variable_probe_then_read_many_matches_read_many() {
5031 let executor = deterministic::Runner::default();
5034 executor.start(|context| async move {
5035 let cfg = Config {
5036 partition: "read-many-probe-complete".into(),
5037 items_per_section: NZU64!(5),
5038 compression: None,
5039 codec_config: (),
5040 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
5041 write_buffer: NZUsize!(1024),
5042 replay_buffer: NZUsize!(1024),
5043 };
5044 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
5045 .await
5046 .unwrap();
5047 for i in 0..12u64 {
5048 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5049 }
5050 journal = journal.sync().await.unwrap();
5051
5052 let positions: Vec<u64> = (0..12).collect();
5053 let expected: Vec<u64> = (0..12).map(|i| i * 100).collect();
5054 let reader;
5055 (journal, reader) = journal.snapshot().await.unwrap();
5056 for _ in 0..2 {
5057 let mut served = reader.try_read_many_sync(&positions);
5058 let misses: Vec<u64> = positions
5059 .iter()
5060 .zip(&served)
5061 .filter_map(|(&pos, item)| item.is_none().then_some(pos))
5062 .collect();
5063 let mut fetched = reader.read_many(&misses).await.unwrap().into_iter();
5064 for item in served.iter_mut().filter(|item| item.is_none()) {
5065 *item = fetched.next();
5066 }
5067 let completed: Vec<_> = served.into_iter().map(Option::unwrap).collect();
5068 assert_eq!(completed, expected);
5069 }
5070 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
5071 drop(reader);
5072
5073 journal.destroy().await.unwrap();
5074 });
5075 }
5076
5077 #[test_traced]
5078 fn test_variable_read_many_reuses_probed_offset() {
5079 let executor = deterministic::Runner::default();
5082 executor.start(|context| async move {
5083 let cfg = Config {
5087 partition: "read-many-offset-reuse".into(),
5088 items_per_section: NZU64!(128),
5089 compression: None,
5090 codec_config: (),
5091 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(4)),
5092 write_buffer: NZUsize!(1024),
5093 replay_buffer: NZUsize!(1024),
5094 };
5095 let appended = (0..140)
5096 .map(|i| FixedBytes::new([i as u8; 300]))
5097 .collect::<Vec<_>>();
5098 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5099 .await
5100 .unwrap();
5101 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5102 journal = journal.sync().await.unwrap();
5103 let reader;
5104 (journal, reader) = journal.snapshot().await.unwrap();
5105
5106 let (items, _) = reader.probe_parts(&[0]);
5112 assert!(items[0].is_none());
5113 reader
5114 .read_many(&(128..136).collect::<Vec<u64>>())
5115 .await
5116 .unwrap();
5117 reader.read(4).await.unwrap();
5118
5119 let (items, misses) = reader.probe_parts(&[0]);
5121 assert!(items[0].is_none());
5122 assert_eq!(misses[0].offset, Some(0));
5123
5124 let before = context.encode();
5128 assert_eq!(
5129 reader.read_many(&[0]).await.unwrap(),
5130 vec![appended[0].clone()]
5131 );
5132 let after = context.encode();
5133 assert_eq!(
5134 counter(&after, "offsets_items_read_total"),
5135 counter(&before, "offsets_items_read_total") + 2
5136 );
5137 drop(reader);
5138
5139 journal.destroy().await.unwrap();
5140 });
5141 }
5142
5143 #[test_traced]
5144 fn test_variable_read_many_reuses_probed_successor() {
5145 let executor = deterministic::Runner::default();
5149 executor.start(|context| async move {
5150 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16));
5153 let cfg = Config {
5154 partition: "read-many-successor-reuse".into(),
5155 items_per_section: NZU64!(128),
5156 compression: None,
5157 codec_config: (),
5158 page_cache: page_cache.clone(),
5159 write_buffer: NZUsize!(1024),
5160 replay_buffer: NZUsize!(1024),
5161 };
5162 let appended = (0..128)
5163 .map(|i| FixedBytes::new([i as u8; 300]))
5164 .collect::<Vec<_>>();
5165 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5166 .await
5167 .unwrap();
5168 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5169 let reader;
5170 (journal, reader) = journal.snapshot().await.unwrap();
5171
5172 page_cache.clear();
5175 reader.offsets.read(64).await.unwrap();
5176 assert!(reader.offsets.try_read_sync(63).is_none());
5177 assert!(reader.offsets.try_read_sync(64).is_some());
5178 let (items, misses) = reader.probe_parts(&[63]);
5179 assert!(items[0].is_none());
5180 assert!(misses[0].offset.is_none());
5181 assert!(misses[0].end.is_some());
5182
5183 let before = context.encode();
5185 assert_eq!(
5186 reader.read_many(&[63]).await.unwrap(),
5187 vec![appended[63].clone()]
5188 );
5189 let after = context.encode();
5190 assert_eq!(
5191 counter(&after, "offsets_items_read_total"),
5192 counter(&before, "offsets_items_read_total") + 2
5193 );
5194 drop(reader);
5195
5196 journal.destroy().await.unwrap();
5197 });
5198 }
5199
5200 #[test_traced]
5201 fn test_variable_read_many_cached_frame_with_cold_successor() {
5202 let executor = deterministic::Runner::default();
5203 executor.start(|context| async move {
5204 let (context, recordings) = RecordingContext::new(context);
5205 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16));
5206 let cfg = Config {
5207 partition: "read-many-cached-frame-cold-successor".into(),
5208 items_per_section: NZU64!(128),
5209 compression: None,
5210 codec_config: (),
5211 page_cache: page_cache.clone(),
5212 write_buffer: NZUsize!(1024),
5213 replay_buffer: NZUsize!(1024),
5214 };
5215 let appended = (0..128)
5216 .map(|i| FixedBytes::new([i as u8; 300]))
5217 .collect::<Vec<_>>();
5218 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5219 .await
5220 .unwrap();
5221 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5222 let reader;
5223 (journal, reader) = journal.snapshot().await.unwrap();
5224
5225 page_cache.clear();
5228 assert_eq!(reader.read(62).await.unwrap(), appended[62]);
5229 assert!(reader.offsets.try_read_sync(63).is_some());
5230 assert!(reader.offsets.try_read_sync(64).is_none());
5231 assert_eq!(reader.try_read_sync(63), Some(appended[63].clone()));
5232
5233 recordings.clear();
5234 let before = context.encode();
5235 let positions = [62, 63];
5236 let expected = &appended[62..64];
5237 assert_eq!(
5238 reader.try_read_many_sync(&positions),
5239 expected.iter().cloned().map(Some).collect::<Vec<_>>()
5240 );
5241 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
5242 let after = context.encode();
5243 assert_eq!(
5244 counter(&after, "j_cache_hits") - counter(&before, "j_cache_hits"),
5245 4
5246 );
5247 let recorded = recordings.snapshot();
5248 assert!(recorded.reads.is_empty());
5249 assert!(recorded.batches.is_empty());
5250 assert!(reader.offsets.try_read_sync(64).is_none());
5251 drop(reader);
5252
5253 journal.destroy().await.unwrap();
5254 });
5255 }
5256
5257 #[test_traced]
5258 fn test_variable_read_many_last_frame_uses_blob_size() {
5259 let executor = deterministic::Runner::default();
5263 executor.start(|context| async move {
5264 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16));
5265 let cfg = Config {
5266 partition: "read-many-last-frame".into(),
5267 items_per_section: NZU64!(128),
5268 compression: None,
5269 codec_config: (),
5270 page_cache: page_cache.clone(),
5271 write_buffer: NZUsize!(1024),
5272 replay_buffer: NZUsize!(1024),
5273 };
5274 let appended = (0..6)
5275 .map(|i| FixedBytes::new([i as u8; 300]))
5276 .collect::<Vec<_>>();
5277 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg.clone())
5278 .await
5279 .unwrap();
5280 (journal, _) = journal
5281 .append_many(Many::Flat(&appended[..4]))
5282 .await
5283 .unwrap();
5284 let reader;
5285 (journal, reader) = journal.snapshot().await.unwrap();
5286 (journal, _) = journal
5287 .append_many(Many::Flat(&appended[4..]))
5288 .await
5289 .unwrap();
5290
5291 page_cache.clear();
5295 assert_eq!(reader.try_read_many_sync(&[2, 3]), vec![None, None]);
5296 assert_eq!(reader.read_many(&[2, 3]).await.unwrap(), appended[2..4]);
5297 assert_eq!(
5298 reader.try_read_many_sync(&[3]),
5299 vec![Some(appended[3].clone())]
5300 );
5301 drop(reader);
5302 journal = journal.sync().await.unwrap();
5303 drop(journal);
5304
5305 let mut journal =
5308 Journal::<_, FixedBytes<300>>::init_at_most(context.child("j"), cfg, 4)
5309 .await
5310 .unwrap();
5311 assert_eq!(journal.bounds(), 0..4);
5312 let reader;
5313 (journal, reader) = journal.snapshot().await.unwrap();
5314 page_cache.clear();
5315 assert_eq!(reader.try_read_many_sync(&[3]), vec![None]);
5316 assert_eq!(reader.read_many(&[3]).await.unwrap(), appended[3..4]);
5317 drop(reader);
5318
5319 journal.destroy().await.unwrap();
5320 });
5321 }
5322
5323 #[test_traced]
5324 fn test_variable_read_many_rejects_inconsistent_offsets() {
5325 let executor = deterministic::Runner::default();
5330 executor.start(|context| async move {
5331 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16));
5332 let cfg = Config {
5333 partition: "read-many-inconsistent".into(),
5334 items_per_section: NZU64!(128),
5335 compression: None,
5336 codec_config: (),
5337 page_cache: page_cache.clone(),
5338 write_buffer: NZUsize!(1024),
5339 replay_buffer: NZUsize!(1024),
5340 };
5341 let appended = (0..4)
5342 .map(|i| FixedBytes::new([i as u8; 300]))
5343 .collect::<Vec<_>>();
5344 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5345 .await
5346 .unwrap();
5347 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5348 journal = journal.sync().await.unwrap();
5349
5350 let tampered = |label: &'static str, offsets: &'static [u64]| {
5353 let context = context.child(label);
5354 async move {
5355 let offsets_cfg = fixed::Config {
5356 partition: format!("read-many-inconsistent-{label}"),
5357 items_per_blob: NZU64!(128),
5358 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16)),
5359 write_buffer: NZUsize!(1024),
5360 replay_buffer: NZUsize!(1024),
5361 };
5362 let mut offsets_journal = fixed::Journal::<_, u64>::init(context, offsets_cfg)
5363 .await
5364 .unwrap();
5365 (offsets_journal, _) = offsets_journal
5366 .append_many(Many::Flat(offsets))
5367 .await
5368 .unwrap();
5369 let (_, offsets) = offsets_journal.snapshot().await.unwrap();
5370 offsets
5371 }
5372 };
5373 let reader = |offsets| Reader::<_, FixedBytes<300>> {
5374 data: journal.0.blobs.reader(),
5375 bounds: 0..4,
5376 offsets,
5377 items_per_blob: NZU64!(128),
5378 codec_config: (),
5379 compressed: false,
5380 metrics: journal.0.metrics.clone(),
5381 };
5382
5383 let backwards = reader(tampered("backwards", &[0, 302, 100, 906]).await);
5387 assert_eq!(
5388 backwards.try_read_many_sync(&[0, 1, 2]),
5389 vec![None, None, None]
5390 );
5391 assert!(matches!(
5392 backwards.read_many(&[0, 1, 2]).await,
5393 Err(Error::Corruption(_))
5394 ));
5395 page_cache.clear();
5396 assert!(matches!(
5397 backwards.read_many(&[0, 2]).await,
5398 Err(Error::Corruption(_))
5399 ));
5400
5401 let aliased = reader(tampered("aliased", &[0, 302, 0, 302]).await);
5404 page_cache.clear();
5405 assert!(matches!(
5406 aliased.read_many(&[0, 2]).await,
5407 Err(Error::Corruption(_))
5408 ));
5409 assert_eq!(aliased.read_many(&[0]).await.unwrap(), appended[0..1]);
5410 assert_eq!(aliased.try_read_many_sync(&[0, 2]), vec![None, None]);
5411 assert!(matches!(
5412 aliased.read_many(&[0, 2]).await,
5413 Err(Error::Corruption(_))
5414 ));
5415
5416 let oversized = reader(tampered("oversized", &[0, 302, 604, u64::MAX / 2]).await);
5418 assert_eq!(oversized.try_read_many_sync(&[2]), vec![None]);
5419 assert!(matches!(
5420 oversized.read_many(&[2]).await,
5421 Err(Error::Corruption(_))
5422 ));
5423
5424 let short = reader(tampered("short", &[0, 302, 603, 906]).await);
5427 assert_eq!(short.try_read_many_sync(&[1]), vec![None]);
5428 assert!(matches!(
5429 short.read_many(&[1]).await,
5430 Err(Error::OffsetDataMismatch {
5431 section: 0,
5432 offset: 302,
5433 expected_len: 301,
5434 actual_len: 302,
5435 })
5436 ));
5437 drop(backwards);
5438 drop(aliased);
5439 drop(oversized);
5440 drop(short);
5441
5442 journal.destroy().await.unwrap();
5443 });
5444 }
5445
5446 #[test_traced]
5447 fn test_variable_read_many_rejects_overlapping_prefix_frame() {
5448 let executor = deterministic::Runner::default();
5452 executor.start(|context| async move {
5453 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16));
5454 let cfg = Config {
5455 partition: "read-many-overlapping-prefix".into(),
5456 items_per_section: NZU64!(128),
5457 compression: None,
5458 codec_config: (),
5459 page_cache: page_cache.clone(),
5460 write_buffer: NZUsize!(1024),
5461 replay_buffer: NZUsize!(1024),
5462 };
5463 let appended = (0..128)
5464 .map(|i| FixedBytes::new([i as u8; 300]))
5465 .collect::<Vec<_>>();
5466 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5467 .await
5468 .unwrap();
5469 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5470 journal = journal.sync().await.unwrap();
5471
5472 let mut offsets: Vec<u64> = (0..128).map(|i| i * 302).collect();
5474 offsets[63] = 0;
5475 let mut offsets_journal = fixed::Journal::<_, u64>::init(
5476 context.child("offsets"),
5477 fixed::Config {
5478 partition: "read-many-overlapping-prefix-offsets".into(),
5479 items_per_blob: NZU64!(128),
5480 page_cache: page_cache.clone(),
5481 write_buffer: NZUsize!(1024),
5482 replay_buffer: NZUsize!(1024),
5483 },
5484 )
5485 .await
5486 .unwrap();
5487 (offsets_journal, _) = offsets_journal
5488 .append_many(Many::Flat(&offsets))
5489 .await
5490 .unwrap();
5491 let (_, offsets) = offsets_journal.snapshot().await.unwrap();
5492 let reader = Reader::<_, FixedBytes<300>> {
5493 data: journal.0.blobs.reader(),
5494 bounds: 0..128,
5495 offsets,
5496 items_per_blob: NZU64!(128),
5497 codec_config: (),
5498 compressed: false,
5499 metrics: journal.0.metrics.clone(),
5500 };
5501
5502 page_cache.clear();
5505 assert_eq!(reader.read(0).await.unwrap(), appended[0]);
5506 assert!(reader.offsets.try_read_sync(63).is_some());
5507 assert!(reader.offsets.try_read_sync(64).is_none());
5508
5509 assert_eq!(reader.try_read_many_sync(&[0, 63]), vec![None, None]);
5510 assert!(matches!(
5511 reader.read_many(&[0, 63]).await,
5512 Err(Error::Corruption(_))
5513 ));
5514 drop(reader);
5515
5516 journal.destroy().await.unwrap();
5517 });
5518 }
5519
5520 #[test_traced]
5521 fn test_variable_read_many_batches_cold_misses() {
5522 let executor = deterministic::Runner::default();
5525 executor.start(|context| async move {
5526 let (context, recordings) = RecordingContext::new(context);
5527 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16));
5528 let cfg = Config {
5529 partition: "read-many-batches".into(),
5530 items_per_section: NZU64!(5),
5531 compression: None,
5532 codec_config: (),
5533 page_cache: page_cache.clone(),
5534 write_buffer: NZUsize!(1024),
5535 replay_buffer: NZUsize!(1024),
5536 };
5537 let appended = (0..4)
5538 .map(|i| FixedBytes::new([i as u8; 300]))
5539 .collect::<Vec<_>>();
5540 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5541 .await
5542 .unwrap();
5543 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5544 journal = journal.sync().await.unwrap();
5545 let reader;
5546 (journal, reader) = journal.snapshot().await.unwrap();
5547
5548 page_cache.clear();
5552 reader.offsets.read_many_inner(&[0, 1, 3]).await.unwrap();
5553 recordings.clear();
5554 assert_eq!(
5555 reader.read_many(&[0, 3]).await.unwrap(),
5556 vec![appended[0].clone(), appended[3].clone()]
5557 );
5558 let snapshot = recordings.snapshot();
5559 assert_eq!(snapshot.batches, vec![2]);
5560 assert_eq!(snapshot.reads.len(), 2);
5561 drop(reader);
5562
5563 journal.destroy().await.unwrap();
5564 });
5565 }
5566
5567 #[test_traced]
5568 fn test_variable_read_many_consecutive_after_reopen() {
5569 let executor = deterministic::Runner::default();
5570 executor.start(|context| async move {
5571 let cfg = Config {
5572 partition: "read-many-consecutive-after-reopen".into(),
5573 items_per_section: NZU64!(20),
5574 compression: None,
5575 codec_config: (),
5576 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
5577 write_buffer: NZUsize!(1024),
5578 replay_buffer: NZUsize!(1024),
5579 };
5580
5581 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5582 .await
5583 .unwrap();
5584 for i in 0..20u64 {
5585 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5586 }
5587 let journal = journal.sync().await.unwrap();
5588 drop(journal);
5589
5590 let cfg = Config {
5591 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
5592 ..cfg
5593 };
5594 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
5595 .await
5596 .unwrap();
5597 let reader;
5598 (journal, reader) = journal.snapshot().await.unwrap();
5599 let positions: Vec<u64> = (3..10).collect();
5600 let items = reader.read_many(&positions).await.unwrap();
5601 assert_eq!(items, vec![300, 400, 500, 600, 700, 800, 900]);
5602 drop(reader);
5603
5604 journal.destroy().await.unwrap();
5605 });
5606 }
5607
5608 #[test_traced]
5609 fn test_variable_read_many_scattered_single_runs_across_blobs() {
5610 let executor = deterministic::Runner::default();
5616 executor.start(|context| async move {
5617 let cfg = Config {
5618 partition: "read-many-scattered-runs".into(),
5619 items_per_section: NZU64!(5),
5620 compression: None,
5621 codec_config: (),
5622 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16)),
5623 write_buffer: NZUsize!(1024),
5624 replay_buffer: NZUsize!(1024),
5625 };
5626
5627 let items = (0..30)
5631 .map(|i| FixedBytes::new([i as u8; 300]))
5632 .collect::<Vec<_>>();
5633 let mut journal =
5634 Journal::<_, FixedBytes<300>>::init(context.child("first"), cfg.clone())
5635 .await
5636 .unwrap();
5637 for item in &items {
5638 (journal, _) = journal.append(item).await.unwrap();
5639 }
5640 let journal = journal.sync().await.unwrap();
5641 drop(journal);
5642
5643 let cfg = Config {
5645 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(16)),
5646 ..cfg
5647 };
5648 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("second"), cfg)
5649 .await
5650 .unwrap();
5651 let reader;
5652 (journal, reader) = journal.snapshot().await.unwrap();
5653
5654 reader.read(6).await.unwrap();
5657 reader.read(16).await.unwrap();
5658
5659 for hit in [5, 6, 15, 17] {
5663 assert!(reader.try_read_sync(hit).is_some(), "position {hit}");
5664 }
5665 let misses = [0, 3, 10, 12, 20, 21, 23];
5666 for miss in misses {
5667 assert!(reader.try_read_sync(miss).is_none(), "position {miss}");
5668 }
5669
5670 let positions = [0, 3, 5, 6, 10, 12, 15, 17, 20, 21, 23];
5673 let expected: Vec<_> = positions
5674 .iter()
5675 .map(|&p| items[p as usize].clone())
5676 .collect();
5677 let before = context.encode();
5678 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
5679
5680 let after = context.encode();
5682 assert_eq!(
5683 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
5684 4
5685 );
5686 assert_eq!(
5687 counter(&after, "second_cache_misses") - counter(&before, "second_cache_misses"),
5688 misses.len() as u64
5689 );
5690
5691 let before = context.encode();
5693 assert_eq!(reader.read_many(&positions).await.unwrap(), expected);
5694 let after = context.encode();
5695 assert_eq!(
5696 counter(&after, "second_cache_hits") - counter(&before, "second_cache_hits"),
5697 positions.len() as u64
5698 );
5699 assert_eq!(
5700 counter(&after, "second_cache_misses"),
5701 counter(&before, "second_cache_misses")
5702 );
5703 drop(reader);
5704
5705 journal.destroy().await.unwrap();
5706 });
5707 }
5708
5709 #[test_traced]
5710 fn test_variable_read_many_cold_offsets_across_blobs() {
5711 let executor = deterministic::Runner::default();
5717 executor.start(|context| async move {
5718 let page_cache = CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(256));
5719 let cfg = Config {
5720 partition: "read-many-cold-offsets".into(),
5721 items_per_section: NZU64!(128),
5722 compression: None,
5723 codec_config: (),
5724 page_cache: page_cache.clone(),
5725 write_buffer: NZUsize!(1024),
5726 replay_buffer: NZUsize!(1024),
5727 };
5728 let appended = (0..258u64)
5729 .map(|p| {
5730 let mut bytes = [p as u8; 300];
5731 bytes[..8].copy_from_slice(&p.to_be_bytes());
5732 FixedBytes::new(bytes)
5733 })
5734 .collect::<Vec<_>>();
5735 let mut journal = Journal::<_, FixedBytes<300>>::init(context.child("j"), cfg)
5736 .await
5737 .unwrap();
5738 (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
5739 let reader;
5740 (journal, reader) = journal.snapshot().await.unwrap();
5741 let positions = [0, 1, 3, 62, 63, 64, 65, 126, 127, 128, 129, 255, 256, 257];
5742 let expected: Vec<_> = positions
5743 .iter()
5744 .map(|&p| appended[p as usize].clone())
5745 .collect();
5746
5747 page_cache.clear();
5749 let (items, misses) = reader.probe_parts(&positions);
5750 assert!(items.iter().all(Option::is_none));
5751 let miss = |p: u64| misses.iter().find(|miss| miss.position == p).unwrap();
5752 assert!(miss(0).offset.is_none() && miss(0).end.is_none());
5753 assert!(miss(255).offset.is_none() && miss(255).end.is_none());
5754 assert!(miss(257).offset.is_some() && miss(257).end.is_none());
5755 assert_eq!(complete(&reader, items, misses).await.unwrap(), expected);
5756
5757 page_cache.clear();
5761 reader.offsets.read(64).await.unwrap();
5762 let (items, misses) = reader.probe_parts(&positions);
5763 assert!(items.iter().all(Option::is_none));
5764 let miss = |p: u64| misses.iter().find(|miss| miss.position == p).unwrap();
5765 assert!(miss(63).offset.is_none() && miss(63).end.is_some());
5766 assert!(miss(64).offset.is_some() && miss(64).end.is_some());
5767 assert!(miss(127).offset.is_some() && miss(127).end.is_none());
5768 assert_eq!(complete(&reader, items, misses).await.unwrap(), expected);
5769 drop(reader);
5770
5771 journal.destroy().await.unwrap();
5772 });
5773 }
5774
5775 #[test_traced]
5781 fn test_variable_offsets_partition_loss_after_prune_unrecoverable() {
5782 let executor = deterministic::Runner::default();
5783 executor.start(|context| async move {
5784 let cfg = Config {
5785 partition: "offsets-loss-after-prune".into(),
5786 items_per_section: NZU64!(10),
5787 compression: None,
5788 codec_config: (),
5789 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
5790 write_buffer: NZUsize!(1024),
5791 replay_buffer: NZUsize!(1024),
5792 };
5793
5794 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5795 .await
5796 .unwrap();
5797
5798 for i in 0..40u64 {
5800 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5801 }
5802
5803 let (journal, _) = journal.prune(20).await.unwrap();
5805 let bounds = journal.bounds();
5806 assert_eq!(bounds.start, 20);
5807 assert_eq!(bounds.end, 40);
5808
5809 let journal = journal.sync().await.unwrap();
5810 drop(journal);
5811
5812 context
5814 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
5815 .await
5816 .expect("Failed to remove offsets blobs partition");
5817 context
5818 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
5819 .await
5820 .expect("Failed to remove offsets metadata partition");
5821
5822 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
5825 assert!(matches!(result, Err(Error::Corruption(_))));
5826 });
5827 }
5828
5829 #[test_traced]
5837 fn test_variable_align_data_offsets_mismatch() {
5838 let executor = deterministic::Runner::default();
5839 executor.start(|context| async move {
5840 let cfg = Config {
5841 partition: "data-loss-test".into(),
5842 items_per_section: NZU64!(10),
5843 compression: None,
5844 codec_config: (),
5845 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
5846 write_buffer: NZUsize!(1024),
5847 replay_buffer: NZUsize!(1024),
5848 };
5849
5850 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
5851 .await
5852 .unwrap();
5853
5854 for i in 0..20u64 {
5856 (variable, _) = variable.append(&(i * 100)).await.unwrap();
5857 }
5858
5859 let variable = variable.sync().await.unwrap();
5860 drop(variable);
5861
5862 context
5864 .remove(&cfg.data_partition(), None)
5865 .await
5866 .expect("Failed to remove data partition");
5867
5868 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
5871 .await
5872 .expect("Should align offsets to match empty data");
5873
5874 assert_eq!(journal.size(), 20);
5876
5877 assert!(journal.bounds().is_empty());
5879
5880 for i in 0..20 {
5882 assert!(matches!(
5883 journal.read(i).await,
5884 Err(crate::journal::Error::ItemPruned(_))
5885 ));
5886 }
5887
5888 let pos;
5890 (journal, pos) = journal.append(&999).await.unwrap();
5891 assert_eq!(pos, 20);
5892 assert_eq!(journal.read(20).await.unwrap(), 999);
5893
5894 journal.destroy().await.unwrap();
5895 });
5896 }
5897
5898 #[test_traced]
5901 fn test_variable_replay_range() {
5902 let executor = deterministic::Runner::default();
5903 executor.start(|context| async move {
5904 let cfg = Config {
5905 partition: "replay-range".into(),
5906 items_per_section: NZU64!(10),
5907 compression: None,
5908 codec_config: (),
5909 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
5910 write_buffer: NZUsize!(1024),
5911 replay_buffer: NZUsize!(1024),
5912 };
5913 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
5914
5915 for i in 0..40u64 {
5917 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5918 }
5919
5920 {
5922 let stream = replay_items(&journal, 7..25, NZUsize!(20), ReadOptions::default())
5923 .await
5924 .unwrap();
5925 futures::pin_mut!(stream);
5926 for i in 7..25u64 {
5927 let (pos, item) = stream.next().await.unwrap().unwrap();
5928 assert_eq!(pos, i);
5929 assert_eq!(item, i * 100);
5930 }
5931 assert!(stream.next().await.is_none());
5932 }
5933
5934 {
5936 let stream = replay_items(&journal, 5..5, NZUsize!(20), ReadOptions::default())
5937 .await
5938 .unwrap();
5939 futures::pin_mut!(stream);
5940 assert!(stream.next().await.is_none());
5941 }
5942
5943 let res = replay_items(&journal, 0..41, NZUsize!(20), ReadOptions::default())
5945 .await
5946 .map(|_| ());
5947 assert!(matches!(
5948 res,
5949 Err(crate::journal::Error::ItemOutOfRange(41))
5950 ));
5951
5952 #[allow(clippy::reversed_empty_ranges)]
5954 let res = replay_items(&journal, 10..5, NZUsize!(20), ReadOptions::default())
5955 .await
5956 .map(|_| ());
5957 assert!(matches!(
5958 res,
5959 Err(crate::journal::Error::ItemOutOfRange(10))
5960 ));
5961
5962 journal.destroy().await.unwrap();
5963 });
5964 }
5965
5966 #[test_traced]
5968 fn test_variable_replay() {
5969 let executor = deterministic::Runner::default();
5970 executor.start(|context| async move {
5971 let cfg = Config {
5972 partition: "replay".into(),
5973 items_per_section: NZU64!(10),
5974 compression: None,
5975 codec_config: (),
5976 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
5977 write_buffer: NZUsize!(1024),
5978 replay_buffer: NZUsize!(1024),
5979 };
5980
5981 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
5983
5984 for i in 0..40u64 {
5986 (journal, _) = journal.append(&(i * 100)).await.unwrap();
5987 }
5988
5989 {
5991 let reader;
5992 (journal, reader) = journal.snapshot().await.unwrap();
5993 let stream = replay_items(
5994 &reader,
5995 0..reader.bounds().end,
5996 NZUsize!(20),
5997 ReadOptions::default(),
5998 )
5999 .await
6000 .unwrap();
6001 futures::pin_mut!(stream);
6002 for i in 0..40u64 {
6003 let (pos, item) = stream.next().await.unwrap().unwrap();
6004 assert_eq!(pos, i);
6005 assert_eq!(item, i * 100);
6006 }
6007 assert!(stream.next().await.is_none());
6008 }
6009
6010 {
6012 let reader;
6013 (journal, reader) = journal.snapshot().await.unwrap();
6014 let stream = replay_items(
6015 &reader,
6016 15..reader.bounds().end,
6017 NZUsize!(20),
6018 ReadOptions::default(),
6019 )
6020 .await
6021 .unwrap();
6022 futures::pin_mut!(stream);
6023 for i in 15..40u64 {
6024 let (pos, item) = stream.next().await.unwrap().unwrap();
6025 assert_eq!(pos, i);
6026 assert_eq!(item, i * 100);
6027 }
6028 assert!(stream.next().await.is_none());
6029 }
6030
6031 {
6033 let reader;
6034 (journal, reader) = journal.snapshot().await.unwrap();
6035 let stream = replay_items(
6036 &reader,
6037 20..reader.bounds().end,
6038 NZUsize!(20),
6039 ReadOptions::default(),
6040 )
6041 .await
6042 .unwrap();
6043 futures::pin_mut!(stream);
6044 for i in 20..40u64 {
6045 let (pos, item) = stream.next().await.unwrap().unwrap();
6046 assert_eq!(pos, i);
6047 assert_eq!(item, i * 100);
6048 }
6049 assert!(stream.next().await.is_none());
6050 }
6051
6052 (journal, _) = journal.prune(20).await.unwrap();
6054 {
6055 let reader;
6056 (journal, reader) = journal.snapshot().await.unwrap();
6057 let res = replay_items(
6058 &reader,
6059 0..reader.bounds().end,
6060 NZUsize!(20),
6061 ReadOptions::default(),
6062 )
6063 .await;
6064 assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_))));
6065 }
6066 {
6067 let reader;
6068 (journal, reader) = journal.snapshot().await.unwrap();
6069 let res = replay_items(
6070 &reader,
6071 19..reader.bounds().end,
6072 NZUsize!(20),
6073 ReadOptions::default(),
6074 )
6075 .await;
6076 assert!(matches!(res, Err(crate::journal::Error::ItemPruned(_))));
6077 }
6078
6079 {
6081 let reader;
6082 (journal, reader) = journal.snapshot().await.unwrap();
6083 let stream = replay_items(
6084 &reader,
6085 20..reader.bounds().end,
6086 NZUsize!(20),
6087 ReadOptions::default(),
6088 )
6089 .await
6090 .unwrap();
6091 futures::pin_mut!(stream);
6092 for i in 20..40u64 {
6093 let (pos, item) = stream.next().await.unwrap().unwrap();
6094 assert_eq!(pos, i);
6095 assert_eq!(item, i * 100);
6096 }
6097 assert!(stream.next().await.is_none());
6098 }
6099
6100 {
6102 let reader;
6103 (journal, reader) = journal.snapshot().await.unwrap();
6104 let stream = replay_items(
6105 &reader,
6106 40..reader.bounds().end,
6107 NZUsize!(20),
6108 ReadOptions::default(),
6109 )
6110 .await
6111 .unwrap();
6112 futures::pin_mut!(stream);
6113 assert!(stream.next().await.is_none());
6114 }
6115
6116 {
6118 let reader;
6119 (journal, reader) = journal.snapshot().await.unwrap();
6120 let res = replay_items(
6121 &reader,
6122 41..reader.bounds().end,
6123 NZUsize!(20),
6124 ReadOptions::default(),
6125 )
6126 .await;
6127 assert!(matches!(
6128 res,
6129 Err(crate::journal::Error::ItemOutOfRange(41))
6130 ));
6131 }
6132
6133 journal.destroy().await.unwrap();
6134 });
6135 }
6136
6137 #[test_traced]
6138 fn test_variable_replay_stops_after_error() {
6139 let executor = deterministic::Runner::default();
6140 executor.start(|context| async move {
6141 let cfg = Config {
6142 partition: "replay-stops-after-error".into(),
6143 items_per_section: NZU64!(10),
6144 compression: None,
6145 codec_config: (),
6146 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6147 write_buffer: NZUsize!(1024),
6148 replay_buffer: NZUsize!(1024),
6149 };
6150
6151 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
6152 .await
6153 .unwrap();
6154 for i in 0..30u64 {
6155 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6156 }
6157 let journal = journal.sync().await.unwrap();
6158 drop(journal);
6159
6160 let (blob, _) = context
6161 .open(&cfg.data_partition(), &1u64.to_be_bytes())
6162 .await
6163 .unwrap();
6164 blob.write_at(0, vec![0xFF; 1], WriteOptions::SYNC)
6165 .await
6166 .unwrap();
6167
6168 drop(blob);
6169 {
6170 let cache = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10));
6171 let mut writers = Vec::new();
6172 for blob_index in 0..3u64 {
6173 let (blob, size) = context
6174 .open(&cfg.data_partition(), &blob_index.to_be_bytes())
6175 .await
6176 .unwrap();
6177 writers.push(
6178 Writer::new(blob, size, cfg.write_buffer.get(), cache.clone())
6179 .await
6180 .unwrap(),
6181 );
6182 }
6183
6184 let mut states = Vec::new();
6185 for (blob_index, writer) in writers.iter().enumerate() {
6186 let blob = blob_index as u64;
6187 states.push(ReplayState::<_, u64> {
6188 blob,
6189 replay: Blob::Writer(writer)
6190 .replay_from(0, NZUsize!(1024), ReadOptions::default())
6191 .unwrap(),
6192 budget: 1024,
6193 pos: blob * 10,
6194 end_pos: (blob + 1) * 10,
6195 offset: 0,
6196 codec_config: (),
6197 compressed: false,
6198 _marker: PhantomData,
6199 });
6200 }
6201
6202 let stream = crate::journal::contiguous::replay_stream_from_states(states);
6205 futures::pin_mut!(stream);
6206
6207 let mut next = 0u64;
6208 while next < 10 {
6209 let batch = stream.next().await.unwrap().unwrap();
6210 assert_eq!(batch.start, next);
6211 assert!(!batch.items.is_empty());
6212 next = batch.start + batch.items.len() as u64;
6213 for (position, item) in batch {
6214 assert_eq!(item, position * 100);
6215 }
6216 }
6217 assert_eq!(next, 10);
6218 assert!(matches!(
6219 stream.next().await.unwrap(),
6220 Err(Error::Corruption(_))
6221 ));
6222 assert!(stream.next().await.is_none());
6223 }
6224 });
6225 }
6226
6227 #[test_traced]
6228 fn test_variable_contiguous() {
6229 let executor = deterministic::Runner::default();
6230 executor.start(|context| async move {
6231 run_contiguous_tests(move |test_name: String, idx: usize, cap: Option<u64>| {
6232 let label = test_name.replace('-', "_");
6233 let context = context
6234 .child("test")
6235 .with_attribute("name", &label)
6236 .with_attribute("index", idx);
6237 async move {
6238 let cfg = Config {
6239 partition: format!("generic-test-{test_name}"),
6240 items_per_section: NZU64!(10),
6241 compression: None,
6242 codec_config: (),
6243 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6244 write_buffer: NZUsize!(1024),
6245 replay_buffer: NZUsize!(1024),
6246 };
6247 match cap {
6248 Some(size) => Journal::<_, u64>::init_at_most(context, cfg, size).await,
6249 None => Journal::<_, u64>::init(context, cfg).await,
6250 }
6251 }
6252 .boxed()
6253 })
6254 .await;
6255 });
6256 }
6257
6258 #[test_traced]
6261 fn test_variable_prune_durability_survives_crash() {
6262 fn cfg() -> Config<()> {
6263 Config {
6264 partition: "variable-prune-durability".into(),
6265 items_per_section: NZU64!(3),
6266 compression: None,
6267 codec_config: (),
6268 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6269 write_buffer: NZUsize!(1024),
6270 replay_buffer: NZUsize!(1024),
6271 }
6272 }
6273
6274 let executor = deterministic::Runner::default();
6275 let (_, checkpoint) = executor.start_and_recover(|context| async move {
6276 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg())
6277 .await
6278 .unwrap();
6279
6280 for i in 0..8u64 {
6284 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6285 }
6286 let (journal, pruned) = journal.prune(3).await.unwrap();
6287 assert!(pruned);
6288 drop(journal);
6289 });
6290
6291 deterministic::Runner::from(checkpoint).start(|context| async move {
6292 let journal = Journal::<_, u64>::init(context.child("recover"), cfg())
6293 .await
6294 .unwrap();
6295 assert_eq!(
6296 journal.bounds(),
6297 3..8,
6298 "pruned journal lost acknowledged items"
6299 );
6300 for i in 3..8u64 {
6301 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6302 }
6303 journal.destroy().await.unwrap();
6304 });
6305 }
6306
6307 #[test_traced]
6309 fn test_variable_multiple_sequential_prunes() {
6310 let executor = deterministic::Runner::default();
6311 executor.start(|context| async move {
6312 let cfg = Config {
6313 partition: "sequential-prunes".into(),
6314 items_per_section: NZU64!(10),
6315 compression: None,
6316 codec_config: (),
6317 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6318 write_buffer: NZUsize!(1024),
6319 replay_buffer: NZUsize!(1024),
6320 };
6321
6322 let mut journal = Journal::<_, u64>::init(context, cfg).await.unwrap();
6323
6324 for i in 0..40u64 {
6326 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6327 }
6328
6329 let bounds = journal.bounds();
6331 assert_eq!(bounds.start, 0);
6332 assert_eq!(bounds.end, 40);
6333
6334 let pruned;
6336 (journal, pruned) = journal.prune(10).await.unwrap();
6337 assert!(pruned);
6338
6339 assert_eq!(journal.bounds().start, 10);
6341
6342 assert!(matches!(
6344 journal.read(0).await,
6345 Err(crate::journal::Error::ItemPruned(_))
6346 ));
6347 assert_eq!(journal.read(10).await.unwrap(), 1000);
6348 assert_eq!(journal.read(19).await.unwrap(), 1900);
6349
6350 let pruned;
6352 (journal, pruned) = journal.prune(20).await.unwrap();
6353 assert!(pruned);
6354
6355 assert_eq!(journal.bounds().start, 20);
6357
6358 assert!(matches!(
6360 journal.read(10).await,
6361 Err(crate::journal::Error::ItemPruned(_))
6362 ));
6363 assert!(matches!(
6364 journal.read(19).await,
6365 Err(crate::journal::Error::ItemPruned(_))
6366 ));
6367 assert_eq!(journal.read(20).await.unwrap(), 2000);
6368 assert_eq!(journal.read(29).await.unwrap(), 2900);
6369
6370 let pruned;
6372 (journal, pruned) = journal.prune(30).await.unwrap();
6373 assert!(pruned);
6374
6375 assert_eq!(journal.bounds().start, 30);
6377
6378 assert!(matches!(
6380 journal.read(20).await,
6381 Err(crate::journal::Error::ItemPruned(_))
6382 ));
6383 assert!(matches!(
6384 journal.read(29).await,
6385 Err(crate::journal::Error::ItemPruned(_))
6386 ));
6387 assert_eq!(journal.read(30).await.unwrap(), 3000);
6388 assert_eq!(journal.read(39).await.unwrap(), 3900);
6389
6390 assert_eq!(journal.size(), 40);
6392
6393 journal.destroy().await.unwrap();
6394 });
6395 }
6396
6397 #[test_traced]
6399 fn test_variable_prune_all_then_reinit() {
6400 let executor = deterministic::Runner::default();
6401 executor.start(|context| async move {
6402 let cfg = Config {
6403 partition: "prune-all-reinit".into(),
6404 items_per_section: NZU64!(10),
6405 compression: None,
6406 codec_config: (),
6407 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6408 write_buffer: NZUsize!(1024),
6409 replay_buffer: NZUsize!(1024),
6410 };
6411
6412 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6413 .await
6414 .unwrap();
6415
6416 for i in 0..100u64 {
6418 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6419 }
6420
6421 let bounds = journal.bounds();
6422 assert_eq!(bounds.end, 100);
6423 assert_eq!(bounds.start, 0);
6424
6425 let pruned;
6427 (journal, pruned) = journal.prune(100).await.unwrap();
6428 assert!(pruned);
6429
6430 let bounds = journal.bounds();
6432 assert_eq!(bounds.end, 100);
6433 assert!(bounds.is_empty());
6434
6435 for i in 0..100 {
6437 assert!(matches!(
6438 journal.read(i).await,
6439 Err(crate::journal::Error::ItemPruned(_))
6440 ));
6441 }
6442
6443 journal.sync().await.unwrap();
6445 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6446 .await
6447 .unwrap();
6448
6449 let bounds = journal.bounds();
6451 assert_eq!(bounds.end, 100);
6452 assert!(bounds.is_empty());
6453
6454 for i in 0..100 {
6456 assert!(matches!(
6457 journal.read(i).await,
6458 Err(crate::journal::Error::ItemPruned(_))
6459 ));
6460 }
6461
6462 (journal, _) = journal.append(&10000).await.unwrap();
6464 let bounds = journal.bounds();
6465 assert_eq!(bounds.end, 101);
6466 assert_eq!(bounds.start, 100);
6468
6469 assert_eq!(journal.read(100).await.unwrap(), 10000);
6471
6472 assert!(matches!(
6474 journal.read(99).await,
6475 Err(crate::journal::Error::ItemPruned(_))
6476 ));
6477
6478 journal.destroy().await.unwrap();
6479 });
6480 }
6481
6482 #[test_traced]
6484 fn test_variable_recovery_prune_crash_offsets_behind() {
6485 let executor = deterministic::Runner::default();
6486 executor.start(|context| async move {
6487 let cfg = Config {
6488 partition: "recovery-prune-crash".into(),
6489 items_per_section: NZU64!(10),
6490 compression: None,
6491 codec_config: (),
6492 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6493 write_buffer: NZUsize!(1024),
6494 replay_buffer: NZUsize!(1024),
6495 };
6496
6497 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6498 .await
6499 .unwrap();
6500
6501 for i in 0..40u64 {
6503 (variable, _) = variable.append(&(i * 100)).await.unwrap();
6504 }
6505
6506 let (mut variable, _) = variable.prune(10).await.unwrap();
6508 assert_eq!(variable.bounds().start, 10);
6509
6510 (variable, _) = variable.test_prune_data(2).await.unwrap();
6512 variable.sync().await.unwrap();
6513
6514 let (recovery_context, recordings) = RecordingContext::new(context.child("second"));
6516 let variable = Journal::<_, u64>::init(recovery_context, cfg.clone())
6517 .await
6518 .unwrap();
6519
6520 let reads = recordings.snapshot().reads;
6523 assert!(reads.len() > 2);
6524 let (metadata_reads, recovery_reads) = reads.split_at(2);
6525 assert_eq!(metadata_reads, [ReadOptions::DONT_CACHE; 2]);
6526
6527 assert!(
6530 recovery_reads
6531 .iter()
6532 .all(|options| *options == ReadOptions::default())
6533 );
6534
6535 let bounds = variable.bounds();
6537 assert_eq!(bounds.start, 20);
6538 assert_eq!(bounds.end, 40);
6539
6540 assert!(matches!(
6542 variable.read(10).await,
6543 Err(crate::journal::Error::ItemPruned(_))
6544 ));
6545
6546 assert_eq!(variable.read(20).await.unwrap(), 2000);
6548 assert_eq!(variable.read(39).await.unwrap(), 3900);
6549
6550 variable.destroy().await.unwrap();
6551 });
6552 }
6553
6554 #[test_traced]
6557 fn test_variable_recovery_prune_crash_offsets_end_behind() {
6558 let executor = deterministic::Runner::default();
6559 executor.start(|context| async move {
6560 let cfg = Config {
6561 partition: "recovery-prune-offsets-end-behind".into(),
6562 items_per_section: NZU64!(10),
6563 compression: None,
6564 codec_config: (),
6565 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6566 write_buffer: NZUsize!(1024),
6567 replay_buffer: NZUsize!(1024),
6568 };
6569
6570 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6571 .await
6572 .unwrap();
6573
6574 for i in 0..7u64 {
6577 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6578 }
6579 let mut journal = journal.sync().await.unwrap();
6580 for i in 7..12u64 {
6581 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6582 }
6583
6584 journal.0.halt_before_offsets_prune = true;
6588 {
6589 let fut = journal.prune(10);
6590 futures::pin_mut!(fut);
6591 assert!(
6592 futures::poll!(fut.as_mut()).is_pending(),
6593 "prune must park before offsets.prune"
6594 );
6595 }
6596
6597 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6598 .await
6599 .expect("prune crash must leave a recoverable journal");
6600 assert_eq!(journal.bounds(), 10..12);
6601 for i in 10..12u64 {
6602 assert_eq!(journal.read(i).await.unwrap(), i * 100);
6603 }
6604 journal.destroy().await.unwrap();
6605 });
6606 }
6607
6608 #[test_traced]
6613 fn test_variable_recovery_offsets_ahead_corruption() {
6614 let executor = deterministic::Runner::default();
6615 executor.start(|context| async move {
6616 let cfg = Config {
6617 partition: "recovery-offsets-ahead".into(),
6618 items_per_section: NZU64!(10),
6619 compression: None,
6620 codec_config: (),
6621 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6622 write_buffer: NZUsize!(1024),
6623 replay_buffer: NZUsize!(1024),
6624 };
6625
6626 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6627 .await
6628 .unwrap();
6629
6630 for i in 0..40u64 {
6632 (variable, _) = variable.append(&(i * 100)).await.unwrap();
6633 }
6634
6635 let (mut variable, _) = variable.test_prune_offsets(20).await.unwrap();
6637 (variable, _) = variable.test_prune_data(1).await.unwrap();
6638
6639 let variable = variable.sync().await.unwrap();
6640 drop(variable);
6641
6642 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
6644 assert!(matches!(result, Err(Error::Corruption(_))));
6645 });
6646 }
6647
6648 #[test_traced]
6651 fn test_variable_recovery_offsets_empty_different_blob_is_corruption() {
6652 let executor = deterministic::Runner::default();
6653 executor.start(|context| async move {
6654 let cfg = Config {
6655 partition: "offsets-empty-diff-blob".into(),
6656 items_per_section: NZU64!(10),
6657 compression: None,
6658 codec_config: (),
6659 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6660 write_buffer: NZUsize!(1024),
6661 replay_buffer: NZUsize!(1024),
6662 };
6663
6664 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6665 .await
6666 .unwrap();
6667
6668 for i in 0..15u64 {
6669 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6670 }
6671 let mut journal = journal.sync().await.unwrap();
6672
6673 journal.0.offsets = journal.0.offsets.clear_to_size(20).await.unwrap();
6676 drop(journal);
6677
6678 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
6679 assert!(matches!(result, Err(Error::Corruption(_))));
6680 });
6681 }
6682
6683 #[test_traced]
6686 fn test_variable_recovery_offsets_end_behind_data_oldest_is_corruption() {
6687 let executor = deterministic::Runner::default();
6688 executor.start(|context| async move {
6689 let cfg = Config {
6690 partition: "offsets-end-behind-data-oldest".into(),
6691 items_per_section: NZU64!(10),
6692 compression: None,
6693 codec_config: (),
6694 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6695 write_buffer: NZUsize!(1024),
6696 replay_buffer: NZUsize!(1024),
6697 };
6698
6699 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6700 .await
6701 .unwrap();
6702
6703 for i in 0..15u64 {
6704 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6705 }
6706 let mut journal = journal.sync().await.unwrap();
6707
6708 (journal, _) = journal.test_prune_data(1).await.unwrap();
6711 let journal = journal.test_truncate_offsets(5).await.unwrap();
6712 drop(journal);
6713
6714 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
6715 assert!(matches!(result, Err(Error::Corruption(_))));
6716 });
6717 }
6718
6719 #[test_traced]
6722 fn test_variable_bounded_recovery_offsets_end_behind_data_oldest_is_corruption() {
6723 let executor = deterministic::Runner::default();
6724 executor.start(|context| async move {
6725 let cfg = Config {
6726 partition: "bounded-offsets-end-behind-data-oldest".into(),
6727 items_per_section: NZU64!(10),
6728 compression: None,
6729 codec_config: (),
6730 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6731 write_buffer: NZUsize!(1024),
6732 replay_buffer: NZUsize!(1024),
6733 };
6734
6735 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6736 .await
6737 .unwrap();
6738 for i in 0..15u64 {
6739 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6740 }
6741 let mut journal = journal.sync().await.unwrap();
6742
6743 (journal, _) = journal.test_prune_data(1).await.unwrap();
6746 let journal = journal.test_truncate_offsets(5).await.unwrap();
6747 drop(journal);
6748
6749 let result = Journal::<_, u64>::init_at_most(context.child("second"), cfg, 10).await;
6750 assert!(matches!(result, Err(Error::Corruption(_))), "{result:?}");
6751 });
6752 }
6753
6754 #[test_traced]
6757 fn test_variable_recovery_offsets_start_mid_blob_ahead_of_data() {
6758 let executor = deterministic::Runner::default();
6759 executor.start(|context| async move {
6760 let cfg = Config {
6761 partition: "offsets-mid-blob-ahead".into(),
6762 items_per_section: NZU64!(10),
6763 compression: None,
6764 codec_config: (),
6765 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6766 write_buffer: NZUsize!(1024),
6767 replay_buffer: NZUsize!(1024),
6768 };
6769
6770 let mut journal =
6774 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
6775 .await
6776 .unwrap();
6777 for i in 0..5u64 {
6778 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6779 }
6780 journal.sync().await.unwrap();
6781
6782 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6783 .await
6784 .unwrap();
6785 assert_eq!(journal.bounds(), 7..12);
6786 assert_eq!(journal.read(7).await.unwrap(), 0);
6787 assert_eq!(journal.read(11).await.unwrap(), 400);
6788 journal.destroy().await.unwrap();
6789 });
6790 }
6791
6792 #[test_traced]
6796 fn test_variable_recovery_watermark_below_offsets_start() {
6797 let executor = deterministic::Runner::default();
6798 executor.start(|context| async move {
6799 let cfg = Config {
6800 partition: "watermark-below-start".into(),
6801 items_per_section: NZU64!(10),
6802 compression: None,
6803 codec_config: (),
6804 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6805 write_buffer: NZUsize!(1024),
6806 replay_buffer: NZUsize!(1024),
6807 };
6808
6809 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6810 .await
6811 .unwrap();
6812 for i in 0..25u64 {
6813 (journal, _) = journal.append(&(i * 100)).await.unwrap();
6814 }
6815 let journal = journal.sync().await.unwrap();
6816
6817 let (journal, _) = journal.prune(10).await.unwrap();
6819 let journal = journal
6820 .test_set_offsets_recovery_watermark(5)
6821 .await
6822 .unwrap();
6823 drop(journal);
6824
6825 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6827 .await
6828 .unwrap();
6829 assert_eq!(journal.bounds(), 10..25);
6830 assert_eq!(journal.read(10).await.unwrap(), 1000);
6831 assert_eq!(journal.read(24).await.unwrap(), 2400);
6832 journal.destroy().await.unwrap();
6833 });
6834 }
6835
6836 #[test_traced]
6838 fn test_variable_recovery_append_crash_offsets_behind() {
6839 let executor = deterministic::Runner::default();
6840 executor.start(|context| async move {
6841 let cfg = Config {
6842 partition: "recovery-append-crash".into(),
6843 items_per_section: NZU64!(10),
6844 compression: None,
6845 codec_config: (),
6846 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6847 write_buffer: NZUsize!(1024),
6848 replay_buffer: NZUsize!(1024),
6849 };
6850
6851 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6852 .await
6853 .unwrap();
6854
6855 for i in 0..15u64 {
6857 (variable, _) = variable.append(&(i * 100)).await.unwrap();
6858 }
6859
6860 assert_eq!(variable.size(), 15);
6861
6862 for i in 15..20u64 {
6864 (variable, _, _) = variable.test_append_data(1, i * 100).await.unwrap();
6865 }
6866 variable.sync().await.unwrap();
6870 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
6871 .await
6872 .unwrap();
6873
6874 let bounds = variable.bounds();
6876 assert_eq!(bounds.end, 20);
6877 assert_eq!(bounds.start, 0);
6878
6879 for i in 0..20u64 {
6881 assert_eq!(variable.read(i).await.unwrap(), i * 100);
6882 }
6883
6884 assert_eq!(variable.test_offsets_size(), 20);
6886
6887 variable.destroy().await.unwrap();
6888 });
6889 }
6890
6891 #[test_traced]
6892 fn test_variable_recovery_rejects_overlong_data_blob() {
6893 let executor = deterministic::Runner::default();
6894 executor.start(|context| async move {
6895 let cfg = Config {
6896 partition: "recovery-overlong-data-blob".into(),
6897 items_per_section: NZU64!(10),
6898 compression: None,
6899 codec_config: (),
6900 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6901 write_buffer: NZUsize!(1024),
6902 replay_buffer: NZUsize!(1024),
6903 };
6904
6905 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6906 .await
6907 .unwrap();
6908
6909 for i in 0..11u64 {
6910 (journal, _, _) = journal.test_append_data(0, i * 100).await.unwrap();
6911 }
6912 let (blobs, sync) = journal.0.blobs.start_sync().await.unwrap();
6913 journal.0.blobs = blobs;
6914 sync.await.unwrap();
6915 drop(journal);
6916
6917 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
6918 assert!(matches!(result, Err(Error::Corruption(_))));
6919 });
6920 }
6921
6922 #[test_traced]
6927 fn test_variable_recovery_rejects_over_capacity_non_newest_blob() {
6928 let executor = deterministic::Runner::default();
6929 executor.start(|context| async move {
6930 let cfg = Config {
6931 partition: "recovery-over-capacity-non-newest".into(),
6932 items_per_section: NZU64!(10),
6933 compression: None,
6934 codec_config: (),
6935 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6936 write_buffer: NZUsize!(1024),
6937 replay_buffer: NZUsize!(1024),
6938 };
6939
6940 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
6941 .await
6942 .unwrap();
6943
6944 for i in 0..11u64 {
6946 (journal, _, _) = journal.test_append_data(0, i * 100).await.unwrap();
6947 }
6948 let (blobs, sync) = journal.0.blobs.start_sync().await.unwrap();
6951 journal.0.blobs = blobs;
6952 sync.await.unwrap();
6953 (journal, _, _) = journal.test_append_data(1, 9999).await.unwrap();
6954 drop(journal);
6956
6957 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
6958 assert!(matches!(result, Err(Error::Corruption(_))));
6959 });
6960 }
6961
6962 #[test_traced]
6963 fn test_variable_recovery_preserves_rolled_predecessors_without_commit() {
6964 let executor = deterministic::Runner::default();
6965 executor.start(|context| async move {
6966 let cfg = Config::<()> {
6967 partition: "recovery-empty-data-tail".into(),
6968 items_per_section: NZU64!(1),
6969 compression: None,
6970 codec_config: (),
6971 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
6972 write_buffer: NZUsize!(1024),
6973 replay_buffer: NZUsize!(1024),
6974 };
6975 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
6976 .await
6977 .unwrap();
6978
6979 let appended;
6983 (journal, appended) = journal.append(&10).await.unwrap();
6984 assert_eq!(appended, 0);
6985 journal = journal.sync().await.unwrap();
6986 let appended;
6987 (journal, appended) = journal.append(&20).await.unwrap();
6988 assert_eq!(appended, 1);
6989 let appended;
6990 (journal, appended) = journal.append(&30).await.unwrap();
6991 assert_eq!(appended, 2);
6992 drop(journal);
6993
6994 let data_partition = cfg.data_partition();
6995 let mut data_blobs = context.scan(&data_partition).await.unwrap();
6996 data_blobs.sort();
6997 assert_eq!(data_blobs.len(), 4);
6998 for name in &data_blobs[..3] {
6999 let (_, size) = context.open(&data_partition, name).await.unwrap();
7000 assert!(size > 0);
7001 }
7002 let (_, size) = context.open(&data_partition, &data_blobs[3]).await.unwrap();
7003 assert_eq!(size, 0);
7004
7005 let cfg = Config {
7007 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7008 ..cfg
7009 };
7010 let mut journal = Journal::<_, u64>::init(context.child("recovered"), cfg.clone())
7011 .await
7012 .unwrap();
7013 assert_eq!(journal.bounds(), 0..3);
7014 assert_eq!(journal.read(0).await.unwrap(), 10);
7015 assert_eq!(journal.read(1).await.unwrap(), 20);
7016 assert_eq!(journal.read(2).await.unwrap(), 30);
7017 let appended;
7018 (journal, appended) = journal.append(&42).await.unwrap();
7019 assert_eq!(appended, 3);
7020 assert_eq!(journal.read(3).await.unwrap(), 42);
7021 drop(journal);
7022
7023 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
7025 assert_eq!(data_blobs.len(), 5);
7026
7027 let journal = Journal::<_, u64>::init(context.child("recovered"), cfg)
7028 .await
7029 .unwrap();
7030 journal.destroy().await.unwrap();
7031 });
7032 }
7033
7034 #[test_traced]
7035 fn test_variable_recovery_preserves_first_rollover_without_commit() {
7036 let executor = deterministic::Runner::default();
7037 executor.start(|context| async move {
7038 let cfg = Config::<()> {
7039 partition: "recovery-empty-data-no-items".into(),
7040 items_per_section: NZU64!(1),
7041 compression: None,
7042 codec_config: (),
7043 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7044 write_buffer: NZUsize!(1024),
7045 replay_buffer: NZUsize!(1024),
7046 };
7047 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
7048 .await
7049 .unwrap();
7050
7051 let appended;
7054 (journal, appended) = journal.append(&10).await.unwrap();
7055 assert_eq!(appended, 0);
7056 let appended;
7057 (journal, appended) = journal.append(&20).await.unwrap();
7058 assert_eq!(appended, 1);
7059 drop(journal);
7060
7061 let data_partition = cfg.data_partition();
7062 let mut data_blobs = context.scan(&data_partition).await.unwrap();
7063 data_blobs.sort();
7064 assert_eq!(data_blobs.len(), 3);
7065 for name in &data_blobs[..2] {
7066 let (_, size) = context.open(&data_partition, name).await.unwrap();
7067 assert!(size > 0);
7068 }
7069 let (_, size) = context.open(&data_partition, &data_blobs[2]).await.unwrap();
7070 assert_eq!(size, 0);
7071
7072 let cfg = Config {
7073 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7074 ..cfg
7075 };
7076 let mut journal = Journal::<_, u64>::init(context.child("recovered"), cfg)
7077 .await
7078 .unwrap();
7079 assert_eq!(journal.bounds(), 0..2);
7080 let appended;
7081 (journal, appended) = journal.append(&42).await.unwrap();
7082 assert_eq!(appended, 2);
7083 assert_eq!(journal.read(2).await.unwrap(), 42);
7084 journal.destroy().await.unwrap();
7085 });
7086 }
7087
7088 #[test_traced]
7092 fn test_variable_recovery_truncates_torn_interior_page() {
7093 let executor = deterministic::Runner::default();
7094 executor.start(|context| async move {
7095 let cfg = Config::<()> {
7096 partition: "variable-torn-interior".into(),
7097 items_per_section: NZU64!(30),
7098 compression: None,
7099 codec_config: (),
7100 page_cache: CacheRef::new(NZU16!(64), NZUsize!(10)),
7101 write_buffer: NZUsize!(2048),
7102 replay_buffer: NZUsize!(2048),
7103 };
7104 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7105 .await
7106 .unwrap();
7107 for i in 0..33u64 {
7108 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7109 }
7110 journal.commit().await.unwrap();
7111
7112 corrupt_page(&context, &cfg.data_partition(), &0u64.to_be_bytes(), 2, 64).await;
7114
7115 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
7118 .await
7119 .unwrap();
7120 assert_eq!(journal.bounds(), 0..14);
7121 for i in 0..14u64 {
7122 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7123 }
7124 let appended;
7125 (journal, appended) = journal.append(&4242).await.unwrap();
7126 assert_eq!(appended, 14);
7127 journal.destroy().await.unwrap();
7128 });
7129 }
7130
7131 #[test_traced]
7134 fn test_variable_recovery_truncates_torn_interior_page_in_tail() {
7135 let executor = deterministic::Runner::default();
7136 executor.start(|context| async move {
7137 let cfg = Config::<()> {
7138 partition: "variable-torn-interior-tail".into(),
7139 items_per_section: NZU64!(30),
7140 compression: None,
7141 codec_config: (),
7142 page_cache: CacheRef::new(NZU16!(64), NZUsize!(10)),
7143 write_buffer: NZUsize!(2048),
7144 replay_buffer: NZUsize!(2048),
7145 };
7146 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7147 .await
7148 .unwrap();
7149 for i in 0..50u64 {
7150 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7151 }
7152 journal.commit().await.unwrap();
7153
7154 corrupt_page(&context, &cfg.data_partition(), &1u64.to_be_bytes(), 1, 64).await;
7156
7157 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg)
7159 .await
7160 .unwrap();
7161 assert_eq!(journal.bounds(), 0..37);
7162 for i in 0..37u64 {
7163 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7164 }
7165 let appended;
7166 (journal, appended) = journal.append(&4242).await.unwrap();
7167 assert_eq!(appended, 37);
7168 journal.destroy().await.unwrap();
7169 });
7170 }
7171
7172 fn torn_rewrite_cfg(partition: &str) -> Config<()> {
7176 Config {
7177 partition: partition.into(),
7178 items_per_section: NZU64!(64),
7179 compression: None,
7180 codec_config: (),
7181 page_cache: CacheRef::new(NZU16!(100), NZUsize!(3)),
7182 write_buffer: NZUsize!(2048),
7183 replay_buffer: NZUsize!(2048),
7184 }
7185 }
7186
7187 async fn torn_rewrite_journal(
7190 context: &deterministic::Context,
7191 cfg: &Config<()>,
7192 ) -> Journal<deterministic::Context, FixedBytes<32>> {
7193 let mut journal = Journal::<_, FixedBytes<32>>::init(context.child("seed"), cfg.clone())
7194 .await
7195 .unwrap();
7196 for i in 0..33u64 {
7197 (journal, _) = journal
7198 .append(&FixedBytes::new([i as u8; 32]))
7199 .await
7200 .unwrap();
7201 }
7202 journal = journal.sync().await.unwrap();
7203 for i in 33..38u64 {
7204 (journal, _) = journal
7205 .append(&FixedBytes::new([i as u8; 32]))
7206 .await
7207 .unwrap();
7208 }
7209 let (journal, handle) = journal.start_sync().await.unwrap();
7210 handle.await.unwrap();
7211 journal
7212 }
7213
7214 #[test_traced]
7217 fn test_variable_bounded_recovery_reads_torn_rewrite_before_publish() {
7218 let executor = deterministic::Runner::default();
7219 executor.start(|context| async move {
7220 let cfg = torn_rewrite_cfg("variable-torn-rewrite-read");
7221 let journal = torn_rewrite_journal(&context, &cfg).await;
7222 drop(journal);
7223
7224 tear_page_rewrite(
7226 &context,
7227 &cfg.data_partition(),
7228 &0u64.to_be_bytes(),
7229 10,
7230 95,
7231 100,
7232 )
7233 .await;
7234
7235 let recovery =
7236 Recovery::<_, FixedBytes<32>>::open(context.child("capped"), cfg.clone(), Some(33))
7237 .await
7238 .unwrap();
7239 assert_eq!(recovery.bounds(), 0..33);
7240 assert_eq!(
7241 recovery.read(32).await.unwrap(),
7242 FixedBytes::new([32u8; 32])
7243 );
7244 assert_eq!(recovery.last_matching(33, |_| true).await.unwrap(), 33);
7245 let journal = recovery.finish(33).await.unwrap();
7246 assert_eq!(journal.bounds(), 0..33);
7247 assert_eq!(journal.read(32).await.unwrap(), FixedBytes::new([32u8; 32]));
7248 journal.destroy().await.unwrap();
7249 });
7250 }
7251
7252 #[test_traced]
7256 fn test_variable_bounded_open_repairs_torn_rewrites() {
7257 let executor = deterministic::Runner::default();
7258 executor.start(|context| async move {
7259 let cfg = torn_rewrite_cfg("variable-torn-rewrites");
7260 let journal = torn_rewrite_journal(&context, &cfg).await;
7261 drop(journal);
7262
7263 tear_page_rewrite(
7266 &context,
7267 &cfg.data_partition(),
7268 &0u64.to_be_bytes(),
7269 10,
7270 95,
7271 100,
7272 )
7273 .await;
7274 tear_page_rewrite(
7275 &context,
7276 &format!("{}-blobs", cfg.offsets_config().partition),
7277 &0u64.to_be_bytes(),
7278 2,
7279 70,
7280 100,
7281 )
7282 .await;
7283
7284 let journal =
7285 Journal::<_, FixedBytes<32>>::init_at_most(context.child("capped"), cfg, 33)
7286 .await
7287 .unwrap();
7288 assert_eq!(journal.bounds(), 0..33);
7289 for i in 0..33u64 {
7290 assert_eq!(
7291 journal.read(i).await.unwrap(),
7292 FixedBytes::new([i as u8; 32])
7293 );
7294 }
7295 journal.destroy().await.unwrap();
7296 });
7297 }
7298
7299 #[test_traced]
7304 fn test_variable_recovery_adopts_torn_page_below_watermark() {
7305 let executor = deterministic::Runner::default();
7306 executor.start(|context| async move {
7307 let cfg = Config::<()> {
7308 partition: "variable-torn-below-watermark".into(),
7309 items_per_section: NZU64!(30),
7310 compression: None,
7311 codec_config: (),
7312 page_cache: CacheRef::new(NZU16!(64), NZUsize!(10)),
7313 write_buffer: NZUsize!(2048),
7314 replay_buffer: NZUsize!(2048),
7315 };
7316 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7317 .await
7318 .unwrap();
7319 for i in 0..33u64 {
7320 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7321 }
7322 journal.sync().await.unwrap();
7324
7325 corrupt_page(&context, &cfg.data_partition(), &0u64.to_be_bytes(), 2, 64).await;
7327 let size_before = context
7328 .logical_blob(&cfg.data_partition(), &0u64.to_be_bytes())
7329 .unwrap()
7330 .len();
7331
7332 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7336 .await
7337 .expect("acknowledged damage must not fail recovery");
7338 let size_after = context
7339 .logical_blob(&cfg.data_partition(), &0u64.to_be_bytes())
7340 .unwrap()
7341 .len();
7342 assert_eq!(
7343 size_after, size_before,
7344 "adoption must preserve the evidence"
7345 );
7346 let mut damaged = 0;
7347 for i in 0..30u64 {
7348 match journal.read(i).await {
7349 Ok(item) => assert_eq!(item, i * 100),
7350 Err(_) => damaged += 1,
7351 }
7352 }
7353 assert!(damaged > 0, "the torn page must surface as read errors");
7354 for i in 30..33u64 {
7355 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7356 }
7357 drop(journal);
7358
7359 let _ = Journal::<_, u64>::init(context.child("third"), cfg.clone())
7361 .await
7362 .unwrap();
7363 let size_retry = context
7364 .logical_blob(&cfg.data_partition(), &0u64.to_be_bytes())
7365 .unwrap()
7366 .len();
7367 assert_eq!(size_retry, size_before);
7368 });
7369 }
7370
7371 #[test_traced]
7376 fn test_variable_recovery_adopts_shortened_blob_below_watermark() {
7377 let executor = deterministic::Runner::default();
7378 executor.start(|context| async move {
7379 let cfg = Config::<()> {
7380 partition: "variable-short-below-watermark".into(),
7381 items_per_section: NZU64!(30),
7382 compression: None,
7383 codec_config: (),
7384 page_cache: CacheRef::new(NZU16!(64), NZUsize!(10)),
7385 write_buffer: NZUsize!(2048),
7386 replay_buffer: NZUsize!(2048),
7387 };
7388 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7389 .await
7390 .unwrap();
7391 for i in 0..33u64 {
7392 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7393 }
7394 journal.sync().await.unwrap();
7395
7396 let physical_page_size = 64 + 12;
7399 let (blob, size) = context
7400 .open(&cfg.data_partition(), &0u64.to_be_bytes())
7401 .await
7402 .unwrap();
7403 assert!(size > 2 * physical_page_size);
7404 blob.resize(2 * physical_page_size).await.unwrap();
7405 blob.sync().await.unwrap();
7406
7407 drop(blob);
7408 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
7409 .await
7410 .unwrap();
7411 assert_eq!(journal.size(), 33);
7412 for i in 0..14u64 {
7413 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7414 }
7415 assert!(journal.read(20).await.is_err());
7416 for i in 30..33u64 {
7417 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7418 }
7419 });
7420 }
7421
7422 #[test_traced]
7426 fn test_variable_recovery_adopts_shortened_old_blob_below_watermark() {
7427 let executor = deterministic::Runner::default();
7428 executor.start(|context| async move {
7429 let cfg = Config::<()> {
7430 partition: "variable-short-old-below-watermark".into(),
7431 items_per_section: NZU64!(10),
7432 compression: None,
7433 codec_config: (),
7434 page_cache: CacheRef::new(NZU16!(64), NZUsize!(10)),
7435 write_buffer: NZUsize!(2048),
7436 replay_buffer: NZUsize!(2048),
7437 };
7438 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7439 .await
7440 .unwrap();
7441 for i in 0..25u64 {
7442 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7443 }
7444 journal.sync().await.unwrap();
7445
7446 let physical_page_size = 64 + 12;
7450 let (blob, size) = context
7451 .open(&cfg.data_partition(), &0u64.to_be_bytes())
7452 .await
7453 .unwrap();
7454 assert!(size > physical_page_size);
7455 blob.resize(physical_page_size).await.unwrap();
7456 blob.sync().await.unwrap();
7457
7458 drop(blob);
7459 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
7460 .await
7461 .unwrap();
7462 assert_eq!(journal.size(), 25);
7463 for i in 0..7u64 {
7464 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7465 }
7466 assert!(journal.read(8).await.is_err());
7467 for i in 10..25u64 {
7468 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7469 }
7470 });
7471 }
7472
7473 #[test_traced]
7476 fn test_variable_recovery_rejects_torn_page_below_mid_blob_watermark() {
7477 let executor = deterministic::Runner::default();
7478 executor.start(|context| async move {
7479 let cfg = Config::<()> {
7480 partition: "variable-torn-mid-blob-watermark".into(),
7481 items_per_section: NZU64!(30),
7482 compression: None,
7483 codec_config: (),
7484 page_cache: CacheRef::new(NZU16!(64), NZUsize!(10)),
7485 write_buffer: NZUsize!(2048),
7486 replay_buffer: NZUsize!(2048),
7487 };
7488 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7489 .await
7490 .unwrap();
7491 for i in 0..20u64 {
7492 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7493 }
7494 journal.sync().await.unwrap();
7496
7497 corrupt_page(&context, &cfg.data_partition(), &0u64.to_be_bytes(), 1, 64).await;
7499 let (_, size_before) = context
7500 .open(&cfg.data_partition(), &0u64.to_be_bytes())
7501 .await
7502 .unwrap();
7503
7504 for child in ["second", "retry"] {
7505 let result = Journal::<_, u64>::init(context.child(child), cfg.clone()).await;
7506 assert!(matches!(result, Err(Error::Corruption(_))));
7507 }
7508
7509 let (_, size_after) = context
7511 .open(&cfg.data_partition(), &0u64.to_be_bytes())
7512 .await
7513 .unwrap();
7514 assert_eq!(size_after, size_before);
7515 });
7516 }
7517
7518 #[test_traced]
7526 fn test_variable_recovery_rolls_back_durable_blob_after_gap() {
7527 let executor = deterministic::Runner::default();
7528 executor.start(|context| async move {
7529 let cfg = Config {
7530 partition: "recovery-rollback-after-gap".into(),
7531 items_per_section: NZU64!(10),
7532 compression: None,
7533 codec_config: (),
7534 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7535 write_buffer: NZUsize!(1024),
7536 replay_buffer: NZUsize!(1024),
7537 };
7538
7539 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7540 .await
7541 .unwrap();
7542
7543 for i in 0..10u64 {
7545 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7546 }
7547 journal = journal.sync().await.unwrap();
7548
7549 for i in 10..30u64 {
7552 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7553 }
7554 journal = journal.test_sync_data_blob(2).await.unwrap();
7555 drop(journal);
7556 let data_partition = cfg.data_partition();
7557 let (blob, _) = context
7558 .open(&data_partition, &1u64.to_be_bytes())
7559 .await
7560 .unwrap();
7561 blob.resize(0).await.unwrap();
7562 blob.sync().await.unwrap();
7563 drop(blob);
7564
7565 let mut names = context.scan(&data_partition).await.unwrap();
7568 names.sort();
7569 assert_eq!(names.len(), 4);
7570 let sizes = {
7571 let mut sizes = Vec::new();
7572 for name in &names {
7573 let (_, size) = context.open(&data_partition, name).await.unwrap();
7574 sizes.push(size);
7575 }
7576 sizes
7577 };
7578 assert!(sizes[0] > 0, "blob 0 should be durable");
7579 assert_eq!(sizes[1], 0, "blob 1 should be the gap");
7580 assert!(sizes[2] > 0, "blob 2 should be incidentally durable");
7581 assert_eq!(sizes[3], 0, "blob 3 should be the empty tail");
7582
7583 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7586 .await
7587 .unwrap();
7588 assert_eq!(journal.bounds(), 0..10);
7589 for i in 0..10u64 {
7590 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7591 }
7592 assert!(matches!(
7593 journal.read(10).await,
7594 Err(Error::ItemOutOfRange(10))
7595 ));
7596
7597 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
7600 assert_eq!(data_blobs.len(), 2);
7601
7602 let appended;
7604 (journal, appended) = journal.append(&1234).await.unwrap();
7605 assert_eq!(appended, 10);
7606 assert_eq!(journal.read(10).await.unwrap(), 1234);
7607
7608 journal.destroy().await.unwrap();
7609 });
7610 }
7611
7612 #[test_traced]
7622 fn test_variable_recovery_empty_oldest_blob_orphaned_newer_blob() {
7623 let executor = deterministic::Runner::default();
7624 executor.start(|context| async move {
7625 let cfg = Config {
7626 partition: "recovery-empty-oldest-blob".into(),
7627 items_per_section: NZU64!(10),
7628 compression: None,
7629 codec_config: (),
7630 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7631 write_buffer: NZUsize!(1024),
7632 replay_buffer: NZUsize!(1024),
7633 };
7634
7635 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7637 .await
7638 .unwrap();
7639 for i in 0..20u64 {
7640 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7641 }
7642 let journal = journal.sync().await.unwrap();
7643 drop(journal);
7644
7645 let data_partition = cfg.data_partition();
7648 let mut names = context.scan(&data_partition).await.unwrap();
7649 names.sort();
7650 assert_eq!(names.len(), 3);
7651 let (blob0, size0) = context.open(&data_partition, &names[0]).await.unwrap();
7652 assert!(size0 > 0, "blob 0 should start durable");
7653 blob0.resize(0).await.unwrap();
7654 blob0.sync().await.unwrap();
7655 context
7656 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
7657 .await
7658 .unwrap();
7659 context
7660 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
7661 .await
7662 .unwrap();
7663
7664 drop(blob0);
7666 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7667 .await
7668 .unwrap();
7669 assert_eq!(journal.bounds(), 0..0);
7670 assert!(matches!(
7671 journal.read(0).await,
7672 Err(Error::ItemOutOfRange(0))
7673 ));
7674
7675 let appended;
7677 (journal, appended) = journal.append(&42).await.unwrap();
7678 assert_eq!(appended, 0);
7679 assert_eq!(journal.read(0).await.unwrap(), 42);
7680 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
7681 assert_eq!(
7682 data_blobs.len(),
7683 1,
7684 "orphaned newer blob should be truncated away"
7685 );
7686
7687 journal.destroy().await.unwrap();
7688 });
7689 }
7690
7691 #[test_traced]
7697 fn test_variable_recovery_clean_short_oldest_blob_orphaned_newer_blob() {
7698 let executor = deterministic::Runner::default();
7699 executor.start(|context| async move {
7700 let cfg = Config {
7701 partition: "recovery-clean-short-oldest-blob".into(),
7702 items_per_section: NZU64!(64),
7703 compression: None,
7704 codec_config: (),
7705 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7706 write_buffer: NZUsize!(1024),
7707 replay_buffer: NZUsize!(1024),
7708 };
7709
7710 let mut journal =
7713 Journal::<_, FixedBytes<31>>::init(context.child("first"), cfg.clone())
7714 .await
7715 .unwrap();
7716 for i in 0..128u8 {
7717 (journal, _) = journal.append(&FixedBytes::new([i; 31])).await.unwrap();
7718 }
7719 let journal = journal.sync().await.unwrap();
7720 drop(journal);
7721
7722 let physical_page_size = LARGE_PAGE_SIZE.get() as u64 + 12;
7723 let items_in_page = LARGE_PAGE_SIZE.get() as u64 / 32;
7724 assert!(items_in_page < cfg.items_per_section.get());
7725
7726 let data_partition = cfg.data_partition();
7727 let mut names = context.scan(&data_partition).await.unwrap();
7728 names.sort();
7729 assert_eq!(names.len(), 3);
7730
7731 let (blob0, size0) = context.open(&data_partition, &names[0]).await.unwrap();
7734 assert!(size0 > physical_page_size);
7735 blob0.resize(physical_page_size).await.unwrap();
7736 blob0.sync().await.unwrap();
7737
7738 context
7741 .remove(&format!("{}-blobs", cfg.offsets_partition()), None)
7742 .await
7743 .unwrap();
7744 context
7745 .remove(&format!("{}-metadata", cfg.offsets_partition()), None)
7746 .await
7747 .unwrap();
7748
7749 drop(blob0);
7752 let mut journal =
7753 Journal::<_, FixedBytes<31>>::init(context.child("second"), cfg.clone())
7754 .await
7755 .unwrap();
7756 assert_eq!(journal.bounds(), 0..items_in_page);
7757 assert_eq!(
7758 journal.read(items_in_page - 1).await.unwrap(),
7759 FixedBytes::new([(items_in_page - 1) as u8; 31])
7760 );
7761 assert!(matches!(
7762 journal.read(items_in_page).await,
7763 Err(Error::ItemOutOfRange(pos)) if pos == items_in_page
7764 ));
7765
7766 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
7767 assert_eq!(
7768 data_blobs.len(),
7769 1,
7770 "orphaned newer blob should be truncated away"
7771 );
7772
7773 let pos;
7775 (journal, pos) = journal.append(&FixedBytes::new([42; 31])).await.unwrap();
7776 assert_eq!(pos, items_in_page);
7777 assert_eq!(
7778 journal.read(items_in_page).await.unwrap(),
7779 FixedBytes::new([42; 31])
7780 );
7781
7782 journal.destroy().await.unwrap();
7783 });
7784 }
7785
7786 #[test_traced]
7790 fn test_variable_recovery_unsynced_tail_keeps_contiguous_prefix() {
7791 let executor = deterministic::Runner::default();
7792 executor.start(|context| async move {
7793 let cfg = Config {
7794 partition: "recovery-partial-sync-loop".into(),
7795 items_per_section: NZU64!(10),
7796 compression: None,
7797 codec_config: (),
7798 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7799 write_buffer: NZUsize!(1024),
7800 replay_buffer: NZUsize!(1024),
7801 };
7802
7803 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7804 .await
7805 .unwrap();
7806
7807 for i in 0..25u64 {
7810 (journal, _) = journal.append(&(i * 100)).await.unwrap();
7811 }
7812
7813 journal = journal.test_sync_data_blob(0).await.unwrap();
7816 journal = journal.test_sync_data_blob(1).await.unwrap();
7817 drop(journal);
7818
7819 let data_partition = cfg.data_partition();
7822 let mut names = context.scan(&data_partition).await.unwrap();
7823 names.sort();
7824 assert_eq!(names.len(), 3);
7825 for (blob, name) in names.iter().enumerate() {
7826 let (_, size) = context.open(&data_partition, name).await.unwrap();
7827 if blob < 2 {
7828 assert!(size > 0, "blob {blob} should be durable");
7829 } else {
7830 assert_eq!(size, 0, "blob {blob} should be empty");
7831 }
7832 }
7833
7834 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7837 .await
7838 .unwrap();
7839 assert_eq!(journal.bounds(), 0..20);
7840 for i in 0..20u64 {
7841 assert_eq!(journal.read(i).await.unwrap(), i * 100);
7842 }
7843 assert!(matches!(
7844 journal.read(20).await,
7845 Err(Error::ItemOutOfRange(20))
7846 ));
7847
7848 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
7851 assert_eq!(data_blobs.len(), 3);
7852 let appended;
7853 (journal, appended) = journal.append(&2000).await.unwrap();
7854 assert_eq!(appended, 20);
7855 assert_eq!(journal.read(20).await.unwrap(), 2000);
7856
7857 journal.destroy().await.unwrap();
7858 });
7859 }
7860
7861 #[test_traced]
7863 fn test_variable_recovery_multiple_prunes_crash() {
7864 let executor = deterministic::Runner::default();
7865 executor.start(|context| async move {
7866 let cfg = Config {
7867 partition: "recovery-multiple-prunes".into(),
7868 items_per_section: NZU64!(10),
7869 compression: None,
7870 codec_config: (),
7871 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7872 write_buffer: NZUsize!(1024),
7873 replay_buffer: NZUsize!(1024),
7874 };
7875
7876 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7877 .await
7878 .unwrap();
7879
7880 for i in 0..50u64 {
7882 (variable, _) = variable.append(&(i * 100)).await.unwrap();
7883 }
7884
7885 let (mut variable, _) = variable.prune(10).await.unwrap();
7887 assert_eq!(variable.bounds().start, 10);
7888
7889 (variable, _) = variable.test_prune_data(3).await.unwrap();
7891 variable.sync().await.unwrap();
7892
7893 let variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7895 .await
7896 .unwrap();
7897
7898 let bounds = variable.bounds();
7900 assert_eq!(bounds.start, 30);
7901 assert_eq!(bounds.end, 50);
7902
7903 assert!(matches!(
7905 variable.read(10).await,
7906 Err(crate::journal::Error::ItemPruned(_))
7907 ));
7908 assert!(matches!(
7909 variable.read(20).await,
7910 Err(crate::journal::Error::ItemPruned(_))
7911 ));
7912
7913 assert_eq!(variable.read(30).await.unwrap(), 3000);
7915 assert_eq!(variable.read(49).await.unwrap(), 4900);
7916
7917 variable.destroy().await.unwrap();
7918 });
7919 }
7920
7921 #[test_traced]
7927 fn test_variable_recovery_offsets_behind_data_multi_blob() {
7928 let executor = deterministic::Runner::default();
7929 executor.start(|context| async move {
7930 let cfg = Config {
7931 partition: "recovery-rewind-crash".into(),
7932 items_per_section: NZU64!(10),
7933 compression: None,
7934 codec_config: (),
7935 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7936 write_buffer: NZUsize!(1024),
7937 replay_buffer: NZUsize!(1024),
7938 };
7939
7940 let mut variable = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7941 .await
7942 .unwrap();
7943
7944 for i in 0..25u64 {
7946 (variable, _) = variable.append(&(i * 100)).await.unwrap();
7947 }
7948
7949 assert_eq!(variable.size(), 25);
7950
7951 let variable = variable.test_truncate_offsets(5).await.unwrap();
7953
7954 variable.sync().await.unwrap();
7956 let mut variable = Journal::<_, u64>::init(context.child("second"), cfg.clone())
7957 .await
7958 .unwrap();
7959
7960 let bounds = variable.bounds();
7962 assert_eq!(bounds.end, 25);
7963 assert_eq!(bounds.start, 0);
7964
7965 for i in 0..25u64 {
7967 assert_eq!(variable.read(i).await.unwrap(), i * 100);
7968 }
7969
7970 assert_eq!(variable.test_offsets_size(), 25);
7972
7973 let pos;
7975 (variable, pos) = variable.append(&2500).await.unwrap();
7976 assert_eq!(pos, 25);
7977 assert_eq!(variable.read(25).await.unwrap(), 2500);
7978
7979 variable.destroy().await.unwrap();
7980 });
7981 }
7982
7983 #[test_traced]
7984 fn test_variable_recovery_rejects_watermark_beyond_retained_data() {
7985 let executor = deterministic::Runner::default();
7986 executor.start(|context| async move {
7987 let cfg = Config {
7988 partition: "recovery-anchor-too-far".into(),
7989 items_per_section: NZU64!(10),
7990 compression: None,
7991 codec_config: (),
7992 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
7993 write_buffer: NZUsize!(1024),
7994 replay_buffer: NZUsize!(1024),
7995 };
7996
7997 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
7998 .await
7999 .unwrap();
8000
8001 for i in 0..20u64 {
8002 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8003 }
8004 let journal = journal.sync().await.unwrap();
8005
8006 let journal = journal
8009 .test_set_offsets_recovery_watermark(15)
8010 .await
8011 .unwrap();
8012 journal.test_truncate_data(12).await.unwrap();
8013
8014 for child in ["second", "retry"] {
8016 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
8017 Err(Error::Corruption(message)) => assert!(message.starts_with(
8018 "offsets recovery watermark 15 exceeds retained data end 12 ("
8019 )),
8020 Err(error) => panic!("unexpected error: {error}"),
8021 Ok(_) => panic!("missing acknowledged data was accepted"),
8022 }
8023 }
8024 });
8025 }
8026
8027 #[test_traced]
8028 fn test_variable_recovery_rejects_missing_data_below_watermark() {
8029 let executor = deterministic::Runner::default();
8030 executor.start(|context| async move {
8031 let cfg = Config {
8032 partition: "recovery-short-middle-retry".into(),
8033 items_per_section: NZU64!(10),
8034 compression: None,
8035 codec_config: (),
8036 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8037 write_buffer: NZUsize!(1024),
8038 replay_buffer: NZUsize!(1024),
8039 };
8040
8041 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8042 .await
8043 .unwrap();
8044
8045 for i in 0..30u64 {
8046 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8047 }
8048 let journal = journal.sync().await.unwrap();
8049
8050 let journal = journal
8053 .test_set_offsets_recovery_watermark(15)
8054 .await
8055 .unwrap();
8056 let partition = journal.test_truncate_data(12).await.unwrap();
8057 let mut orphan = partition.open_recovery(2).await.unwrap();
8058 let mut encoded = Vec::new();
8059 encode_frame_into(None, &9999u64, &mut encoded).unwrap();
8060 (orphan, _) = orphan.append(&encoded).await.unwrap();
8061 orphan = orphan.sync().await.unwrap();
8062 drop(orphan);
8063
8064 for child in ["second", "retry"] {
8068 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
8069 Err(Error::Corruption(message)) => assert_eq!(
8070 message,
8071 "data blobs shorter than offsets recovery watermark 15"
8072 ),
8073 Err(error) => panic!("unexpected error: {error}"),
8074 Ok(_) => panic!("missing acknowledged data was accepted"),
8075 }
8076 }
8077
8078 let data_blobs = context.scan(&cfg.data_partition()).await.unwrap();
8079 assert_eq!(
8080 data_blobs.len(),
8081 3,
8082 "corruption evidence should not be removed"
8083 );
8084 });
8085 }
8086
8087 #[test_traced]
8091 fn test_variable_init_at_most_truncation_survives_crash() {
8092 const PAGE_SIZE: NonZeroU16 = NZU16!(36);
8097 const BOUND: u64 = 8;
8098
8099 fn cfg() -> Config<()> {
8100 Config {
8101 partition: "variable-init-at-most-truncation-crash".into(),
8102 items_per_section: NZU64!(100),
8103 compression: None,
8104 codec_config: (),
8105 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(3)),
8106 write_buffer: NZUsize!(1),
8107 replay_buffer: NZUsize!(2048),
8108 }
8109 }
8110 let appended: Vec<u64> = (100..109).collect();
8111
8112 let executor = deterministic::Runner::default();
8113 let (appended, checkpoint) = executor.start_and_recover(|context| async move {
8114 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg())
8115 .await
8116 .unwrap();
8117 for value in 0..24u64 {
8118 (journal, _) = journal.append(&value).await.unwrap();
8119 }
8120 let journal = journal.sync().await.unwrap();
8121 drop(journal);
8122
8123 *context.storage_fault_config().write() = deterministic::FaultConfig {
8125 write_rate: Some(deterministic::WriteConfig {
8126 failure_rate: probability!(0.0),
8127 retention_rate: probability!(1.0),
8128 mode: deterministic::PartialWriteMode::Prefix,
8129 }),
8130 resize_rate: Some(deterministic::ResizeConfig {
8131 failure_rate: probability!(0.0),
8132 partial_rate: probability!(0.0),
8133 }),
8134 ..Default::default()
8135 };
8136 let journal = Journal::<_, u64>::init_at_most(context.child("cap"), cfg(), BOUND)
8137 .await
8138 .unwrap();
8139 assert_eq!(journal.bounds(), 0..BOUND);
8140 let (journal, _) = journal.append_many(Many::Flat(&appended)).await.unwrap();
8141 drop(journal);
8142 appended
8143 });
8144
8145 deterministic::Runner::from(checkpoint).start(|context| async move {
8146 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
8147 let journal = Journal::<_, u64>::init(context.child("recover"), cfg())
8148 .await
8149 .unwrap();
8150 let bounds = journal.bounds();
8151 assert_eq!(bounds.start, 0);
8152 assert!(
8153 (BOUND..=BOUND + appended.len() as u64).contains(&bounds.end),
8154 "recovered size {} is not a prefix of the new history",
8155 bounds.end
8156 );
8157 for pos in 0..BOUND {
8158 assert_eq!(journal.read(pos).await.unwrap(), pos);
8159 }
8160 for pos in BOUND..bounds.end {
8161 assert_eq!(
8162 journal.read(pos).await.unwrap(),
8163 appended[(pos - BOUND) as usize],
8164 "content at retained position {pos} was never written there"
8165 );
8166 }
8167 journal.destroy().await.unwrap();
8168 });
8169 }
8170
8171 #[test_traced]
8172 fn test_variable_init_at_most_commit_reopen() {
8173 let executor = deterministic::Runner::default();
8174 executor.start(|context| async move {
8175 let cfg = Config {
8176 partition: "rewind-commit-reopen".into(),
8177 items_per_section: NZU64!(10),
8178 compression: None,
8179 codec_config: (),
8180 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8181 write_buffer: NZUsize!(1024),
8182 replay_buffer: NZUsize!(1024),
8183 };
8184
8185 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8186 .await
8187 .unwrap();
8188
8189 for i in 0..25u64 {
8190 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8191 }
8192 let journal = journal.sync().await.unwrap();
8193
8194 let journal = {
8195 _ = journal.sync().await.unwrap();
8196 Journal::<_, u64>::init_at_most(context.child("cap"), cfg.clone(), 12).await
8197 }
8198 .unwrap();
8199 journal.commit().await.unwrap();
8200
8201 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8202 .await
8203 .unwrap();
8204 assert_eq!(journal.bounds(), 0..12);
8205 for i in 0..12u64 {
8206 assert_eq!(journal.read(i).await.unwrap(), i * 100);
8207 }
8208 assert!(matches!(
8209 journal.read(12).await,
8210 Err(Error::ItemOutOfRange(12))
8211 ));
8212
8213 journal.destroy().await.unwrap();
8214 });
8215 }
8216
8217 #[test_traced]
8218 fn test_variable_recovery_rejects_synced_data_truncate_to_boundary() {
8219 let executor = deterministic::Runner::default();
8220 executor.start(|context| async move {
8221 let cfg = Config {
8222 partition: "recovery-boundary-data-rewind".into(),
8223 items_per_section: NZU64!(10),
8224 compression: None,
8225 codec_config: (),
8226 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8227 write_buffer: NZUsize!(1024),
8228 replay_buffer: NZUsize!(1024),
8229 };
8230
8231 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8232 .await
8233 .unwrap();
8234
8235 for i in 0..20u64 {
8236 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8237 }
8238 let journal = journal.sync().await.unwrap();
8239 journal.test_truncate_data(10).await.unwrap();
8240
8241 for child in ["second", "retry"] {
8246 match Journal::<_, u64>::init(context.child(child), cfg.clone()).await {
8247 Err(Error::Corruption(message)) => assert_eq!(
8248 message,
8249 "offsets recovery watermark 20 exceeds retained data end 10 (offsets \
8250 bounds 0..20)"
8251 ),
8252 Err(error) => panic!("unexpected error: {error}"),
8253 Ok(_) => panic!("missing acknowledged data was accepted"),
8254 }
8255 }
8256 });
8257 }
8258
8259 #[test_traced]
8260 fn test_variable_recovery_truncates_short_data_blob_after_anchor() {
8261 let executor = deterministic::Runner::default();
8262 executor.start(|context| async move {
8263 let cfg = Config {
8264 partition: "recovery-short-blob-after-anchor".into(),
8265 items_per_section: NZU64!(10),
8266 compression: None,
8267 codec_config: (),
8268 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8269 write_buffer: NZUsize!(1024),
8270 replay_buffer: NZUsize!(1024),
8271 };
8272
8273 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8274 .await
8275 .unwrap();
8276
8277 for i in 0..25u64 {
8278 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8279 }
8280 journal = journal.sync().await.unwrap();
8281
8282 let journal = journal
8286 .test_set_offsets_recovery_watermark(10)
8287 .await
8288 .unwrap();
8289 let offset = journal.0.offsets.reader().read(12).await.unwrap();
8290 drop(journal);
8291
8292 let (blob, size) = context
8294 .open(&cfg.data_partition(), &1u64.to_be_bytes())
8295 .await
8296 .unwrap();
8297 let mut writer = PagedRecovery::open(blob, size, 1024, cfg.page_cache.clone())
8298 .await
8299 .unwrap();
8300 writer = writer.truncate(offset).await.unwrap();
8301 writer = writer.sync().await.unwrap();
8302 drop(writer);
8303
8304 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8305 .await
8306 .unwrap();
8307 assert_eq!(journal.bounds(), 0..12);
8308 assert_eq!(journal.test_offsets_size(), 12);
8309 for i in 0..12u64 {
8310 assert_eq!(journal.read(i).await.unwrap(), i * 100);
8311 }
8312 assert!(matches!(
8313 journal.read(12).await,
8314 Err(Error::ItemOutOfRange(12))
8315 ));
8316
8317 journal.destroy().await.unwrap();
8318 });
8319 }
8320
8321 #[test_traced]
8322 fn test_variable_init_persists_offsets_trailing_item_repair() {
8323 let executor = deterministic::Runner::default();
8324 let ((offsets_blob_partition, expected_size), checkpoint) =
8325 executor.start_and_recover(|context| async move {
8326 let cfg = Config {
8327 partition: "offsets-init-repair-sync".into(),
8328 items_per_section: NZU64!(10),
8329 compression: None,
8330 codec_config: (),
8331 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8332 write_buffer: NZUsize!(1024),
8333 replay_buffer: NZUsize!(1024),
8334 };
8335 let offsets_blob_partition = format!("{}-blobs", cfg.offsets_partition());
8336 let expected_size = 2 * std::mem::size_of::<u64>() as u64;
8337
8338 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8339 .await
8340 .unwrap();
8341 (journal, _) = journal.append(&10).await.unwrap();
8342 (journal, _) = journal.append(&20).await.unwrap();
8343 let journal = journal.sync().await.unwrap();
8344 drop(journal);
8345
8346 let (blob, raw_size) = context
8347 .open(&offsets_blob_partition, &0u64.to_be_bytes())
8348 .await
8349 .unwrap();
8350 let mut append = Writer::new(
8351 blob,
8352 raw_size,
8353 2048,
8354 CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8355 )
8356 .await
8357 .unwrap();
8358 assert_eq!(append.size(), expected_size);
8359 (append, _) = append.append(&[0]).await.unwrap();
8360 append = append.sync().await.unwrap();
8361 drop(append);
8362
8363 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
8364 .await
8365 .unwrap();
8366 assert_eq!(journal.bounds(), 0..2);
8367 drop(journal);
8368
8369 (offsets_blob_partition, expected_size)
8370 });
8371
8372 deterministic::Runner::from(checkpoint).start(move |context| async move {
8373 let (blob, raw_size) = context
8374 .open(&offsets_blob_partition, &0u64.to_be_bytes())
8375 .await
8376 .unwrap();
8377 let append = Writer::new(
8378 blob,
8379 raw_size,
8380 2048,
8381 CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8382 )
8383 .await
8384 .unwrap();
8385 assert_eq!(append.size(), expected_size);
8386 });
8387 }
8388
8389 #[test_traced]
8390 fn test_variable_init_persists_data_tail_repair() {
8391 let executor = deterministic::Runner::default();
8392 let ((data_partition, expected_size), checkpoint) =
8393 executor.start_and_recover(|context| async move {
8394 let cfg = Config {
8395 partition: "data-init-repair-sync".into(),
8396 items_per_section: NZU64!(10),
8397 compression: None,
8398 codec_config: (),
8399 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8400 write_buffer: NZUsize!(1024),
8401 replay_buffer: NZUsize!(1024),
8402 };
8403 let data_partition = cfg.data_partition();
8404
8405 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8406 .await
8407 .unwrap();
8408 (journal, _) = journal.append(&10).await.unwrap();
8409 (journal, _) = journal.append(&20).await.unwrap();
8410 let journal = journal.sync().await.unwrap();
8411 drop(journal);
8412
8413 let (blob, raw_size) = context
8414 .open(&data_partition, &0u64.to_be_bytes())
8415 .await
8416 .unwrap();
8417 let mut append = Writer::new(
8418 blob,
8419 raw_size,
8420 2048,
8421 CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8422 )
8423 .await
8424 .unwrap();
8425 let expected_size = append.size();
8426 (append, _) = append.append(&[0xFF, 0xFF]).await.unwrap();
8427 append = append.sync().await.unwrap();
8428 drop(append);
8429
8430 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
8431 .await
8432 .unwrap();
8433 assert_eq!(journal.bounds(), 0..2);
8434 drop(journal);
8435
8436 (data_partition, expected_size)
8437 });
8438
8439 deterministic::Runner::from(checkpoint).start(move |context| async move {
8440 let (blob, raw_size) = context
8441 .open(&data_partition, &0u64.to_be_bytes())
8442 .await
8443 .unwrap();
8444 let append = Writer::new(
8445 blob,
8446 raw_size,
8447 2048,
8448 CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8449 )
8450 .await
8451 .unwrap();
8452 assert_eq!(append.size(), expected_size);
8453 });
8454 }
8455
8456 #[test_traced]
8459 fn test_variable_recovery_empty_offsets_after_prune_and_append() {
8460 let executor = deterministic::Runner::default();
8461 executor.start(|context| async move {
8462 let cfg = Config {
8463 partition: "recovery-empty-after-prune".into(),
8464 items_per_section: NZU64!(10),
8465 compression: None,
8466 codec_config: (),
8467 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8468 write_buffer: NZUsize!(1024),
8469 replay_buffer: NZUsize!(1024),
8470 };
8471
8472 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8473 .await
8474 .unwrap();
8475
8476 for i in 0..10u64 {
8478 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8479 }
8480 let bounds = journal.bounds();
8481 assert_eq!(bounds.end, 10);
8482 assert_eq!(bounds.start, 0);
8483
8484 (journal, _) = journal.prune(10).await.unwrap();
8486 let bounds = journal.bounds();
8487 assert_eq!(bounds.end, 10);
8488 assert!(bounds.is_empty()); for i in 10..20u64 {
8493 (journal, _, _) = journal.test_append_data(1, i * 100).await.unwrap();
8494 }
8495 let (blobs, sync) = journal.0.blobs.start_sync().await.unwrap();
8497 journal.0.blobs = blobs;
8498 sync.await.unwrap();
8499
8500 drop(journal);
8504
8505 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8507 .await
8508 .expect("Should recover from crash after data sync but before offsets sync");
8509
8510 let bounds = journal.bounds();
8512 assert_eq!(bounds.end, 20);
8513 assert_eq!(bounds.start, 10);
8514
8515 for i in 10..20u64 {
8517 assert_eq!(journal.read(i).await.unwrap(), i * 100);
8518 }
8519
8520 for i in 0..10 {
8522 assert!(matches!(journal.read(i).await, Err(Error::ItemPruned(_))));
8523 }
8524
8525 journal.destroy().await.unwrap();
8526 });
8527 }
8528
8529 #[test_traced]
8531 fn test_variable_concurrent_sync_recovery() {
8532 let executor = deterministic::Runner::default();
8533 executor.start(|context| async move {
8534 let cfg = Config {
8535 partition: "concurrent-sync-recovery".into(),
8536 items_per_section: NZU64!(10),
8537 compression: None,
8538 codec_config: (),
8539 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
8540 write_buffer: NZUsize!(1024),
8541 replay_buffer: NZUsize!(1024),
8542 };
8543
8544 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8545 .await
8546 .unwrap();
8547
8548 for i in 0..15u64 {
8550 (journal, _) = journal.append(&(i * 100)).await.unwrap();
8551 }
8552
8553 let journal = journal.commit().await.unwrap();
8555
8556 drop(journal);
8558
8559 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8560 .await
8561 .unwrap();
8562
8563 assert_eq!(journal.size(), 15);
8565 for i in 0..15u64 {
8566 assert_eq!(journal.read(i).await.unwrap(), i * 100);
8567 }
8568
8569 journal.destroy().await.unwrap();
8570 });
8571 }
8572
8573 #[test_traced]
8574 fn test_variable_recovery_from_mid_blob_durable_anchor() {
8575 let executor = deterministic::Runner::default();
8576 executor.start(|context| async move {
8577 let cfg = Config {
8578 partition: "mid-blob-durable-anchor".into(),
8579 items_per_section: NZU64!(5),
8580 compression: None,
8581 codec_config: (),
8582 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8583 write_buffer: NZUsize!(1024),
8584 replay_buffer: NZUsize!(1024),
8585 };
8586
8587 let mut journal =
8588 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
8589 .await
8590 .unwrap();
8591 let appended;
8592 (journal, appended) = journal.append(&700).await.unwrap();
8593 assert_eq!(appended, 7);
8594 journal = journal.sync().await.unwrap();
8595
8596 for i in 1..6u64 {
8597 let appended;
8598 (journal, appended) = journal.append(&(700 + i)).await.unwrap();
8599 assert_eq!(appended, 7 + i);
8600 }
8601 journal.commit().await.unwrap();
8602
8603 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8604 .await
8605 .unwrap();
8606 assert_eq!(journal.bounds(), 7..13);
8607 for i in 0..6u64 {
8608 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
8609 }
8610
8611 journal.destroy().await.unwrap();
8612 });
8613 }
8614
8615 #[test_traced]
8616 fn test_init_at_size_rejects_conflicting_offsets_partitions() {
8617 let executor = deterministic::Runner::default();
8618 executor.start(|context| async move {
8619 let cfg = Config {
8620 partition: "init-at-size-conflicting-offsets".into(),
8621 items_per_section: NZU64!(5),
8622 compression: None,
8623 codec_config: (),
8624 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8625 write_buffer: NZUsize!(1024),
8626 replay_buffer: NZUsize!(1024),
8627 };
8628 let legacy_partition = cfg.offsets_partition();
8629 let blobs_partition = format!("{legacy_partition}-blobs");
8630
8631 for partition in [&legacy_partition, &blobs_partition] {
8632 let (blob, _) = context.open(partition, &0u64.to_be_bytes()).await.unwrap();
8633 blob.write_at(0, vec![0], WriteOptions::SYNC).await.unwrap();
8634 }
8635
8636 let result = Journal::<_, u64>::init_at_size(context.child("storage"), cfg, 7).await;
8637 assert!(matches!(result, Err(Error::Corruption(_))));
8638
8639 assert_eq!(context.scan(&legacy_partition).await.unwrap().len(), 1);
8642 assert_eq!(context.scan(&blobs_partition).await.unwrap().len(), 1);
8643 });
8644 }
8645
8646 #[test_traced]
8647 fn test_init_at_size_zero() {
8648 let executor = deterministic::Runner::default();
8649 executor.start(|context| async move {
8650 let cfg = Config {
8651 partition: "init-at-size-zero".into(),
8652 items_per_section: NZU64!(5),
8653 compression: None,
8654 codec_config: (),
8655 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8656 write_buffer: NZUsize!(1024),
8657 replay_buffer: NZUsize!(1024),
8658 };
8659
8660 let mut journal =
8661 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 0)
8662 .await
8663 .unwrap();
8664
8665 assert_eq!(journal.size(), 0);
8667
8668 assert!(journal.bounds().is_empty());
8670
8671 let pos;
8673 (journal, pos) = journal.append(&100).await.unwrap();
8674 assert_eq!(pos, 0);
8675 assert_eq!(journal.size(), 1);
8676 assert_eq!(journal.read(0).await.unwrap(), 100);
8677
8678 journal.destroy().await.unwrap();
8679 });
8680 }
8681
8682 #[test_traced]
8683 fn test_init_at_size_blob_boundary() {
8684 let executor = deterministic::Runner::default();
8685 executor.start(|context| async move {
8686 let cfg = Config {
8687 partition: "init-at-size-boundary".into(),
8688 items_per_section: NZU64!(5),
8689 compression: None,
8690 codec_config: (),
8691 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8692 write_buffer: NZUsize!(1024),
8693 replay_buffer: NZUsize!(1024),
8694 };
8695
8696 let mut journal =
8698 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 10)
8699 .await
8700 .unwrap();
8701
8702 let bounds = journal.bounds();
8704 assert_eq!(bounds.end, 10);
8705
8706 assert!(bounds.is_empty());
8708
8709 let pos;
8711 (journal, pos) = journal.append(&1000).await.unwrap();
8712 assert_eq!(pos, 10);
8713 assert_eq!(journal.size(), 11);
8714 assert_eq!(journal.read(10).await.unwrap(), 1000);
8715
8716 let pos;
8718 (journal, pos) = journal.append(&1001).await.unwrap();
8719 assert_eq!(pos, 11);
8720 assert_eq!(journal.read(11).await.unwrap(), 1001);
8721
8722 journal.destroy().await.unwrap();
8723 });
8724 }
8725
8726 #[test_traced]
8727 fn test_init_at_size_mid_blob() {
8728 let executor = deterministic::Runner::default();
8729 executor.start(|context| async move {
8730 let cfg = Config {
8731 partition: "init-at-size-mid".into(),
8732 items_per_section: NZU64!(5),
8733 compression: None,
8734 codec_config: (),
8735 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8736 write_buffer: NZUsize!(1024),
8737 replay_buffer: NZUsize!(1024),
8738 };
8739
8740 let mut journal =
8742 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 7)
8743 .await
8744 .unwrap();
8745
8746 let bounds = journal.bounds();
8748 assert_eq!(bounds.end, 7);
8749
8750 assert!(bounds.is_empty());
8752
8753 let pos;
8755 (journal, pos) = journal.append(&700).await.unwrap();
8756 assert_eq!(pos, 7);
8757 assert_eq!(journal.size(), 8);
8758 assert_eq!(journal.read(7).await.unwrap(), 700);
8759
8760 journal.destroy().await.unwrap();
8761 });
8762 }
8763
8764 #[test_traced]
8765 fn test_init_at_size_persistence() {
8766 let executor = deterministic::Runner::default();
8767 executor.start(|context| async move {
8768 let cfg = Config {
8769 partition: "init-at-size-persist".into(),
8770 items_per_section: NZU64!(5),
8771 compression: None,
8772 codec_config: (),
8773 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8774 write_buffer: NZUsize!(1024),
8775 replay_buffer: NZUsize!(1024),
8776 };
8777
8778 let mut journal =
8780 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15)
8781 .await
8782 .unwrap();
8783
8784 for i in 0..5u64 {
8786 let pos;
8787 (journal, pos) = journal.append(&(1500 + i)).await.unwrap();
8788 assert_eq!(pos, 15 + i);
8789 }
8790
8791 assert_eq!(journal.size(), 20);
8792
8793 journal.sync().await.unwrap();
8795
8796 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8797 .await
8798 .unwrap();
8799
8800 let bounds = journal.bounds();
8802 assert_eq!(bounds.end, 20);
8803 assert_eq!(bounds.start, 15);
8804
8805 for i in 0..5u64 {
8807 assert_eq!(journal.read(15 + i).await.unwrap(), 1500 + i);
8808 }
8809
8810 let pos;
8812 (journal, pos) = journal.append(&9999).await.unwrap();
8813 assert_eq!(pos, 20);
8814 assert_eq!(journal.read(20).await.unwrap(), 9999);
8815
8816 journal.destroy().await.unwrap();
8817 });
8818 }
8819
8820 #[test_traced]
8821 fn test_init_at_size_persistence_without_data() {
8822 let executor = deterministic::Runner::default();
8823 executor.start(|context| async move {
8824 let cfg = Config {
8825 partition: "init-at-size-persist-empty".into(),
8826 items_per_section: NZU64!(5),
8827 compression: None,
8828 codec_config: (),
8829 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8830 write_buffer: NZUsize!(1024),
8831 replay_buffer: NZUsize!(1024),
8832 };
8833
8834 let journal = Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 15)
8836 .await
8837 .unwrap();
8838
8839 let bounds = journal.bounds();
8840 assert_eq!(bounds.end, 15);
8841 assert!(bounds.is_empty());
8842
8843 drop(journal);
8845
8846 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8848 .await
8849 .unwrap();
8850
8851 let bounds = journal.bounds();
8852 assert_eq!(bounds.end, 15);
8853 assert!(bounds.is_empty());
8854
8855 let pos;
8857 (journal, pos) = journal.append(&1500).await.unwrap();
8858 assert_eq!(pos, 15);
8859 assert_eq!(journal.read(15).await.unwrap(), 1500);
8860
8861 journal.destroy().await.unwrap();
8862 });
8863 }
8864
8865 #[test_traced]
8866 fn test_init_at_size_clears_existing_data() {
8867 let executor = deterministic::Runner::default();
8868 executor.start(|context| async move {
8869 let cfg = Config {
8870 partition: "init-at-size-clears-existing".into(),
8871 items_per_section: NZU64!(5),
8872 compression: None,
8873 codec_config: (),
8874 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8875 write_buffer: NZUsize!(1024),
8876 replay_buffer: NZUsize!(1024),
8877 };
8878
8879 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8880 .await
8881 .unwrap();
8882 for i in 0..12u64 {
8883 (journal, _) = journal.append(&(100 + i)).await.unwrap();
8884 }
8885 journal.sync().await.unwrap();
8886
8887 let mut journal =
8888 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 7)
8889 .await
8890 .unwrap();
8891 assert_eq!(journal.bounds(), 7..7);
8892 let appended;
8893 (journal, appended) = journal.append(&700).await.unwrap();
8894 assert_eq!(appended, 7);
8895 journal.sync().await.unwrap();
8896
8897 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
8898 .await
8899 .unwrap();
8900 assert_eq!(journal.bounds(), 7..8);
8901 assert_eq!(journal.read(7).await.unwrap(), 700);
8902 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
8903 assert!(matches!(
8904 journal.read(8).await,
8905 Err(Error::ItemOutOfRange(8))
8906 ));
8907
8908 journal.destroy().await.unwrap();
8909 });
8910 }
8911
8912 #[test_traced]
8913 fn test_init_at_size_stages_reset_before_clearing_data() {
8914 let partition = "init-at-size-stage-before-clear-failure".to_string();
8915 let executor = deterministic::Runner::default();
8916 let ((), checkpoint) = executor.start_and_recover({
8917 let partition = partition.clone();
8918 |context| async move {
8919 let cfg = Config {
8920 partition,
8921 items_per_section: NZU64!(5),
8922 compression: None,
8923 codec_config: (),
8924 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8925 write_buffer: NZUsize!(1024),
8926 replay_buffer: NZUsize!(1024),
8927 };
8928
8929 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8930 .await
8931 .unwrap();
8932 for i in 0..12u64 {
8933 (journal, _) = journal.append(&(100 + i)).await.unwrap();
8934 }
8935 let journal = journal.sync().await.unwrap();
8936 drop(journal);
8937
8938 *context.storage_fault_config().write() = deterministic::FaultConfig {
8939 sync_rate: Some(probability!(1.0)),
8940 ..Default::default()
8941 };
8942 assert!(
8943 Journal::<_, u64>::init_at_size(context.child("reset"), cfg, 7)
8944 .await
8945 .is_err()
8946 );
8947 }
8948 });
8949
8950 deterministic::Runner::from(checkpoint).start(move |context| async move {
8951 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
8952 let cfg = Config {
8953 partition,
8954 items_per_section: NZU64!(5),
8955 compression: None,
8956 codec_config: (),
8957 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8958 write_buffer: NZUsize!(1024),
8959 replay_buffer: NZUsize!(1024),
8960 };
8961
8962 let journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
8963 .await
8964 .unwrap();
8965 assert_eq!(journal.bounds(), 0..12);
8966 for i in 0..12u64 {
8967 assert_eq!(journal.read(i).await.unwrap(), 100 + i);
8968 }
8969
8970 journal.destroy().await.unwrap();
8971 });
8972 }
8973
8974 #[test_traced]
8975 fn test_clear_to_size_stages_reset_before_clearing_data() {
8976 let partition = "clear-to-size-stage-before-clear-failure".to_string();
8977 let executor = deterministic::Runner::default();
8978 let ((), checkpoint) = executor.start_and_recover({
8979 let partition = partition.clone();
8980 |context| async move {
8981 let cfg = Config {
8982 partition,
8983 items_per_section: NZU64!(5),
8984 compression: None,
8985 codec_config: (),
8986 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
8987 write_buffer: NZUsize!(1024),
8988 replay_buffer: NZUsize!(1024),
8989 };
8990
8991 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
8992 .await
8993 .unwrap();
8994 for i in 0..12u64 {
8995 (journal, _) = journal.append(&(100 + i)).await.unwrap();
8996 }
8997 let journal = journal.sync().await.unwrap();
8998
8999 *context.storage_fault_config().write() = deterministic::FaultConfig {
9002 sync_rate: Some(probability!(1.0)),
9003 ..Default::default()
9004 };
9005 assert!(journal.0.clear_to_size(17).await.is_err());
9006 }
9007 });
9008
9009 deterministic::Runner::from(checkpoint).start(move |context| async move {
9010 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
9011 let cfg = Config {
9012 partition,
9013 items_per_section: NZU64!(5),
9014 compression: None,
9015 codec_config: (),
9016 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9017 write_buffer: NZUsize!(1024),
9018 replay_buffer: NZUsize!(1024),
9019 };
9020
9021 let journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
9022 .await
9023 .unwrap();
9024 assert_eq!(journal.bounds(), 0..12);
9025 for i in 0..12u64 {
9026 assert_eq!(journal.read(i).await.unwrap(), 100 + i);
9027 }
9028
9029 journal.destroy().await.unwrap();
9030 });
9031 }
9032
9033 #[test_traced]
9034 fn test_clear_to_size_crash_after_staging_completes_on_init() {
9035 let partition = "clear-to-size-crash-after-staging".to_string();
9036 let executor = deterministic::Runner::default();
9037 let ((), checkpoint) = executor.start_and_recover({
9038 let partition = partition.clone();
9039 |context| async move {
9040 let cfg = Config {
9041 partition,
9042 items_per_section: NZU64!(5),
9043 compression: None,
9044 codec_config: (),
9045 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9046 write_buffer: NZUsize!(1024),
9047 replay_buffer: NZUsize!(1024),
9048 };
9049
9050 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
9051 .await
9052 .unwrap();
9053 for i in 0..12u64 {
9054 (journal, _) = journal.append(&(100 + i)).await.unwrap();
9055 }
9056 journal = journal.sync().await.unwrap();
9057
9058 *context.storage_fault_config().write() = deterministic::FaultConfig {
9062 remove_rate: Some(probability!(1.0)),
9063 ..Default::default()
9064 };
9065 assert!(journal.0.clear_to_size(17).await.is_err());
9066 }
9067 });
9068
9069 deterministic::Runner::from(checkpoint).start(move |context| async move {
9070 *context.storage_fault_config().write() = deterministic::FaultConfig::default();
9071 let cfg = Config {
9072 partition,
9073 items_per_section: NZU64!(5),
9074 compression: None,
9075 codec_config: (),
9076 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9077 write_buffer: NZUsize!(1024),
9078 replay_buffer: NZUsize!(1024),
9079 };
9080
9081 let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
9083 .await
9084 .unwrap();
9085 assert_eq!(journal.bounds(), 17..17);
9086 let appended;
9087 (journal, appended) = journal.append(&700).await.unwrap();
9088 assert_eq!(appended, 17);
9089 journal.sync().await.unwrap();
9090
9091 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg.clone())
9093 .await
9094 .unwrap();
9095 assert_eq!(journal.bounds(), 17..18);
9096 assert_eq!(journal.read(17).await.unwrap(), 700);
9097
9098 journal.destroy().await.unwrap();
9099 });
9100 }
9101
9102 #[test_traced]
9103 fn test_init_at_size_recovers_staged_reset_crash_points() {
9104 let executor = deterministic::Runner::default();
9105 executor.start(|context| async move {
9106 for (index, clear_data) in [false, true].into_iter().enumerate() {
9107 let cfg = Config {
9108 partition: format!("init-at-size-staged-reset-crash-{index}"),
9109 items_per_section: NZU64!(5),
9110 compression: None,
9111 codec_config: (),
9112 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9113 write_buffer: NZUsize!(1024),
9114 replay_buffer: NZUsize!(1024),
9115 };
9116
9117 let mut journal = Journal::<_, u64>::init(
9118 context.child("first").with_attribute("index", index),
9119 cfg.clone(),
9120 )
9121 .await
9122 .unwrap();
9123 for i in 0..12u64 {
9124 (journal, _) = journal.append(&(100 + i)).await.unwrap();
9125 }
9126 let journal = journal.sync().await.unwrap();
9127 drop(journal);
9128
9129 let offsets_cfg = fixed::Config {
9130 partition: cfg.offsets_partition(),
9131 items_per_blob: cfg.items_per_section,
9132 page_cache: cfg.page_cache.clone(),
9133 write_buffer: cfg.write_buffer,
9134 replay_buffer: cfg.replay_buffer,
9135 };
9136 let intent_ctx = context.child("intent").with_attribute("index", index);
9140 fixed::Journal::<_, u64>::test_stage_clear(
9141 intent_ctx.child("meta"),
9142 &offsets_cfg.partition,
9143 7,
9144 )
9145 .await
9146 .unwrap();
9147
9148 if clear_data {
9149 Partition::<deterministic::Context>::remove_all(
9150 &context,
9151 &cfg.data_partition(),
9152 )
9153 .await
9154 .unwrap();
9155 }
9156
9157 let mut journal = Journal::<_, u64>::init(
9158 context.child("recover").with_attribute("index", index),
9159 cfg.clone(),
9160 )
9161 .await
9162 .unwrap();
9163 assert_eq!(journal.bounds(), 7..7);
9164 let appended;
9165 (journal, appended) = journal.append(&700).await.unwrap();
9166 assert_eq!(appended, 7);
9167 journal.sync().await.unwrap();
9168
9169 let journal = Journal::<_, u64>::init(
9170 context.child("reopen").with_attribute("index", index),
9171 cfg.clone(),
9172 )
9173 .await
9174 .unwrap();
9175 assert_eq!(journal.bounds(), 7..8);
9176 assert_eq!(journal.read(7).await.unwrap(), 700);
9177
9178 journal.destroy().await.unwrap();
9179 }
9180 });
9181 }
9182
9183 #[test_traced]
9184 fn test_init_at_size_overwrites_pending_clear_target() {
9185 let executor = deterministic::Runner::default();
9186 executor.start(|context| async move {
9187 let cfg = Config {
9188 partition: "init-at-size-overwrites-pending-target".into(),
9189 items_per_section: NZU64!(5),
9190 compression: None,
9191 codec_config: (),
9192 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9193 write_buffer: NZUsize!(1024),
9194 replay_buffer: NZUsize!(1024),
9195 };
9196
9197 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
9198 .await
9199 .unwrap();
9200 for i in 0..12u64 {
9201 (journal, _) = journal.append(&(100 + i)).await.unwrap();
9202 }
9203 let journal = journal.sync().await.unwrap();
9204 drop(journal);
9205
9206 let offsets_cfg = fixed::Config {
9209 partition: cfg.offsets_partition(),
9210 items_per_blob: cfg.items_per_section,
9211 page_cache: cfg.page_cache.clone(),
9212 write_buffer: cfg.write_buffer,
9213 replay_buffer: cfg.replay_buffer,
9214 };
9215 let stale_ctx = context.child("stale");
9216 fixed::Journal::<_, u64>::test_stage_clear(
9217 stale_ctx.child("meta"),
9218 &offsets_cfg.partition,
9219 5,
9220 )
9221 .await
9222 .unwrap();
9223
9224 let mut journal =
9226 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 10)
9227 .await
9228 .unwrap();
9229 assert_eq!(journal.bounds(), 10..10);
9230 let appended;
9231 (journal, appended) = journal.append(&700).await.unwrap();
9232 assert_eq!(appended, 10);
9233 journal.sync().await.unwrap();
9234
9235 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg.clone())
9237 .await
9238 .unwrap();
9239 assert_eq!(journal.bounds(), 10..11);
9240 assert_eq!(journal.read(10).await.unwrap(), 700);
9241
9242 journal.destroy().await.unwrap();
9243 });
9244 }
9245
9246 #[test_traced]
9247 fn test_init_at_size_discards_same_blob_stale_data() {
9248 let executor = deterministic::Runner::default();
9249 executor.start(|context| async move {
9250 let cfg = Config {
9251 partition: "init-at-size-discards-same-blob-stale-data".into(),
9252 items_per_section: NZU64!(5),
9253 compression: None,
9254 codec_config: (),
9255 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9256 write_buffer: NZUsize!(1024),
9257 replay_buffer: NZUsize!(1024),
9258 };
9259
9260 let mut journal =
9261 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 5)
9262 .await
9263 .unwrap();
9264 for i in 0..4u64 {
9265 let appended;
9266 (journal, appended) = journal.append(&(500 + i)).await.unwrap();
9267 assert_eq!(appended, 5 + i);
9268 }
9269 let journal = journal.sync().await.unwrap();
9270 drop(journal);
9271
9272 Journal::<_, u64>::init_at_size(context.child("reset"), cfg.clone(), 7)
9273 .await
9274 .unwrap();
9275
9276 let mut journal = Journal::<_, u64>::init(context.child("after_reset"), cfg.clone())
9277 .await
9278 .unwrap();
9279 assert_eq!(journal.bounds(), 7..7);
9280 assert!(matches!(
9281 journal.read(7).await,
9282 Err(Error::ItemOutOfRange(7))
9283 ));
9284
9285 let appended;
9286 (journal, appended) = journal.append(&700).await.unwrap();
9287 assert_eq!(appended, 7);
9288 journal.sync().await.unwrap();
9289
9290 let journal = Journal::<_, u64>::init(context.child("after_append"), cfg.clone())
9291 .await
9292 .unwrap();
9293 assert_eq!(journal.bounds(), 7..8);
9294 assert_eq!(journal.read(7).await.unwrap(), 700);
9295 assert!(matches!(
9296 journal.read(8).await,
9297 Err(Error::ItemOutOfRange(8))
9298 ));
9299
9300 journal.destroy().await.unwrap();
9301 });
9302 }
9303
9304 #[test_traced]
9306 fn test_init_at_size_mid_blob_persistence() {
9307 let executor = deterministic::Runner::default();
9308 executor.start(|context| async move {
9309 let cfg = Config {
9310 partition: "init-at-size-mid-blob".into(),
9311 items_per_section: NZU64!(5),
9312 compression: None,
9313 codec_config: (),
9314 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9315 write_buffer: NZUsize!(1024),
9316 replay_buffer: NZUsize!(1024),
9317 };
9318
9319 let mut journal =
9321 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
9322 .await
9323 .unwrap();
9324
9325 for i in 0..3u64 {
9327 let pos;
9328 (journal, pos) = journal.append(&(700 + i)).await.unwrap();
9329 assert_eq!(pos, 7 + i);
9330 }
9331
9332 let bounds = journal.bounds();
9333 assert_eq!(bounds.end, 10);
9334 assert_eq!(bounds.start, 7);
9335
9336 journal.sync().await.unwrap();
9338
9339 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
9341 .await
9342 .unwrap();
9343
9344 let bounds = journal.bounds();
9346 assert_eq!(bounds.end, 10);
9347 assert_eq!(bounds.start, 7);
9348
9349 for i in 0..3u64 {
9351 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
9352 }
9353
9354 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
9356
9357 journal.destroy().await.unwrap();
9358 });
9359 }
9360
9361 #[test_traced]
9363 fn test_init_at_size_mid_blob_multi_blob_persistence() {
9364 let executor = deterministic::Runner::default();
9365 executor.start(|context| async move {
9366 let cfg = Config {
9367 partition: "init-at-size-multi-blob".into(),
9368 items_per_section: NZU64!(5),
9369 compression: None,
9370 codec_config: (),
9371 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9372 write_buffer: NZUsize!(1024),
9373 replay_buffer: NZUsize!(1024),
9374 };
9375
9376 let mut journal =
9378 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
9379 .await
9380 .unwrap();
9381
9382 for i in 0..8u64 {
9384 let pos;
9385 (journal, pos) = journal.append(&(700 + i)).await.unwrap();
9386 assert_eq!(pos, 7 + i);
9387 }
9388
9389 let bounds = journal.bounds();
9390 assert_eq!(bounds.end, 15);
9391 assert_eq!(bounds.start, 7);
9392
9393 journal.sync().await.unwrap();
9395
9396 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
9398 .await
9399 .unwrap();
9400
9401 let bounds = journal.bounds();
9403 assert_eq!(bounds.end, 15);
9404 assert_eq!(bounds.start, 7);
9405
9406 for i in 0..8u64 {
9408 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
9409 }
9410
9411 journal.destroy().await.unwrap();
9412 });
9413 }
9414
9415 #[test_traced]
9417 fn test_align_journals_data_empty_mid_blob_pruning_boundary() {
9418 let executor = deterministic::Runner::default();
9419 executor.start(|context| async move {
9420 let cfg = Config {
9421 partition: "align-journals-mid-blob-pruning-boundary".into(),
9422 items_per_section: NZU64!(5),
9423 compression: None,
9424 codec_config: (),
9425 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9426 write_buffer: NZUsize!(1024),
9427 replay_buffer: NZUsize!(1024),
9428 };
9429
9430 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
9432 .await
9433 .unwrap();
9434 for i in 0..7u64 {
9435 (journal, _) = journal.append(&(100 + i)).await.unwrap();
9436 }
9437 journal = journal.sync().await.unwrap();
9438
9439 drop(journal);
9441 Partition::<deterministic::Context>::remove_all(&context, &cfg.data_partition())
9442 .await
9443 .unwrap();
9444
9445 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
9447 .await
9448 .unwrap();
9449 let bounds = journal.bounds();
9450 assert_eq!(bounds.end, 7);
9451 assert!(bounds.is_empty());
9452
9453 let pos;
9455 (journal, pos) = journal.append(&777).await.unwrap();
9456 assert_eq!(pos, 7);
9457 assert_eq!(journal.size(), 8);
9458 assert_eq!(journal.read(7).await.unwrap(), 777);
9459
9460 let (blobs, sync) = journal.0.blobs.start_sync().await.unwrap();
9462 journal.0.blobs = blobs;
9463 sync.await.unwrap();
9464 drop(journal);
9465
9466 let journal = Journal::<_, u64>::init(context.child("third"), cfg.clone())
9468 .await
9469 .unwrap();
9470 let bounds = journal.bounds();
9471 assert_eq!(bounds.end, 8);
9472 assert_eq!(bounds.start, 7);
9473 assert_eq!(journal.read(7).await.unwrap(), 777);
9474
9475 journal.destroy().await.unwrap();
9476 });
9477 }
9478
9479 #[test_traced]
9481 fn test_init_at_size_crash_data_synced_offsets_not() {
9482 let executor = deterministic::Runner::default();
9483 executor.start(|context| async move {
9484 let cfg = Config {
9485 partition: "init-at-size-crash-recovery".into(),
9486 items_per_section: NZU64!(5),
9487 compression: None,
9488 codec_config: (),
9489 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9490 write_buffer: NZUsize!(1024),
9491 replay_buffer: NZUsize!(1024),
9492 };
9493
9494 let mut journal =
9496 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
9497 .await
9498 .unwrap();
9499
9500 for i in 0..3u64 {
9502 (journal, _) = journal.append(&(700 + i)).await.unwrap();
9503 }
9504
9505 journal = journal.test_sync_data_blob(1).await.unwrap();
9508
9509 drop(journal);
9512
9513 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
9515 .await
9516 .unwrap();
9517
9518 let bounds = journal.bounds();
9520 assert_eq!(bounds.end, 10);
9521 assert_eq!(bounds.start, 7);
9522
9523 for i in 0..3u64 {
9525 assert_eq!(journal.read(7 + i).await.unwrap(), 700 + i);
9526 }
9527
9528 journal.destroy().await.unwrap();
9529 });
9530 }
9531
9532 #[test_traced]
9533 fn test_prune_does_not_move_oldest_retained_backwards() {
9534 let executor = deterministic::Runner::default();
9535 executor.start(|context| async move {
9536 let cfg = Config {
9537 partition: "prune-no-backwards".into(),
9538 items_per_section: NZU64!(5),
9539 compression: None,
9540 codec_config: (),
9541 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9542 write_buffer: NZUsize!(1024),
9543 replay_buffer: NZUsize!(1024),
9544 };
9545
9546 let mut journal =
9547 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), 7)
9548 .await
9549 .unwrap();
9550
9551 for i in 0..3u64 {
9553 let pos;
9554 (journal, pos) = journal.append(&(700 + i)).await.unwrap();
9555 assert_eq!(pos, 7 + i);
9556 }
9557 assert_eq!(journal.bounds().start, 7);
9558
9559 (journal, _) = journal.prune(8).await.unwrap();
9561 assert_eq!(journal.bounds().start, 7);
9562 assert!(matches!(journal.read(6).await, Err(Error::ItemPruned(6))));
9563 assert_eq!(journal.read(7).await.unwrap(), 700);
9564
9565 journal.destroy().await.unwrap();
9566 });
9567 }
9568
9569 #[test_traced]
9570 fn test_variable_recovery_near_max_data_synced_offsets_not() {
9571 let executor = deterministic::Runner::default();
9572 executor.start(|context| async move {
9573 let cfg = Config {
9574 partition: "near-max-data-synced-offsets-not".into(),
9575 items_per_section: NZU64!(10),
9576 compression: None,
9577 codec_config: (),
9578 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9579 write_buffer: NZUsize!(1024),
9580 replay_buffer: NZUsize!(1024),
9581 };
9582
9583 let mut journal =
9584 Journal::<_, u64>::init_at_size(context.child("first"), cfg.clone(), u64::MAX - 1)
9585 .await
9586 .unwrap();
9587 let appended;
9588 (journal, appended) = journal.append(&7).await.unwrap();
9589 assert_eq!(appended, u64::MAX - 1);
9590 journal = journal
9591 .test_sync_data_blob(position_to_blob(u64::MAX - 1, cfg.items_per_section.get()))
9592 .await
9593 .unwrap();
9594 drop(journal);
9595
9596 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
9597 .await
9598 .unwrap();
9599 assert_eq!(journal.bounds(), (u64::MAX - 1)..u64::MAX);
9600 assert_eq!(journal.read(u64::MAX - 1).await.unwrap(), 7);
9601
9602 journal.destroy().await.unwrap();
9603 });
9604 }
9605
9606 #[test_traced]
9607 fn test_init_at_size_large_offset() {
9608 let executor = deterministic::Runner::default();
9609 executor.start(|context| async move {
9610 let cfg = Config {
9611 partition: "init-at-size-large".into(),
9612 items_per_section: NZU64!(5),
9613 compression: None,
9614 codec_config: (),
9615 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9616 write_buffer: NZUsize!(1024),
9617 replay_buffer: NZUsize!(1024),
9618 };
9619
9620 let mut journal =
9622 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 1000)
9623 .await
9624 .unwrap();
9625
9626 let bounds = journal.bounds();
9627 assert_eq!(bounds.end, 1000);
9628 assert!(bounds.is_empty());
9630
9631 let pos;
9633 (journal, pos) = journal.append(&100000).await.unwrap();
9634 assert_eq!(pos, 1000);
9635 assert_eq!(journal.read(1000).await.unwrap(), 100000);
9636
9637 journal.destroy().await.unwrap();
9638 });
9639 }
9640
9641 #[test_traced]
9642 fn test_init_at_size_prune_and_append() {
9643 let executor = deterministic::Runner::default();
9644 executor.start(|context| async move {
9645 let cfg = Config {
9646 partition: "init-at-size-prune".into(),
9647 items_per_section: NZU64!(5),
9648 compression: None,
9649 codec_config: (),
9650 page_cache: CacheRef::new(SMALL_PAGE_SIZE, NZUsize!(2)),
9651 write_buffer: NZUsize!(1024),
9652 replay_buffer: NZUsize!(1024),
9653 };
9654
9655 let mut journal =
9657 Journal::<_, u64>::init_at_size(context.child("storage"), cfg.clone(), 20)
9658 .await
9659 .unwrap();
9660
9661 for i in 0..10u64 {
9663 (journal, _) = journal.append(&(2000 + i)).await.unwrap();
9664 }
9665
9666 assert_eq!(journal.size(), 30);
9667
9668 (journal, _) = journal.prune(25).await.unwrap();
9670
9671 let bounds = journal.bounds();
9672 assert_eq!(bounds.end, 30);
9673 assert_eq!(bounds.start, 25);
9674
9675 for i in 25..30u64 {
9677 assert_eq!(journal.read(i).await.unwrap(), 2000 + (i - 20));
9678 }
9679
9680 let pos;
9682 (journal, pos) = journal.append(&3000).await.unwrap();
9683 assert_eq!(pos, 30);
9684
9685 journal.destroy().await.unwrap();
9686 });
9687 }
9688
9689 #[test_traced]
9691 fn test_init_sync_no_existing_data() {
9692 let executor = deterministic::Runner::default();
9693 executor.start(|context| async move {
9694 let cfg = Config {
9695 partition: "test-fresh-start".into(),
9696 items_per_section: NZU64!(5),
9697 compression: None,
9698 codec_config: (),
9699 write_buffer: NZUsize!(1024),
9700 replay_buffer: NZUsize!(1024),
9701 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
9702 };
9703
9704 let lower_bound = 10;
9706 let upper_bound = 26;
9707 let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
9708 context.child("storage"),
9709 cfg.clone(),
9710 lower_bound..upper_bound,
9711 )
9712 .await
9713 .expect("Failed to initialize journal with sync boundaries");
9714
9715 let bounds = journal.bounds();
9716 assert_eq!(bounds.end, lower_bound);
9717 assert!(bounds.is_empty());
9718
9719 let pos1;
9721 (journal, pos1) = journal.append(&42u64).await.unwrap();
9722 assert_eq!(pos1, lower_bound);
9723 assert_eq!(journal.read(pos1).await.unwrap(), 42u64);
9724
9725 let pos2;
9726 (journal, pos2) = journal.append(&43u64).await.unwrap();
9727 assert_eq!(pos2, lower_bound + 1);
9728 assert_eq!(journal.read(pos2).await.unwrap(), 43u64);
9729
9730 journal.destroy().await.unwrap();
9731 });
9732 }
9733
9734 #[test_traced]
9736 fn test_init_sync_existing_data_overlap() {
9737 let executor = deterministic::Runner::default();
9738 executor.start(|context| async move {
9739 let cfg = Config {
9740 partition: "test-overlap".into(),
9741 items_per_section: NZU64!(5),
9742 compression: None,
9743 codec_config: (),
9744 write_buffer: NZUsize!(1024),
9745 replay_buffer: NZUsize!(1024),
9746 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
9747 };
9748
9749 let mut journal =
9751 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
9752 .await
9753 .expect("Failed to create initial journal");
9754
9755 for i in 0..20u64 {
9757 (journal, _) = journal.append(&(i * 100)).await.unwrap();
9758 }
9759 let journal = journal.sync().await.unwrap();
9760 drop(journal);
9761
9762 let lower_bound = 8;
9765 let upper_bound = 31;
9766 let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
9767 context.child("storage"),
9768 cfg.clone(),
9769 lower_bound..upper_bound,
9770 )
9771 .await
9772 .expect("Failed to initialize journal with overlap");
9773
9774 assert_eq!(journal.size(), 20);
9775
9776 assert_eq!(journal.bounds().start, 5); assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
9781 assert!(matches!(journal.read(4).await, Err(Error::ItemPruned(_))));
9782
9783 assert_eq!(journal.read(5).await.unwrap(), 500);
9785 assert_eq!(journal.read(8).await.unwrap(), 800);
9786 assert_eq!(journal.read(19).await.unwrap(), 1900);
9787
9788 assert!(matches!(
9790 journal.read(20).await,
9791 Err(Error::ItemOutOfRange(_))
9792 ));
9793
9794 let pos;
9796 (journal, pos) = journal.append(&999).await.unwrap();
9797 assert_eq!(pos, 20);
9798 assert_eq!(journal.read(20).await.unwrap(), 999);
9799
9800 journal.destroy().await.unwrap();
9801 });
9802 }
9803
9804 #[should_panic]
9806 #[test_traced]
9807 fn test_init_sync_invalid_parameters() {
9808 let executor = deterministic::Runner::default();
9809 executor.start(|context| async move {
9810 let cfg = Config {
9811 partition: "test-invalid".into(),
9812 items_per_section: NZU64!(5),
9813 compression: None,
9814 codec_config: (),
9815 write_buffer: NZUsize!(1024),
9816 replay_buffer: NZUsize!(1024),
9817 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
9818 };
9819
9820 #[allow(clippy::reversed_empty_ranges)]
9821 let _result = authenticated::init_sync::<_, Journal<_, u64>>(
9822 context.child("storage"),
9823 cfg,
9824 10..5, )
9826 .await;
9827 });
9828 }
9829
9830 #[test_traced]
9832 fn test_init_sync_existing_data_exact_match() {
9833 let executor = deterministic::Runner::default();
9834 executor.start(|context| async move {
9835 let items_per_section = NZU64!(5);
9836 let cfg = Config {
9837 partition: "test-exact-match".into(),
9838 items_per_section,
9839 compression: None,
9840 codec_config: (),
9841 write_buffer: NZUsize!(1024),
9842 replay_buffer: NZUsize!(1024),
9843 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
9844 };
9845
9846 let mut journal =
9848 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
9849 .await
9850 .expect("Failed to create initial journal");
9851
9852 for i in 0..20u64 {
9854 (journal, _) = journal.append(&(i * 100)).await.unwrap();
9855 }
9856 let journal = journal.sync().await.unwrap();
9857 drop(journal);
9858
9859 let lower_bound = 5; let upper_bound = 20; let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
9863 context.child("storage"),
9864 cfg.clone(),
9865 lower_bound..upper_bound,
9866 )
9867 .await
9868 .expect("Failed to initialize journal with exact match");
9869
9870 assert_eq!(journal.size(), 20);
9871
9872 assert_eq!(journal.bounds().start, 5); assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
9877 assert!(matches!(journal.read(4).await, Err(Error::ItemPruned(_))));
9878
9879 assert_eq!(journal.read(5).await.unwrap(), 500);
9881 assert_eq!(journal.read(10).await.unwrap(), 1000);
9882 assert_eq!(journal.read(19).await.unwrap(), 1900);
9883
9884 assert!(matches!(
9886 journal.read(20).await,
9887 Err(Error::ItemOutOfRange(_))
9888 ));
9889
9890 let pos;
9892 (journal, pos) = journal.append(&999).await.unwrap();
9893 assert_eq!(pos, 20);
9894 assert_eq!(journal.read(20).await.unwrap(), 999);
9895
9896 journal.destroy().await.unwrap();
9897 });
9898 }
9899
9900 #[test_traced]
9902 fn test_init_sync_truncates_data_exceeding_upper_bound() {
9903 let executor = deterministic::Runner::default();
9904 executor.start(|context| async move {
9905 let items_per_section = NZU64!(5);
9906 let cfg = Config {
9907 partition: "test-unexpected-data".into(),
9908 items_per_section,
9909 compression: None,
9910 codec_config: (),
9911 write_buffer: NZUsize!(1024),
9912 replay_buffer: NZUsize!(1024),
9913 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
9914 };
9915
9916 let mut journal =
9918 Journal::<deterministic::Context, u64>::init(context.child("initial"), cfg.clone())
9919 .await
9920 .expect("Failed to create initial journal");
9921
9922 for i in 0..30u64 {
9924 (journal, _) = journal.append(&(i * 1000)).await.unwrap();
9925 }
9926 let journal = journal.sync().await.unwrap();
9927 drop(journal);
9928
9929 let lower_bound = 8; let upper_bound = 20;
9932 let journal = authenticated::init_sync::<_, Journal<_, u64>>(
9933 context.child("sync"),
9934 cfg.clone(),
9935 lower_bound..upper_bound,
9936 )
9937 .await
9938 .expect("Failed to truncate journal to the older sync range");
9939
9940 assert_eq!(journal.bounds(), 5..upper_bound);
9941 for i in lower_bound..upper_bound {
9942 assert_eq!(journal.read(i).await.unwrap(), i * 1000);
9943 }
9944 journal.destroy().await.unwrap();
9945 });
9946 }
9947
9948 #[test_traced]
9950 fn test_init_sync_empty_stale_position_beyond_upper_bound() {
9951 let executor = deterministic::Runner::default();
9952 executor.start(|context| async move {
9953 let cfg = Config {
9954 partition: "test-empty-stale-position".into(),
9955 items_per_section: NZU64!(5),
9956 compression: None,
9957 codec_config: (),
9958 write_buffer: NZUsize!(1024),
9959 replay_buffer: NZUsize!(1024),
9960 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
9961 };
9962
9963 let stale_size = 30;
9964 let journal = Journal::<deterministic::Context, u64>::init_at_size(
9965 context.child("first"),
9966 cfg.clone(),
9967 stale_size,
9968 )
9969 .await
9970 .expect("Failed to create stale empty journal");
9971 assert_eq!(journal.size(), stale_size);
9972 assert!(journal.bounds().is_empty());
9973 drop(journal);
9974
9975 let lower_bound = 10;
9976 let upper_bound = 26;
9977 let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
9978 context.child("second"),
9979 cfg.clone(),
9980 lower_bound..upper_bound,
9981 )
9982 .await
9983 .expect("Failed to repair stale empty journal");
9984
9985 assert_eq!(journal.size(), lower_bound);
9986 assert!(journal.bounds().is_empty());
9987
9988 let pos;
9989 (journal, pos) = journal.append(&999).await.unwrap();
9990 assert_eq!(pos, lower_bound);
9991 assert_eq!(journal.read(pos).await.unwrap(), 999);
9992
9993 journal.destroy().await.unwrap();
9994 });
9995 }
9996
9997 #[test_traced]
9999 fn test_init_sync_recovers_from_stale_clear_to_size() {
10000 let executor = deterministic::Runner::default();
10001 executor.start(|context| async move {
10002 let cfg = Config {
10003 partition: "test-stale-clear-to-size".into(),
10004 items_per_section: NZU64!(5),
10005 compression: None,
10006 codec_config: (),
10007 write_buffer: NZUsize!(1024),
10008 replay_buffer: NZUsize!(1024),
10009 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
10010 };
10011
10012 let journal = Journal::<deterministic::Context, u64>::init_at_size(
10013 context.child("first"),
10014 cfg.clone(),
10015 9,
10016 )
10017 .await
10018 .expect("Failed to create stale empty journal");
10019 let journal = journal.sync().await.unwrap();
10020 drop(journal);
10021
10022 match context.remove(&cfg.data_partition(), None).await {
10025 Ok(()) | Err(commonware_runtime::Error::PartitionMissing(_)) => {}
10026 Err(error) => panic!("failed to clear data partition: {error}"),
10027 }
10028
10029 let lower_bound = 7;
10030 let upper_bound = 20;
10031 let journal = authenticated::init_sync::<_, Journal<_, u64>>(
10032 context.child("second"),
10033 cfg.clone(),
10034 lower_bound..upper_bound,
10035 )
10036 .await
10037 .expect("Failed to repair stale empty journal");
10038
10039 assert_eq!(journal.size(), lower_bound);
10040 let bounds = journal.bounds();
10041 assert!(bounds.is_empty());
10042 assert_eq!(bounds.start, lower_bound);
10043
10044 journal.destroy().await.unwrap();
10045 });
10046 }
10047
10048 #[rstest::rstest]
10050 fn test_init_sync_after_interrupted_clear(
10051 #[values(7, 50, 60)] start: u64,
10052 #[values(false, true)] removed: bool,
10053 ) {
10054 deterministic::Runner::default().start(|context| async move {
10055 let cfg = Config {
10056 partition: "test-interrupted-clear".into(),
10057 items_per_section: NZU64!(5),
10058 compression: None,
10059 codec_config: (),
10060 write_buffer: NZUsize!(1024),
10061 replay_buffer: NZUsize!(1024),
10062 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
10063 };
10064
10065 let mut journal = Journal::<_, u64>::init(context.child("seed"), cfg.clone())
10067 .await
10068 .unwrap();
10069 for value in 0..30u64 {
10070 (journal, _) = journal.append(&value).await.unwrap();
10071 }
10072 drop(journal.sync().await.unwrap());
10073
10074 fixed::Journal::<_, u64>::test_stage_clear(
10078 context.child("intent"),
10079 &cfg.offsets_partition(),
10080 50,
10081 )
10082 .await
10083 .unwrap();
10084 if removed {
10085 context.remove(&cfg.data_partition(), None).await.unwrap();
10086 }
10087
10088 let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
10092 context.child("sync"),
10093 cfg.clone(),
10094 start..start + 20,
10095 )
10096 .await
10097 .unwrap();
10098 assert_eq!(journal.bounds(), start..start);
10099
10100 for value in 0..3u64 {
10103 let pos;
10104 (journal, pos) = journal.append(&(1000 + value)).await.unwrap();
10105 assert_eq!(pos, start + value);
10106 }
10107 drop(journal.sync().await.unwrap());
10108
10109 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
10111 .await
10112 .unwrap();
10113 assert_eq!(journal.bounds(), start..start + 3);
10114 for value in 0..3u64 {
10115 assert_eq!(journal.read(start + value).await.unwrap(), 1000 + value);
10116 }
10117
10118 for pos in 0..30 {
10121 let result = journal.read(pos).await;
10122 if pos < start {
10123 assert!(
10124 matches!(result, Err(Error::ItemPruned(_))),
10125 "{pos}: {result:?}"
10126 );
10127 } else if pos >= start + 3 {
10128 assert!(
10129 matches!(result, Err(Error::ItemOutOfRange(_))),
10130 "{pos}: {result:?}"
10131 );
10132 }
10133 }
10134
10135 journal.destroy().await.unwrap();
10136 });
10137 }
10138
10139 #[test_traced]
10141 fn test_init_sync_existing_data_stale() {
10142 let executor = deterministic::Runner::default();
10143 executor.start(|context| async move {
10144 let items_per_section = NZU64!(5);
10145 let cfg = Config {
10146 partition: "test-stale".into(),
10147 items_per_section,
10148 compression: None,
10149 codec_config: (),
10150 write_buffer: NZUsize!(1024),
10151 replay_buffer: NZUsize!(1024),
10152 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
10153 };
10154
10155 let mut journal =
10157 Journal::<deterministic::Context, u64>::init(context.child("first"), cfg.clone())
10158 .await
10159 .expect("Failed to create initial journal");
10160
10161 for i in 0..10u64 {
10163 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10164 }
10165 let journal = journal.sync().await.unwrap();
10166 drop(journal);
10167
10168 let lower_bound = 15; let upper_bound = 26; let journal = authenticated::init_sync::<_, Journal<_, u64>>(
10172 context.child("second"),
10173 cfg.clone(),
10174 lower_bound..upper_bound,
10175 )
10176 .await
10177 .expect("Failed to initialize journal with stale data");
10178
10179 assert_eq!(journal.size(), 15);
10180
10181 assert!(journal.bounds().is_empty());
10183
10184 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
10186 assert!(matches!(journal.read(9).await, Err(Error::ItemPruned(_))));
10187 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(_))));
10188
10189 journal.destroy().await.unwrap();
10190 });
10191 }
10192
10193 #[test_traced]
10195 fn test_init_sync_blob_boundaries() {
10196 let executor = deterministic::Runner::default();
10197 executor.start(|context| async move {
10198 let items_per_section = NZU64!(5);
10199 let cfg = Config {
10200 partition: "test-boundaries".into(),
10201 items_per_section,
10202 compression: None,
10203 codec_config: (),
10204 write_buffer: NZUsize!(1024),
10205 replay_buffer: NZUsize!(1024),
10206 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
10207 };
10208
10209 let mut journal =
10211 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
10212 .await
10213 .expect("Failed to create initial journal");
10214
10215 for i in 0..25u64 {
10217 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10218 }
10219 let journal = journal.sync().await.unwrap();
10220 drop(journal);
10221
10222 let lower_bound = 15; let upper_bound = 25; let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
10226 context.child("storage"),
10227 cfg.clone(),
10228 lower_bound..upper_bound,
10229 )
10230 .await
10231 .expect("Failed to initialize journal at boundaries");
10232
10233 assert_eq!(journal.size(), 25);
10234
10235 assert_eq!(journal.bounds().start, 15);
10237
10238 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
10240 assert!(matches!(journal.read(14).await, Err(Error::ItemPruned(_))));
10241
10242 assert_eq!(journal.read(15).await.unwrap(), 1500);
10244 assert_eq!(journal.read(20).await.unwrap(), 2000);
10245 assert_eq!(journal.read(24).await.unwrap(), 2400);
10246
10247 assert!(matches!(
10249 journal.read(25).await,
10250 Err(Error::ItemOutOfRange(_))
10251 ));
10252
10253 let pos;
10255 (journal, pos) = journal.append(&999).await.unwrap();
10256 assert_eq!(pos, 25);
10257 assert_eq!(journal.read(25).await.unwrap(), 999);
10258
10259 journal.destroy().await.unwrap();
10260 });
10261 }
10262
10263 #[test_traced]
10265 fn test_init_sync_same_blob_bounds() {
10266 let executor = deterministic::Runner::default();
10267 executor.start(|context| async move {
10268 let items_per_section = NZU64!(5);
10269 let cfg = Config {
10270 partition: "test-same-blob".into(),
10271 items_per_section,
10272 compression: None,
10273 codec_config: (),
10274 write_buffer: NZUsize!(1024),
10275 replay_buffer: NZUsize!(1024),
10276 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
10277 };
10278
10279 let mut journal =
10281 Journal::<deterministic::Context, u64>::init(context.child("storage"), cfg.clone())
10282 .await
10283 .expect("Failed to create initial journal");
10284
10285 for i in 0..15u64 {
10287 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10288 }
10289 let journal = journal.sync().await.unwrap();
10290 drop(journal);
10291
10292 let lower_bound = 10; let upper_bound = 15; let mut journal = authenticated::init_sync::<_, Journal<_, u64>>(
10296 context.child("storage"),
10297 cfg.clone(),
10298 lower_bound..upper_bound,
10299 )
10300 .await
10301 .expect("Failed to initialize journal with same-blob bounds");
10302
10303 assert_eq!(journal.size(), 15);
10304
10305 assert_eq!(journal.bounds().start, 10);
10308
10309 assert!(matches!(journal.read(0).await, Err(Error::ItemPruned(_))));
10311 assert!(matches!(journal.read(9).await, Err(Error::ItemPruned(_))));
10312
10313 assert_eq!(journal.read(10).await.unwrap(), 1000);
10315 assert_eq!(journal.read(11).await.unwrap(), 1100);
10316 assert_eq!(journal.read(14).await.unwrap(), 1400);
10317
10318 assert!(matches!(
10320 journal.read(15).await,
10321 Err(Error::ItemOutOfRange(_))
10322 ));
10323
10324 let pos;
10326 (journal, pos) = journal.append(&999).await.unwrap();
10327 assert_eq!(pos, 15);
10328 assert_eq!(journal.read(15).await.unwrap(), 999);
10329
10330 journal.destroy().await.unwrap();
10331 });
10332 }
10333
10334 fn single_item_per_section_config() -> Config<()> {
10336 Config {
10337 partition: "single-item-per-blob".into(),
10338 items_per_section: NZU64!(1),
10339 compression: None,
10340 codec_config: (),
10341 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10342 write_buffer: NZUsize!(1024),
10343 replay_buffer: NZUsize!(1024),
10344 }
10345 }
10346
10347 #[test_traced]
10350 fn test_single_item_per_blob() {
10351 let executor = deterministic::Runner::default();
10352 executor.start(|context| async move {
10353 let cfg = single_item_per_section_config();
10354
10355 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
10357 .await
10358 .unwrap();
10359
10360 let bounds = journal.bounds();
10362 assert_eq!(bounds.end, 0);
10363 assert!(bounds.is_empty());
10364
10365 let pos;
10367 (journal, pos) = journal.append(&0).await.unwrap();
10368 assert_eq!(pos, 0);
10369 assert_eq!(journal.size(), 1);
10370
10371 journal = journal.sync().await.unwrap();
10373
10374 let value = journal.read(journal.size() - 1).await.unwrap();
10376 assert_eq!(value, 0);
10377
10378 for i in 1..10u64 {
10380 let pos;
10381 (journal, pos) = journal.append(&(i * 100)).await.unwrap();
10382 assert_eq!(pos, i);
10383 assert_eq!(journal.size(), i + 1);
10384
10385 let value = journal.read(journal.size() - 1).await.unwrap();
10387 assert_eq!(value, i * 100);
10388 }
10389
10390 for i in 0..10u64 {
10392 assert_eq!(journal.read(i).await.unwrap(), i * 100);
10393 }
10394
10395 journal = journal.sync().await.unwrap();
10398 let pruned;
10399 (journal, pruned) = journal.prune(5).await.unwrap();
10400 assert!(pruned);
10401
10402 assert_eq!(journal.size(), 10);
10404
10405 assert_eq!(journal.bounds().start, 5);
10407
10408 let value = journal.read(journal.size() - 1).await.unwrap();
10410 assert_eq!(value, 900);
10411
10412 for i in 0..5 {
10414 assert!(matches!(
10415 journal.read(i).await,
10416 Err(crate::journal::Error::ItemPruned(_))
10417 ));
10418 }
10419
10420 for i in 5..10u64 {
10422 assert_eq!(journal.read(i).await.unwrap(), i * 100);
10423 }
10424
10425 for i in 10..15u64 {
10427 let pos;
10428 (journal, pos) = journal.append(&(i * 100)).await.unwrap();
10429 assert_eq!(pos, i);
10430
10431 let value = journal.read(journal.size() - 1).await.unwrap();
10433 assert_eq!(value, i * 100);
10434 }
10435
10436 journal.sync().await.unwrap();
10438 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
10439 .await
10440 .unwrap();
10441
10442 assert_eq!(journal.size(), 15);
10444
10445 assert_eq!(journal.bounds().start, 5);
10447
10448 let value = journal.read(journal.size() - 1).await.unwrap();
10450 assert_eq!(value, 1400);
10451
10452 for i in 5..15u64 {
10454 assert_eq!(journal.read(i).await.unwrap(), i * 100);
10455 }
10456
10457 journal.destroy().await.unwrap();
10458
10459 let mut journal = Journal::<_, u64>::init(context.child("after_destroy"), cfg)
10462 .await
10463 .unwrap();
10464 assert!(journal.bounds().is_empty());
10465 assert_eq!(journal.size(), 0);
10466 for i in 0..10u64 {
10467 (journal, _) = journal.append(&((i + 1000) * 100)).await.unwrap();
10468 }
10469 journal = journal.sync().await.unwrap();
10470 assert_eq!(journal.bounds().end, 10);
10471 for i in 0..10u64 {
10472 assert_eq!(journal.read(i).await.unwrap(), (i + 1000) * 100);
10473 }
10474 journal.destroy().await.unwrap();
10475 });
10476 }
10477
10478 #[test_traced]
10482 fn test_single_item_per_blob_restart_after_prune() {
10483 let executor = deterministic::Runner::default();
10484 executor.start(|context| async move {
10485 let cfg = single_item_per_section_config();
10486
10487 let mut journal = Journal::<_, u64>::init(context.child("third"), cfg.clone())
10488 .await
10489 .unwrap();
10490
10491 for i in 0..10u64 {
10493 (journal, _) = journal.append(&(i * 1000)).await.unwrap();
10494 }
10495
10496 (journal, _) = journal.prune(5).await.unwrap();
10498 let bounds = journal.bounds();
10499 assert_eq!(bounds.end, 10);
10500 assert_eq!(bounds.start, 5);
10501
10502 journal.sync().await.unwrap();
10504
10505 let journal = Journal::<_, u64>::init(context.child("fourth"), cfg.clone())
10507 .await
10508 .unwrap();
10509
10510 let bounds = journal.bounds();
10512 assert_eq!(bounds.end, 10);
10513 assert_eq!(bounds.start, 5);
10514
10515 let value = journal.read(journal.size() - 1).await.unwrap();
10517 assert_eq!(value, 9000);
10518
10519 for i in 5..10u64 {
10521 assert_eq!(journal.read(i).await.unwrap(), i * 1000);
10522 }
10523
10524 journal.destroy().await.unwrap();
10525
10526 let mut journal = Journal::<_, u64>::init(context.child("after_destroy"), cfg)
10529 .await
10530 .unwrap();
10531 assert!(journal.bounds().is_empty());
10532 assert_eq!(journal.size(), 0);
10533 for i in 0..10u64 {
10534 (journal, _) = journal.append(&((i + 1000) * 1000)).await.unwrap();
10535 }
10536 journal = journal.sync().await.unwrap();
10537 assert_eq!(journal.bounds().end, 10);
10538 for i in 0..10u64 {
10539 assert_eq!(journal.read(i).await.unwrap(), (i + 1000) * 1000);
10540 }
10541 journal.destroy().await.unwrap();
10542 });
10543 }
10544
10545 #[test_traced]
10548 fn test_single_item_per_blob_prune_all() {
10549 let executor = deterministic::Runner::default();
10550 executor.start(|context| async move {
10551 let cfg = single_item_per_section_config();
10552
10553 let mut journal = Journal::<_, u64>::init(context.child("fifth"), cfg.clone())
10555 .await
10556 .unwrap();
10557
10558 for i in 0..5u64 {
10559 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10560 }
10561 journal = journal.sync().await.unwrap();
10562
10563 (journal, _) = journal.prune(5).await.unwrap();
10565 let bounds = journal.bounds();
10566 assert_eq!(bounds.end, 5); assert!(bounds.is_empty()); let result = journal.read(journal.size() - 1).await;
10571 assert!(matches!(result, Err(crate::journal::Error::ItemPruned(4))));
10572
10573 (journal, _) = journal.append(&500).await.unwrap();
10575 let bounds = journal.bounds();
10576 assert_eq!(bounds.start, 5);
10577 assert_eq!(journal.read(bounds.end - 1).await.unwrap(), 500);
10578
10579 journal.destroy().await.unwrap();
10580 });
10581 }
10582
10583 #[test_traced]
10584 fn test_variable_journal_clear_to_size() {
10585 let executor = deterministic::Runner::default();
10586 executor.start(|context| async move {
10587 let cfg = Config {
10588 partition: "clear-test".into(),
10589 items_per_section: NZU64!(10),
10590 compression: None,
10591 codec_config: (),
10592 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10593 write_buffer: NZUsize!(1024),
10594 replay_buffer: NZUsize!(1024),
10595 };
10596
10597 let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg.clone())
10598 .await
10599 .unwrap();
10600
10601 for i in 0..25u64 {
10603 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10604 }
10605 let bounds = journal.bounds();
10606 assert_eq!(bounds.end, 25);
10607 assert_eq!(bounds.start, 0);
10608 journal = journal.sync().await.unwrap();
10609
10610 journal.0 = journal.0.clear_to_size(100).await.unwrap();
10612 let bounds = journal.bounds();
10613 assert_eq!(bounds.end, 100);
10614 assert!(bounds.is_empty());
10615
10616 for i in 0..25 {
10618 assert!(matches!(
10619 journal.read(i).await,
10620 Err(crate::journal::Error::ItemPruned(_))
10621 ));
10622 }
10623
10624 drop(journal);
10626 let mut journal =
10627 Journal::<_, u64>::init(context.child("journal_after_clear"), cfg.clone())
10628 .await
10629 .unwrap();
10630 let bounds = journal.bounds();
10631 assert_eq!(bounds.end, 100);
10632 assert!(bounds.is_empty());
10633
10634 for i in 100..105u64 {
10636 let pos;
10637 (journal, pos) = journal.append(&(i * 100)).await.unwrap();
10638 assert_eq!(pos, i);
10639 }
10640 let bounds = journal.bounds();
10641 assert_eq!(bounds.end, 105);
10642 assert_eq!(bounds.start, 100);
10643
10644 for i in 100..105u64 {
10646 assert_eq!(journal.read(i).await.unwrap(), i * 100);
10647 }
10648
10649 journal.sync().await.unwrap();
10651
10652 let journal = Journal::<_, u64>::init(context.child("journal_reopened"), cfg)
10653 .await
10654 .unwrap();
10655
10656 let bounds = journal.bounds();
10657 assert_eq!(bounds.end, 105);
10658 assert_eq!(bounds.start, 100);
10659 for i in 100..105u64 {
10660 assert_eq!(journal.read(i).await.unwrap(), i * 100);
10661 }
10662
10663 journal.destroy().await.unwrap();
10664 });
10665 }
10666
10667 #[test_traced]
10668 fn test_variable_journal_clear_to_size_rejects_backward_without_mutation() {
10669 let executor = deterministic::Runner::default();
10670 executor.start(|context| async move {
10671 let cfg = Config {
10672 partition: "clear-to-size-backward".into(),
10673 items_per_section: NZU64!(5),
10674 compression: None,
10675 codec_config: (),
10676 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10677 write_buffer: NZUsize!(1024),
10678 replay_buffer: NZUsize!(1024),
10679 };
10680 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
10681 .await
10682 .unwrap();
10683 for i in 0..2 {
10684 (journal, _) = journal.append(&(100 + i)).await.unwrap();
10685 }
10686 let journal = journal.sync().await.unwrap();
10687
10688 assert!(matches!(
10689 journal.0.clear_to_size(1).await,
10690 Err(Error::ItemOutOfRange(1))
10691 ));
10692
10693 let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
10694 .await
10695 .unwrap();
10696 assert_eq!(journal.bounds(), 0..2);
10697 assert_eq!(journal.read(0).await.unwrap(), 100);
10698 assert_eq!(journal.read(1).await.unwrap(), 101);
10699 journal.destroy().await.unwrap();
10700 });
10701 }
10702
10703 #[test_traced]
10704 fn test_variable_journal_metrics() {
10705 let executor = deterministic::Runner::default();
10706 executor.start(|context| async move {
10707 let cfg = Config {
10708 partition: "metrics".into(),
10709 items_per_section: NZU64!(2),
10710 compression: None,
10711 codec_config: (),
10712 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(10)),
10713 write_buffer: NZUsize!(1024),
10714 replay_buffer: NZUsize!(1024),
10715 };
10716 let mut journal = Journal::<_, u64>::init(context.child("variable_metrics"), cfg)
10717 .await
10718 .unwrap();
10719
10720 let items = [0, 1, 2, 3, 4];
10721 (journal, _) = journal.append_many(Many::Flat(&items)).await.unwrap();
10722 (journal, _) = journal.append(&5).await.unwrap();
10723 let reader;
10724 (journal, reader) = journal.snapshot().await.unwrap();
10725 reader.read(0).await.unwrap();
10726 reader.read_many(&[1, 2]).await.unwrap();
10727 reader.try_read_sync(3).unwrap();
10728 drop(reader);
10729 journal = journal.commit().await.unwrap();
10730 journal = journal.sync().await.unwrap();
10731 let handle;
10732 (journal, handle) = journal.start_sync().await.unwrap();
10733 handle.await.unwrap();
10734 (journal, _) = journal.prune(2).await.unwrap();
10735
10736 let buffer = context.encode();
10737 for expected in [
10738 "variable_metrics_size 6",
10739 "variable_metrics_pruning_boundary 2",
10740 "variable_metrics_retained 4",
10741 "variable_metrics_tail_items 2",
10742 "variable_metrics_append_calls_total 1",
10743 "variable_metrics_append_many_calls_total 1",
10744 "variable_metrics_read_calls_total 1",
10745 "variable_metrics_read_many_calls_total 1",
10746 "variable_metrics_items_read_total 4",
10747 "variable_metrics_start_sync_calls_total 1",
10748 "variable_metrics_commit_calls_total 1",
10749 "variable_metrics_sync_calls_total 1",
10750 "variable_metrics_append_duration_count 1",
10751 "variable_metrics_append_many_duration_count 1",
10752 "variable_metrics_read_duration_count 0",
10753 "variable_metrics_read_many_duration_count 1",
10754 "variable_metrics_commit_duration_count 1",
10755 "variable_metrics_sync_duration_count 1",
10756 "variable_metrics_cache_hits_total 4",
10757 "variable_metrics_cache_misses_total 0",
10758 "variable_metrics_data_tracked",
10759 "variable_metrics_offsets_size 6",
10760 "variable_metrics_offsets_blobs_tracked",
10761 ] {
10762 assert!(buffer.contains(expected), "{expected}\n{buffer}");
10763 }
10764
10765 journal.destroy().await.unwrap();
10766 });
10767 }
10768
10769 #[test_traced]
10770 fn test_variable_journal_read_miss_timed() {
10771 let executor = deterministic::Runner::default();
10773 executor.start(|context| async move {
10774 let cfg = Config {
10777 partition: "miss".into(),
10778 items_per_section: NZU64!(50),
10779 compression: None,
10780 codec_config: (),
10781 page_cache: CacheRef::new(PAGE_SIZE, NZUsize!(10)),
10782 write_buffer: NZUsize!(1024),
10783 replay_buffer: NZUsize!(1024),
10784 };
10785 let mut journal = Journal::<_, u64>::init(context.child("miss"), cfg)
10786 .await
10787 .unwrap();
10788 for i in 0..200u64 {
10789 (journal, _) = journal.append(&i).await.unwrap();
10790 }
10791 journal = journal.sync().await.unwrap();
10792
10793 let reader;
10795 (journal, reader) = journal.snapshot().await.unwrap();
10796 let pos = (0..200)
10797 .find(|&pos| reader.try_read_sync(pos).is_none())
10798 .expect("some position should be cold");
10799 assert_eq!(reader.read(pos).await.unwrap(), pos);
10800 drop(reader);
10801
10802 let buffer = context.encode();
10803 assert!(buffer.contains("miss_read_duration_count 1"), "{buffer}");
10804
10805 journal.destroy().await.unwrap();
10806 });
10807 }
10808
10809 #[test_traced]
10810 fn test_variable_snapshot_frozen_across_roll() {
10811 let executor = deterministic::Runner::default();
10812 executor.start(|context| async move {
10813 let cfg = Config {
10814 partition: "snapshot-frozen".into(),
10815 items_per_section: NZU64!(5),
10816 compression: None,
10817 codec_config: (),
10818 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10819 write_buffer: NZUsize!(1024),
10820 replay_buffer: NZUsize!(1024),
10821 };
10822 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
10823 .await
10824 .unwrap();
10825 for i in 0..7u64 {
10826 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10827 }
10828
10829 let snapshot;
10830 (journal, snapshot) = journal.snapshot().await.unwrap();
10831 assert_eq!(snapshot.bounds(), 0..7);
10832
10833 for i in 7..23u64 {
10836 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10837 }
10838 assert_eq!(snapshot.bounds(), 0..7);
10839 for i in 0..7u64 {
10840 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
10841 }
10842 assert!(matches!(
10843 snapshot.read(7).await,
10844 Err(Error::ItemOutOfRange(7))
10845 ));
10846
10847 let fresh;
10848 (journal, fresh) = journal.snapshot().await.unwrap();
10849 assert_eq!(fresh.bounds(), 0..23);
10850 assert_eq!(fresh.read(22).await.unwrap(), 2200);
10851
10852 drop(snapshot);
10853 drop(fresh);
10854 journal.destroy().await.unwrap();
10855 });
10856 }
10857
10858 #[test_traced]
10859 fn test_variable_prune_under_snapshot() {
10860 let executor = deterministic::Runner::default();
10861 executor.start(|context| async move {
10862 let cfg = Config {
10863 partition: "snapshot-prune".into(),
10864 items_per_section: NZU64!(5),
10865 compression: Some(3),
10866 codec_config: (),
10867 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10868 write_buffer: NZUsize!(1024),
10869 replay_buffer: NZUsize!(1024),
10870 };
10871 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
10872 .await
10873 .unwrap();
10874 for i in 0..17u64 {
10875 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10876 }
10877 journal = journal.sync().await.unwrap();
10878
10879 let snapshot;
10880 (journal, snapshot) = journal.snapshot().await.unwrap();
10881 let pruned;
10882 (journal, pruned) = journal.prune(12).await.unwrap();
10883 assert!(pruned);
10884
10885 assert_eq!(snapshot.bounds(), 0..17);
10887 for i in 0..17u64 {
10888 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
10889 }
10890 assert_eq!(
10891 snapshot.read_many(&[1, 2, 3, 11, 16]).await.unwrap(),
10892 vec![100, 200, 300, 1100, 1600]
10893 );
10894
10895 let fresh;
10896 (journal, fresh) = journal.snapshot().await.unwrap();
10897 assert_eq!(fresh.bounds(), 10..17);
10898 assert!(matches!(fresh.read(3).await, Err(Error::ItemPruned(3))));
10899
10900 drop(snapshot);
10901 drop(fresh);
10902 journal.destroy().await.unwrap();
10903 });
10904 }
10905
10906 #[test_traced]
10907 fn test_variable_snapshots_readable_during_concurrent_appends() {
10908 let executor = deterministic::Runner::default();
10909 executor.start(|context| async move {
10910 let cfg = Config {
10911 partition: "snapshot-concurrent".into(),
10912 items_per_section: NZU64!(5),
10913 compression: None,
10914 codec_config: (),
10915 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10916 write_buffer: NZUsize!(1024),
10917 replay_buffer: NZUsize!(1024),
10918 };
10919 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
10920 .await
10921 .unwrap();
10922
10923 let (mut tx, mut rx) =
10924 futures::channel::mpsc::channel::<Reader<'static, deterministic::Context, u64>>(8);
10925 let validator = context.child("validator").spawn(|_| async move {
10926 let mut validated = 0usize;
10927 while let Some(snapshot) = rx.next().await {
10928 let bounds = snapshot.bounds();
10929 for i in bounds.clone() {
10930 assert_eq!(snapshot.read(i).await.unwrap(), i * 100);
10931 }
10932 validated += (bounds.end - bounds.start) as usize;
10933 }
10934 validated
10935 });
10936
10937 for i in 0..40u64 {
10938 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10939 if i % 7 == 0 {
10940 let snapshot;
10941 (journal, snapshot) = journal.snapshot().await.unwrap();
10942 if tx.try_send(snapshot).is_err() {
10943 break;
10944 }
10945 }
10946 }
10947 drop(tx);
10948 assert!(validator.await.unwrap() > 0);
10949
10950 journal.destroy().await.unwrap();
10951 });
10952 }
10953
10954 #[test_traced]
10955 fn test_variable_replay_from_stale_snapshot() {
10956 let executor = deterministic::Runner::default();
10957 executor.start(|context| async move {
10958 let cfg = Config {
10959 partition: "snapshot-replay".into(),
10960 items_per_section: NZU64!(5),
10961 compression: None,
10962 codec_config: (),
10963 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
10964 write_buffer: NZUsize!(1024),
10965 replay_buffer: NZUsize!(1024),
10966 };
10967 let mut journal = Journal::<_, u64>::init(context.child("j"), cfg)
10968 .await
10969 .unwrap();
10970 for i in 0..7u64 {
10971 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10972 }
10973
10974 let snapshot;
10976 (journal, snapshot) = journal.snapshot().await.unwrap();
10977 assert_eq!(snapshot.bounds(), 0..7);
10978
10979 for i in 7..23u64 {
10981 (journal, _) = journal.append(&(i * 100)).await.unwrap();
10982 }
10983 let pruned;
10984 (journal, pruned) = journal.prune(12).await.unwrap();
10985 assert!(pruned);
10986
10987 {
10988 let stream = replay_items(
10989 &snapshot,
10990 0..snapshot.bounds().end,
10991 NZUsize!(1024),
10992 ReadOptions::default(),
10993 )
10994 .await
10995 .unwrap();
10996 futures::pin_mut!(stream);
10997 let mut expected = 0u64;
10998 while let Some(result) = stream.next().await {
10999 let (pos, item) = result.unwrap();
11000 assert_eq!(pos, expected);
11001 assert_eq!(item, pos * 100);
11002 expected += 1;
11003 }
11004 assert_eq!(expected, 7);
11005 }
11006
11007 drop(snapshot);
11008 journal.destroy().await.unwrap();
11009 });
11010 }
11011
11012 #[test_traced]
11015 fn test_variable_recovery_full_newest_blob_without_successor() {
11016 let executor = deterministic::Runner::default();
11017 executor.start(|context| async move {
11018 let cfg = Config {
11019 partition: "recovery-full-newest-no-successor".into(),
11020 items_per_section: NZU64!(10),
11021 compression: None,
11022 codec_config: (),
11023 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
11024 write_buffer: NZUsize!(1024),
11025 replay_buffer: NZUsize!(1024),
11026 };
11027
11028 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
11029 .await
11030 .unwrap();
11031 for i in 0..10u64 {
11032 (journal, _) = journal.append(&(i * 100)).await.unwrap();
11033 }
11034 let journal = journal.sync().await.unwrap();
11035 drop(journal);
11036
11037 let data_partition = cfg.data_partition();
11040 context
11041 .remove(&data_partition, Some(&1u64.to_be_bytes()))
11042 .await
11043 .unwrap();
11044
11045 let mut journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
11046 .await
11047 .unwrap();
11048 assert_eq!(journal.bounds(), 0..10);
11049 for i in 0..10u64 {
11050 assert_eq!(journal.read(i).await.unwrap(), i * 100);
11051 }
11052 let appended;
11053 (journal, appended) = journal.append(&1000).await.unwrap();
11054 assert_eq!(appended, 10);
11055 assert_eq!(journal.read(10).await.unwrap(), 1000);
11056
11057 journal.destroy().await.unwrap();
11058 });
11059 }
11060
11061 #[test_traced]
11065 fn test_variable_truncate_crash_before_data_truncation() {
11066 let executor = deterministic::Runner::default();
11067 executor.start(|context| async move {
11068 let cfg = Config {
11069 partition: "rewind-crash-offsets-only".into(),
11070 items_per_section: NZU64!(10),
11071 compression: None,
11072 codec_config: (),
11073 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
11074 write_buffer: NZUsize!(1024),
11075 replay_buffer: NZUsize!(1024),
11076 };
11077
11078 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
11079 .await
11080 .unwrap();
11081 for i in 0..25u64 {
11082 (journal, _) = journal.append(&(i * 100)).await.unwrap();
11083 }
11084 let journal = journal.sync().await.unwrap();
11085
11086 let journal = journal.test_truncate_offsets(12).await.unwrap();
11089 drop(journal);
11090
11091 let journal = Journal::<_, u64>::init(context.child("second"), cfg.clone())
11092 .await
11093 .unwrap();
11094 assert_eq!(journal.bounds(), 0..25);
11095 for i in 0..25u64 {
11096 assert_eq!(journal.read(i).await.unwrap(), i * 100);
11097 }
11098
11099 journal.destroy().await.unwrap();
11100 });
11101 }
11102
11103 #[test_traced]
11105 fn test_variable_recovery_rejects_gap_in_retained_blobs() {
11106 let executor = deterministic::Runner::default();
11107 executor.start(|context| async move {
11108 let cfg = Config {
11109 partition: "recovery-gap-in-retained-blobs".into(),
11110 items_per_section: NZU64!(10),
11111 compression: None,
11112 codec_config: (),
11113 page_cache: CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(10)),
11114 write_buffer: NZUsize!(1024),
11115 replay_buffer: NZUsize!(1024),
11116 };
11117
11118 let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
11119 .await
11120 .unwrap();
11121 for i in 0..25u64 {
11122 (journal, _) = journal.append(&(i * 100)).await.unwrap();
11123 }
11124 let journal = journal.sync().await.unwrap();
11125 drop(journal);
11126
11127 context
11129 .remove(&cfg.data_partition(), Some(&1u64.to_be_bytes()))
11130 .await
11131 .unwrap();
11132
11133 let result = Journal::<_, u64>::init(context.child("second"), cfg.clone()).await;
11134 assert!(matches!(result, Err(Error::Corruption(_))));
11135 });
11136 }
11137
11138 #[test_traced]
11141 fn test_variable_destroy_observes_failed_watermark_sync_before_removal() {
11142 let executor = deterministic::Runner::default();
11143 executor.start(|context| async move {
11144 let pending = PendingSyncs::default();
11145 let cfg = initialization_cfg("destroy-failed-watermark", 100);
11146 let data_partition = cfg.data_partition();
11147 let mut journal = Journal::<_, u64>::init(
11148 DelayedSyncContext {
11149 inner: context.child("journal"),
11150 pending: pending.clone(),
11151 },
11152 cfg.clone(),
11153 )
11154 .await
11155 .unwrap();
11156 for item in 1..=3u64 {
11157 (journal, _) = journal.append(&item).await.unwrap();
11158 }
11159 journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
11160
11161 (journal, _) = journal.append(&4).await.unwrap();
11164 let (journal, handle) = journal.start_sync().await.unwrap();
11165 release_pending_syncs(&pending);
11166 handle.await.unwrap();
11167 let (journal, handle) = journal.start_sync().await.unwrap();
11168 drop(handle);
11169 assert_eq!(pending.lock().len(), 1);
11170 fail_pending_syncs(&pending);
11171
11172 assert!(journal.destroy().await.is_err());
11173 assert!(
11174 context
11175 .scan(&data_partition)
11176 .await
11177 .is_ok_and(|names| names.len() == 1)
11178 );
11179 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
11180 .await
11181 .unwrap();
11182 assert_eq!(journal.bounds(), 0..4);
11183 journal.destroy().await.unwrap();
11184 });
11185 }
11186
11187 #[test_traced]
11190 fn test_variable_destroy_observes_failed_offsets_blob_sync_before_removal() {
11191 let executor = deterministic::Runner::default();
11192 executor.start(|context| async move {
11193 let pending = PendingSyncs::default();
11194 let cfg = initialization_cfg("destroy-failed-offsets-blob", 100);
11195 let data_partition = cfg.data_partition();
11196 let mut journal = Journal::<_, u64>::init(
11197 DelayedSyncContext {
11198 inner: context.child("journal"),
11199 pending: pending.clone(),
11200 },
11201 cfg.clone(),
11202 )
11203 .await
11204 .unwrap();
11205 for item in 1..=3u64 {
11206 (journal, _) = journal.append(&item).await.unwrap();
11207 }
11208 journal = drive_pending_syncs(&pending, journal.sync()).await.unwrap();
11209
11210 (journal, _) = journal.append(&4).await.unwrap();
11213 journal = drive_pending_syncs(&pending, journal.commit())
11214 .await
11215 .unwrap();
11216 let (journal, handle) = journal.start_sync().await.unwrap();
11217 drop(handle);
11218 assert_eq!(pending.lock().len(), 1);
11219 fail_pending_syncs(&pending);
11220
11221 assert!(journal.destroy().await.is_err());
11222 assert!(
11223 context
11224 .scan(&data_partition)
11225 .await
11226 .is_ok_and(|names| names.len() == 1)
11227 );
11228 let journal = Journal::<_, u64>::init(context.child("second"), cfg)
11229 .await
11230 .unwrap();
11231 assert_eq!(journal.bounds(), 0..4);
11232 journal.destroy().await.unwrap();
11233 });
11234 }
11235
11236 #[test_traced]
11242 fn test_variable_bounded_open_below_torn_watermark_page_widens() {
11243 let executor = deterministic::Runner::default();
11244 executor.start(|context| async move {
11245 let cfg = torn_rewrite_cfg("variable-torn-watermark-page");
11246 let journal = torn_rewrite_journal(&context, &cfg).await;
11247 drop(journal);
11248
11249 tear_page_rewrite(
11251 &context,
11252 &cfg.data_partition(),
11253 &0u64.to_be_bytes(),
11254 10,
11255 95,
11256 100,
11257 )
11258 .await;
11259 let offsets_partition = format!("{}-blobs", cfg.offsets_config().partition);
11260 tear_page_rewrite(
11261 &context,
11262 &offsets_partition,
11263 &0u64.to_be_bytes(),
11264 2,
11265 70,
11266 100,
11267 )
11268 .await;
11269
11270 let recovery =
11272 Recovery::<_, FixedBytes<32>>::open(context.child("capped"), cfg.clone(), Some(10))
11273 .await
11274 .unwrap();
11275 assert_eq!(recovery.bounds(), 0..10);
11276 assert_eq!(recovery.read(9).await.unwrap(), FixedBytes::new([9u8; 32]));
11277
11278 let physical = (100 + commonware_runtime::buffer::paged::CHECKSUM_SIZE) as usize;
11281 assert_eq!(
11282 context
11283 .logical_blob(&cfg.data_partition(), &0u64.to_be_bytes())
11284 .unwrap()
11285 .len(),
11286 11 * physical
11287 );
11288 let bytes = context
11289 .logical_blob(&offsets_partition, &0u64.to_be_bytes())
11290 .unwrap();
11291 assert!(bytes.len() >= 4 * physical);
11292 assert_eq!(
11293 commonware_runtime::buffer::paged::page_len(
11294 &bytes[2 * physical..3 * physical],
11295 100
11296 ),
11297 Some(64)
11298 );
11299
11300 let recovery = recovery.unbounded().await.unwrap();
11301 assert_eq!(recovery.bounds(), 0..33);
11302 for i in 0..33u64 {
11303 assert_eq!(
11304 recovery.read(i).await.unwrap(),
11305 FixedBytes::new([i as u8; 32])
11306 );
11307 }
11308 });
11309 }
11310}