1use super::{
47 Sealed,
48 read::{PageReader, Replay},
49 tip::Buffer,
50 view::{Tail, View},
51};
52use crate::{
53 Blob, Error, Handle, IoBuf, IoBufMut, IoBufs, ReadOptions, WriteOptions,
54 buffer::{
55 SyncState,
56 paged::{ActiveChecksum, CHECKSUM_SIZE, CacheRef, Checksum, Slot},
57 },
58};
59use bytes::BufMut;
60use commonware_codec::{EncodeSize, Write};
61use commonware_cryptography::Crc32;
62use commonware_utils::Widen;
63use std::{
64 marker::PhantomData,
65 num::{NonZeroU16, NonZeroUsize},
66 sync::Arc,
67};
68use tracing::warn;
69
70fn adjusted_capacity(capacity: usize, page_size: u64) -> usize {
78 let page_size = page_size as usize;
79 let rounded = capacity.next_multiple_of(page_size);
80 let floor = page_size * 2;
81 if rounded < floor {
82 warn!(
83 floor,
84 "requested buffer capacity is too low, increasing it to floor"
85 );
86 }
87 rounded.max(floor)
88}
89
90const fn too_big_for_buffer(
98 buffer_len: usize,
99 buffer_capacity: usize,
100 append_len: usize,
101 page_size: usize,
102) -> bool {
103 let fill = buffer_len.next_multiple_of(page_size) - buffer_len;
104 let overflows_capacity = buffer_len + append_len > buffer_capacity;
105 let has_full_page_after_fill = append_len >= fill + page_size;
106
107 overflows_capacity && has_full_page_after_fill
108}
109
110pub struct Append;
112
113pub struct Recovering;
115
116pub type Recovery<B> = Writer<B, Recovering>;
121
122pub struct Writer<B: Blob, Phase = Append> {
127 phase: PhantomData<Phase>,
129
130 blob: Arc<B>,
132
133 current_page: u64,
135
136 partial_page_state: Option<ActiveChecksum>,
138
139 durable_page_state: Option<ActiveChecksum>,
147
148 sync_state: SyncState,
150
151 id: u64,
153
154 cache_ref: CacheRef,
156
157 buffer: Buffer,
160}
161
162impl<B: Blob> Recovery<B> {
163 pub async fn open(
174 blob: B,
175 original_blob_size: u64,
176 capacity: usize,
177 cache_ref: CacheRef,
178 ) -> Result<Self, Error> {
179 let blob = Arc::new(blob);
180 let page_size: u64 = cache_ref.page_size().widen();
181 let (partial_page_state, pages, invalid_data_found) =
182 Writer::<B>::read_last_valid_page(&blob, original_blob_size, page_size).await?;
183 if invalid_data_found {
184 let new_blob_size = pages * (page_size + CHECKSUM_SIZE);
186 warn!(
187 original_blob_size,
188 new_blob_size, "truncating blob to remove invalid data"
189 );
190 blob.resize(new_blob_size).await?;
191 blob.sync().await?;
192 }
193 let capacity = adjusted_capacity(capacity, page_size);
194
195 let needs_sync = !invalid_data_found;
197
198 let (current_page, partial_page_state, partial_data) = match partial_page_state {
200 Some((partial_page, crc_record)) => (pages - 1, Some(crc_record), Some(partial_page)),
201 None => (pages, None, None),
202 };
203
204 let buffer = Buffer::from(
205 current_page * page_size,
206 partial_data.unwrap_or_default().as_ref(),
207 capacity,
208 page_size as usize,
209 );
210
211 Ok(Self {
212 phase: PhantomData,
213 blob,
214 current_page,
215 partial_page_state,
216 durable_page_state: partial_page_state,
217 sync_state: if needs_sync {
218 SyncState::Dirty
219 } else {
220 SyncState::Clean
221 },
222 id: cache_ref.next_id(),
223 cache_ref,
224 buffer,
225 })
226 }
227
228 async fn sync_shrunk_page(
230 &mut self,
231 page: u64,
232 page_size: u64,
233 new_len: u16,
234 new_crc: u32,
235 old_checksum: &ActiveChecksum,
236 ) -> Result<ActiveChecksum, Error> {
237 let physical_page_size = page_size
243 .checked_add(CHECKSUM_SIZE)
244 .ok_or(Error::OffsetOverflow)?;
245 let crc_start = page
246 .checked_mul(physical_page_size)
247 .and_then(|start| start.checked_add(page_size))
248 .ok_or(Error::OffsetOverflow)?;
249 let old_slot = old_checksum.slot;
250 let new_slot = old_slot.other();
251
252 let new_slot_offset = crc_start
255 .checked_add(new_slot.offset() as u64)
256 .ok_or(Error::OffsetOverflow)?;
257 let staged_slot = Checksum::slot_bytes(0, new_crc);
258 self.sync_state
259 .write_at(
260 &self.blob,
261 new_slot_offset,
262 staged_slot.to_vec(),
263 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
264 )
265 .await?;
266
267 let published_len = Checksum::slot_len_bytes(new_len);
270 self.sync_state
271 .write_at(
272 &self.blob,
273 new_slot_offset,
274 published_len.to_vec(),
275 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
276 )
277 .await?;
278
279 let old_slot_offset = crc_start
284 .checked_add(old_slot.offset() as u64)
285 .ok_or(Error::OffsetOverflow)?;
286 self.sync_state
287 .write_at(
288 &self.blob,
289 old_slot_offset,
290 Checksum::slot_bytes(0, 0).to_vec(),
291 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
292 )
293 .await?;
294
295 Ok(ActiveChecksum::new(new_slot, new_len, new_crc))
296 }
297
298 async fn shrink(mut self, target_size: u64) -> Result<Self, Error> {
301 let page_size: u64 = self.cache_ref.page_size().widen();
302 let physical_page_size = page_size
303 .checked_add(CHECKSUM_SIZE)
304 .ok_or(Error::OffsetOverflow)?;
305
306 self = self.sync().await?;
308
309 let full_pages = target_size / page_size;
311 let partial_bytes = target_size % page_size;
312 let physical_pages = full_pages
313 .checked_add(u64::from(partial_bytes > 0))
314 .ok_or(Error::OffsetOverflow)?;
315 let new_physical_size = physical_pages
316 .checked_mul(physical_page_size)
317 .ok_or(Error::OffsetOverflow)?;
318 let tail_offset = full_pages
319 .checked_mul(page_size)
320 .ok_or(Error::OffsetOverflow)?;
321 let current_physical_size = if self.partial_page_state.is_some() {
322 self.current_page
323 .checked_add(1)
324 .and_then(|pages| pages.checked_mul(physical_page_size))
325 .ok_or(Error::OffsetOverflow)?
326 } else {
327 self.current_page
328 .checked_mul(physical_page_size)
329 .ok_or(Error::OffsetOverflow)?
330 };
331
332 if new_physical_size != current_physical_size {
335 self.sync_state
336 .resize(&self.blob, new_physical_size)
337 .await?;
338 }
339 if partial_bytes > 0 {
340 self.shrink_to_partial(full_pages, partial_bytes, page_size, tail_offset)
341 .await?;
342 return Ok(self);
343 }
344
345 self.partial_page_state = None;
347 self.durable_page_state = None;
348 self.current_page = full_pages;
349 self.buffer.replace(tail_offset, &[]);
350
351 Ok(self)
352 }
353
354 async fn shrink_to_partial(
356 &mut self,
357 full_pages: u64,
358 partial_bytes: u64,
359 page_size: u64,
360 tail_offset: u64,
361 ) -> Result<(), Error> {
362 self.current_page = full_pages;
365
366 let (page_data, old_checksum) = super::get_page_with_checksum_from_blob(
369 &self.blob,
370 full_pages,
371 page_size,
372 ReadOptions::DONT_CACHE,
373 )
374 .await?;
375
376 if (page_data.len() as u64) < partial_bytes {
378 return Err(Error::InvalidChecksum);
379 }
380
381 let new_data = &page_data.as_ref()[..partial_bytes as usize];
382 self.buffer.replace(tail_offset, new_data);
383
384 let final_record = self
385 .sync_shrunk_page(
386 full_pages,
387 page_size,
388 partial_bytes as u16,
389 Crc32::checksum(new_data),
390 &old_checksum,
391 )
392 .await?;
393
394 self.partial_page_state = Some(final_record);
396 self.durable_page_state = Some(final_record);
397
398 Ok(())
399 }
400
401 pub async fn truncate(mut self, size: u64) -> Result<Self, Error> {
409 if size < self.size() {
410 self = self.shrink(size).await?;
411 }
412 self.sync().await
413 }
414}
415
416impl<B: Blob> From<Recovery<B>> for Writer<B> {
417 fn from(recovery: Recovery<B>) -> Self {
421 Self {
422 phase: PhantomData,
423 blob: recovery.blob,
424 current_page: recovery.current_page,
425 partial_page_state: recovery.partial_page_state,
426 durable_page_state: recovery.durable_page_state,
427 sync_state: recovery.sync_state,
428 id: recovery.id,
429 cache_ref: recovery.cache_ref,
430 buffer: recovery.buffer,
431 }
432 }
433}
434
435impl<B: Blob> Writer<B> {
436 pub async fn new(
440 blob: B,
441 size: u64,
442 capacity: usize,
443 cache_ref: CacheRef,
444 ) -> Result<Self, Error> {
445 Ok(Recovery::open(blob, size, capacity, cache_ref)
446 .await?
447 .into())
448 }
449
450 async fn read_last_valid_page(
469 blob: &B,
470 blob_size: u64,
471 page_size: u64,
472 ) -> Result<(Option<(IoBuf, ActiveChecksum)>, u64, bool), Error> {
473 let physical_page_size = page_size + CHECKSUM_SIZE;
474 let partial_bytes = blob_size % physical_page_size;
475 let mut last_page_end = blob_size - partial_bytes;
476
477 let mut invalid_data_found = partial_bytes != 0;
480
481 while last_page_end != 0 {
482 let page_start = last_page_end - physical_page_size;
485 let buf = blob
486 .read_at(
487 page_start,
488 physical_page_size as usize,
489 ReadOptions::default(),
490 )
491 .await?
492 .coalesce()
493 .freeze();
494
495 match Checksum::validate_page(buf.as_ref()) {
496 Some(checksum) => {
497 let len = checksum.len as u64;
499 if len != page_size {
500 let logical_bytes = buf.slice(..len as usize);
502 return Ok((
503 Some((logical_bytes, checksum)),
504 last_page_end / physical_page_size,
505 invalid_data_found,
506 ));
507 }
508 return Ok((None, last_page_end / physical_page_size, invalid_data_found));
510 }
511 None => {
512 last_page_end = page_start;
514 invalid_data_found = true;
515 }
516 }
517 }
518
519 Ok((None, 0, invalid_data_found))
521 }
522
523 pub async fn snapshot(mut self) -> Result<(Self, Sealed<B>), Error> {
531 self.flush_internal(false, false).await?;
533 let sealed = self.sealed_handle(self.id);
534 Ok((self, sealed))
535 }
536
537 pub fn try_snapshot(&self) -> Option<Sealed<B>> {
543 if self.has_flush_work(false) {
544 return None;
545 }
546 Some(self.sealed_handle(self.id))
547 }
548
549 async fn read_page(
551 blob: &B,
552 page: u64,
553 page_size: u64,
554 read_options: ReadOptions,
555 ) -> Result<(IoBuf, u64, u64), Error> {
556 let (logical, _) =
557 super::get_page_with_checksum_from_blob(blob, page, page_size, read_options).await?;
558 let start = page.checked_mul(page_size).ok_or(Error::OffsetOverflow)?;
559 let len: u64 = Widen::widen(logical.len());
560 let end = start.checked_add(len).ok_or(Error::OffsetOverflow)?;
561 Ok((logical, start, end))
562 }
563
564 pub async fn read_range(
569 blob: &B,
570 logical_page_size: NonZeroU16,
571 offset: u64,
572 len: usize,
573 read_options: ReadOptions,
574 ) -> Result<IoBufs, Error> {
575 let len_u64: u64 = Widen::widen(len);
577 let end = offset.checked_add(len_u64).ok_or(Error::OffsetOverflow)?;
578 if len == 0 {
579 return Ok(IoBufs::default());
580 }
581
582 let logical_page_size: u64 = logical_page_size.widen();
584 let first_page = offset / logical_page_size;
585 let last_page = (end - 1) / logical_page_size;
586 let mut out = IoBufs::default();
587
588 for page in first_page..=last_page {
590 let (logical, page_start, page_end) =
591 Self::read_page(blob, page, logical_page_size, read_options).await?;
592 let overlap_start = offset.max(page_start);
593 let overlap_end = end.min(page_end);
594 if overlap_start >= overlap_end {
595 return Err(Error::BlobInsufficientLength);
596 }
597 let start = (overlap_start - page_start) as usize;
598 let end = (overlap_end - page_start) as usize;
599 out.append(logical.slice(start..end));
600 }
601
602 if out.len() != len {
604 return Err(Error::BlobInsufficientLength);
605 }
606 Ok(out)
607 }
608
609 pub async fn read_tail(
615 blob: &B,
616 physical_size: u64,
617 logical_page_size: NonZeroU16,
618 len: usize,
619 read_options: ReadOptions,
620 ) -> Result<(u64, IoBufs), Error> {
621 if physical_size == 0 {
622 return if len == 0 {
623 Ok((0, IoBufs::default()))
624 } else {
625 Err(Error::BlobInsufficientLength)
626 };
627 }
628
629 let page_size: u64 = logical_page_size.widen();
632 let physical_page_size = page_size
633 .checked_add(CHECKSUM_SIZE)
634 .ok_or(Error::OffsetOverflow)?;
635 if !physical_size.is_multiple_of(physical_page_size) {
636 return Err(Error::BlobInsufficientLength);
637 }
638
639 let page = physical_size / physical_page_size - 1;
641 let (tail, page_start, logical_size) =
642 Self::read_page(blob, page, page_size, read_options).await?;
643 let len_u64: u64 = Widen::widen(len);
644 let offset = logical_size
645 .checked_sub(len_u64)
646 .ok_or(Error::BlobInsufficientLength)?;
647
648 let mut out = if offset < page_start {
651 let prefix_len =
652 usize::try_from(page_start - offset).map_err(|_| Error::OffsetOverflow)?;
653 Self::read_range(blob, logical_page_size, offset, prefix_len, read_options).await?
654 } else {
655 IoBufs::default()
656 };
657 let tail_start = usize::try_from(offset.max(page_start) - page_start)
658 .map_err(|_| Error::OffsetOverflow)?;
659 out.append(tail.slice(tail_start..));
660 assert_eq!(out.len(), len);
661 Ok((logical_size, out))
662 }
663
664 fn sealed_handle(&self, id: u64) -> Sealed<B> {
666 let page_size: u64 = self.cache_ref.page_size().widen();
667 let full_pages = self.current_page;
668 assert_eq!(
669 full_pages.checked_mul(page_size),
670 Some(self.buffer.offset),
671 "flushed page count is inconsistent with the buffer offset"
672 );
673
674 let partial_page = if self.buffer.is_empty() {
676 None
677 } else {
678 Some(IoBuf::copy_from_slice(self.buffer.partial()))
679 };
680 Sealed::new(
681 self.blob.clone(),
682 self.buffer.size(),
683 partial_page,
684 self.cache_ref.clone(),
685 id,
686 )
687 }
688
689 pub async fn seal(mut self) -> Result<(Sealed<B>, Handle<()>), Error> {
695 self.sync_state.wait_for_pending().await?;
697 self.flush_internal(true, false).await?;
698
699 let handle = self.sync_state.start_sync(&self.blob).await;
701 Ok((self.sealed_handle(self.id), handle))
702 }
703}
704
705impl<B: Blob, Phase> Writer<B, Phase> {
706 pub async fn append(mut self, buf: &[u8]) -> Result<(Self, u64), Error> {
709 let page_size: usize = self.cache_ref.page_size().widen();
710
711 if too_big_for_buffer(
713 self.buffer.len(),
714 self.buffer.capacity,
715 buf.len(),
716 page_size,
717 ) {
718 return self.append_owned(IoBuf::copy_from_slice(buf)).await;
719 }
720
721 let offset = self.buffer.size();
722 if self.buffer.append(buf) {
723 self.flush_internal(false, false).await?;
724 }
725 Ok((self, offset))
726 }
727
728 pub fn try_append(&mut self, buf: &[u8]) -> Option<u64> {
734 self.buffer.try_append(buf)
735 }
736
737 pub fn try_append_value<T: EncodeSize + Write>(&mut self, value: &T) -> Option<u64> {
747 self.buffer.try_append_value(value)
748 }
749
750 pub async fn append_owned(mut self, buf: IoBuf) -> Result<(Self, u64), Error> {
756 let page_size: usize = self.cache_ref.page_size().widen();
757 let offset = self.buffer.size();
758
759 if !too_big_for_buffer(
761 self.buffer.len(),
762 self.buffer.capacity,
763 buf.len(),
764 page_size,
765 ) {
766 if self.buffer.append(buf.as_ref()) {
767 self.flush_internal(false, false).await?;
768 }
769 return Ok((self, offset));
770 }
771
772 let fill = self.buffer.len().next_multiple_of(page_size) - self.buffer.len();
774
775 if fill > 0 {
778 self.buffer.append(&buf.as_ref()[..fill]);
779 }
780 let boundary = self.buffer.size();
781 if !self.buffer.is_empty() {
782 self.flush_internal(false, false).await?;
783 assert!(
784 self.buffer.size() == boundary && self.buffer.is_empty(),
785 "flush left unexpected buffered bytes before a direct-path append"
786 );
787 }
788
789 let bulk_len = (buf.len() - fill) / page_size * page_size;
792 let bulk = buf.slice(fill..fill + bulk_len);
793 let mut physical_pages = IoBufs::default();
794 self.append_full_pages(std::iter::once(&bulk), None, &mut physical_pages);
795
796 assert!(
797 self.partial_page_state.is_none(),
798 "an empty tip implies no partial page state"
799 );
800
801 self.sync_state.wait_for_pending().await?;
803
804 let chunk_len = self.buffer.capacity;
809 let mut cache_offset = boundary;
810 for chunk in bulk.as_ref().chunks(chunk_len) {
811 let remaining = self.cache_ref.cache(self.id, chunk, cache_offset);
812 assert_eq!(remaining, 0, "cached bulk pages must be page-aligned");
813 cache_offset += chunk.len() as u64;
814 }
815
816 self.current_page += (bulk_len / page_size) as u64;
819 self.buffer
820 .replace(boundary + bulk_len as u64, &buf.as_ref()[fill + bulk_len..]);
821
822 let page_size: u64 = self.cache_ref.page_size().widen();
824 assert_eq!(self.current_page * page_size, self.buffer.offset);
825
826 let physical_page_size = page_size + CHECKSUM_SIZE;
827 let write_at_offset = boundary / page_size * physical_page_size;
828 self.sync_state
829 .write_at(
830 &self.blob,
831 write_at_offset,
832 physical_pages,
833 WriteOptions::DONT_CACHE,
834 )
835 .await?;
836
837 Ok((self, offset))
838 }
839
840 fn partial_page_dirty(
845 full_pages: usize,
846 partial_len: usize,
847 flushed: Option<&ActiveChecksum>,
848 ) -> bool {
849 partial_len != 0
850 && (full_pages > 0 || flushed.is_none_or(|state| state.len as usize != partial_len))
851 }
852
853 fn has_flush_work(&self, write_partial_page: bool) -> bool {
855 let page_size: usize = self.cache_ref.page_size().widen();
856 let full_pages = self.buffer.len() / page_size;
857 if full_pages > 0 {
858 return true;
859 }
860 write_partial_page
861 && Self::partial_page_dirty(
862 full_pages,
863 self.buffer.len() % page_size,
864 self.partial_page_state.as_ref(),
865 )
866 }
867
868 async fn flush_internal(
882 &mut self,
883 write_partial_page: bool,
884 sync: bool,
885 ) -> Result<bool, Error> {
886 if !self.has_flush_work(write_partial_page) {
889 return Ok(false);
890 }
891
892 self.sync_state.wait_for_pending().await?;
896 if self.sync_state.is_clean() {
897 self.durable_page_state = self.partial_page_state;
898 }
899
900 let page_size: usize = self.cache_ref.page_size().widen();
903 let pages_to_cache = self.buffer.len() / page_size;
904 let cache_offset = self.buffer.offset;
905 let full_pages = self.buffer.drain_full_pages();
906 let new_offset = self.buffer.offset;
907
908 let (physical_pages, partial_page_state) = self.to_physical_pages(
911 &full_pages,
912 self.buffer.partial(),
913 write_partial_page,
914 self.partial_page_state.as_ref(),
915 self.durable_page_state.as_ref(),
916 );
917 assert!(
918 !physical_pages.is_empty(),
919 "flush work predicate must match physical page construction"
920 );
921
922 if pages_to_cache > 0 {
925 let cached = self.cache_ref.cache_pages(
926 self.id,
927 full_pages
928 .iter()
929 .flat_map(|chunk| chunk.as_ref().chunks_exact(page_size)),
930 cache_offset,
931 );
932 assert_eq!(cached, pages_to_cache * page_size);
933 }
934
935 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
936 let write_at_offset = self.current_page * physical_page_size as u64;
937
938 self.current_page += pages_to_cache as u64;
941 self.partial_page_state = partial_page_state;
942
943 if pages_to_cache > 0 {
945 self.durable_page_state = None;
946 }
947
948 let page_size: u64 = self.cache_ref.page_size().widen();
950 assert_eq!(self.current_page * page_size, new_offset);
951
952 if sync {
955 self.sync_state
957 .write_at(
958 &self.blob,
959 write_at_offset,
960 physical_pages,
961 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
962 )
963 .await?;
964 self.durable_page_state = partial_page_state;
965 } else {
966 self.sync_state
968 .write_at(
969 &self.blob,
970 write_at_offset,
971 physical_pages,
972 WriteOptions::DONT_CACHE,
973 )
974 .await?;
975 }
976 Ok(sync)
977 }
978
979 pub const fn size(&self) -> u64 {
981 self.buffer.size()
982 }
983
984 pub fn needs_sync(&self) -> bool {
986 !self.sync_state.is_clean() || self.has_flush_work(true)
987 }
988
989 const fn view(&self) -> View<'_, B> {
991 View {
992 blob: &self.blob,
993 cache_ref: &self.cache_ref,
994 id: self.id,
995 size: self.buffer.size(),
996 tail_offset: self.buffer.offset,
997 tail: Tail::Buffered(&self.buffer),
998 }
999 }
1000
1001 pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
1005 self.view().try_read_sync_into(buf, offset)
1006 }
1007
1008 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
1010 self.view().read_at(offset, len).await
1011 }
1012
1013 pub async fn read_up_to(
1018 &self,
1019 offset: u64,
1020 len: usize,
1021 bufs: impl Into<IoBufMut> + Send,
1022 ) -> Result<(IoBufMut, usize), Error> {
1023 self.view().read_up_to(offset, len, bufs).await
1024 }
1025
1026 pub async fn read_many_into(
1034 &self,
1035 buf: &mut [u8],
1036 offsets: &[u64],
1037 item_size: NonZeroUsize,
1038 ) -> Result<usize, Error> {
1039 self.view().read_many_into(buf, offsets, item_size).await
1040 }
1041
1042 pub async fn read_ranges_into(
1053 &self,
1054 buf: &mut [u8],
1055 ranges: &[(u64, usize)],
1056 ) -> Result<usize, Error> {
1057 self.view().read_ranges_into(buf, ranges).await
1058 }
1059
1060 pub fn try_read_many_sync_into(
1063 &self,
1064 buf: &mut [u8],
1065 offsets: &[u64],
1066 item_size: NonZeroUsize,
1067 ) -> Vec<usize> {
1068 self.view().try_read_many_sync_into(buf, offsets, item_size)
1069 }
1070
1071 pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
1074 self.view().try_read_ranges_sync_into(buf, ranges)
1075 }
1076
1077 pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
1079 self.view().read_into(buf, offset).await
1080 }
1081
1082 fn to_physical_pages(
1101 &self,
1102 full_pages: &IoBufs,
1103 partial_page: &[u8],
1104 include_partial_page: bool,
1105 flushed: Option<&ActiveChecksum>,
1106 durable: Option<&ActiveChecksum>,
1107 ) -> (IoBufs, Option<ActiveChecksum>) {
1108 let page_size: usize = self.cache_ref.page_size().widen();
1109 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
1110 let pages_to_write = full_pages.len() / page_size;
1111 let mut write_buffer = IoBufs::default();
1112
1113 if pages_to_write > 0 {
1114 self.append_full_pages(full_pages.iter(), durable, &mut write_buffer);
1115 }
1116
1117 if !include_partial_page {
1118 return (write_buffer, None);
1119 }
1120
1121 if !Self::partial_page_dirty(pages_to_write, partial_page.len(), flushed) {
1122 return (write_buffer, None);
1123 }
1124 let partial_len = partial_page.len();
1125 let crc = Crc32::checksum(partial_page);
1126
1127 let durable = if pages_to_write == 0 { durable } else { None };
1130 let (crc_record, active_checksum) =
1131 Self::build_crc_record(partial_len as u16, crc, durable);
1132
1133 let mut padded = IoBufMut::with_capacity(physical_page_size);
1136 padded.put_slice(partial_page);
1137 let zero_count = page_size - partial_len;
1138 if zero_count > 0 {
1139 padded.put_bytes(0, zero_count);
1140 }
1141 padded.put_slice(&crc_record.to_bytes());
1142 write_buffer.append(padded.freeze());
1143
1144 (write_buffer, Some(active_checksum))
1145 }
1146
1147 fn append_full_pages<'a>(
1150 &self,
1151 data: impl Iterator<Item = &'a IoBuf> + Clone,
1152 old_checksum: Option<&ActiveChecksum>,
1153 write_buffer: &mut IoBufs,
1154 ) {
1155 let page_size: usize = self.cache_ref.page_size().widen();
1156 let pages = data
1157 .clone()
1158 .map(|chunk| {
1159 assert!(chunk.len().is_multiple_of(page_size));
1160 chunk.len() / page_size
1161 })
1162 .sum::<usize>();
1163 assert!(pages > 0);
1164 let page_size_u16 =
1165 u16::try_from(page_size).expect("page size must fit in u16 for CRC record");
1166
1167 let mut crcs = IoBufMut::with_capacity(CHECKSUM_SIZE as usize * pages);
1169 for (page, logical_page) in data
1170 .clone()
1171 .flat_map(|chunk| chunk.as_ref().chunks_exact(page_size))
1172 .enumerate()
1173 {
1174 let crc = Crc32::checksum(logical_page);
1175 let old_checksum = if page == 0 { old_checksum } else { None };
1176 let (crc_record, _) = Self::build_crc_record(page_size_u16, crc, old_checksum);
1177 crcs.put_slice(&crc_record.to_bytes());
1178 }
1179 let crc_blob = crcs.freeze();
1180
1181 let mut crc_start = 0;
1184 for chunk in data {
1185 for start in (0..chunk.len()).step_by(page_size) {
1186 write_buffer.append(chunk.slice(start..start + page_size));
1187 write_buffer.append(crc_blob.slice(crc_start..crc_start + CHECKSUM_SIZE as usize));
1188 crc_start += CHECKSUM_SIZE as usize;
1189 }
1190 }
1191 }
1192
1193 const fn build_crc_record(
1196 new_len: u16,
1197 new_crc: u32,
1198 old_checksum: Option<&ActiveChecksum>,
1199 ) -> (Checksum, ActiveChecksum) {
1200 let Some(old_checksum) = old_checksum else {
1201 return (
1202 Checksum::new(new_len, new_crc),
1203 ActiveChecksum::new(Slot::First, new_len, new_crc),
1204 );
1205 };
1206
1207 let new_slot = old_checksum.slot.other();
1208 let record = match old_checksum.slot {
1209 Slot::First => Checksum {
1210 len1: old_checksum.len,
1211 crc1: old_checksum.crc,
1212 len2: new_len,
1213 crc2: new_crc,
1214 },
1215 Slot::Second => Checksum {
1216 len1: new_len,
1217 crc1: new_crc,
1218 len2: old_checksum.len,
1219 crc2: old_checksum.crc,
1220 },
1221 };
1222 (record, ActiveChecksum::new(new_slot, new_len, new_crc))
1223 }
1224
1225 pub async fn replay(
1238 mut self,
1239 buffer_size: NonZeroUsize,
1240 read_options: ReadOptions,
1241 ) -> Result<(Self, Replay<B>), Error> {
1242 self.flush_internal(true, false).await?;
1245 let page_size = self.cache_ref.page_size();
1246 let logical_size = self.size();
1247 let physical_page_size = u64::from(page_size.get()) + CHECKSUM_SIZE;
1248 let pages = logical_size.div_ceil(u64::from(page_size.get()));
1249 let physical_size = pages
1250 .checked_mul(physical_page_size)
1251 .ok_or(Error::OffsetOverflow)?;
1252 let buffer_pages = (buffer_size.get() / physical_page_size as usize).max(1);
1253 let partial_page = (logical_size > self.buffer.offset)
1254 .then(|| IoBuf::copy_from_slice(self.buffer.partial()));
1255 let replay = Replay::new(PageReader::new(
1256 self.blob.clone(),
1257 physical_size,
1258 logical_size,
1259 partial_page,
1260 buffer_pages,
1261 page_size,
1262 read_options,
1263 ));
1264 Ok((self, replay))
1265 }
1266
1267 pub async fn sync(mut self) -> Result<Self, Error> {
1272 if self.flush_internal(true, true).await? {
1275 return Ok(self);
1276 }
1277
1278 self.sync_state.sync(&self.blob).await?;
1281 self.durable_page_state = self.partial_page_state;
1282 Ok(self)
1283 }
1284
1285 pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
1294 self.flush_internal(true, false).await?;
1296 let handle = self.sync_state.start_sync(&self.blob).await;
1297 Ok((self, handle))
1298 }
1299
1300 pub async fn recoverable_prefix_len(
1316 &self,
1317 proven: u64,
1318 buffer_size: NonZeroUsize,
1319 read_options: ReadOptions,
1320 ) -> Result<u64, Error> {
1321 self.recoverable_prefix_len_at_most(proven, u64::MAX, buffer_size, read_options)
1322 .await
1323 }
1324
1325 pub async fn recoverable_prefix_len_at_most(
1331 &self,
1332 proven: u64,
1333 max_size: u64,
1334 buffer_size: NonZeroUsize,
1335 read_options: ReadOptions,
1336 ) -> Result<u64, Error> {
1337 let logical_page_size: u64 = self.cache_ref.page_size().widen();
1338 let total_pages = (self.current_page + u64::from(self.partial_page_state.is_some()))
1339 .min(max_size.div_ceil(logical_page_size));
1340 let physical_page_size = logical_page_size
1341 .checked_add(CHECKSUM_SIZE)
1342 .ok_or(Error::OffsetOverflow)?;
1343 let physical_page_size_usize =
1344 usize::try_from(physical_page_size).map_err(|_| Error::OffsetOverflow)?;
1345 let max_batch_pages: u64 =
1346 Widen::widen((buffer_size.get() / physical_page_size_usize).max(1));
1347
1348 let start_page = (proven / logical_page_size)
1352 .min(self.current_page)
1353 .min(total_pages);
1354 let mut valid_len = start_page
1355 .checked_mul(logical_page_size)
1356 .ok_or(Error::OffsetOverflow)?;
1357 let mut page = start_page;
1358 while page < total_pages {
1359 let batch_pages = max_batch_pages.min(total_pages - page);
1361 let batch_end = page.checked_add(batch_pages).ok_or(Error::OffsetOverflow)?;
1362 let physical_offset = page
1363 .checked_mul(physical_page_size)
1364 .ok_or(Error::OffsetOverflow)?;
1365 let physical_len = batch_pages
1366 .checked_mul(physical_page_size)
1367 .ok_or(Error::OffsetOverflow)?;
1368 let physical_len = usize::try_from(physical_len).map_err(|_| Error::OffsetOverflow)?;
1369
1370 let physical = self
1372 .blob
1373 .read_at(physical_offset, physical_len, read_options)
1374 .await?
1375 .coalesce();
1376
1377 for physical_page in physical.as_ref().chunks_exact(physical_page_size_usize) {
1379 let Some(checksum) = Checksum::validate_page(physical_page) else {
1380 return Ok(valid_len.min(max_size));
1381 };
1382 let len = u64::from(checksum.len);
1383 valid_len = valid_len.checked_add(len).ok_or(Error::OffsetOverflow)?;
1384
1385 if len < logical_page_size {
1387 return Ok(valid_len.min(max_size));
1388 }
1389 }
1390 page = batch_end;
1391 }
1392 Ok(valid_len.min(max_size))
1393 }
1394
1395 pub async fn wait_for_sync(mut self) -> Result<Self, Error> {
1397 self.sync_state.wait_for_pending().await?;
1398 Ok(self)
1399 }
1400}
1401
1402#[cfg(test)]
1403#[commonware_macros::test_group("miri")]
1404mod tests {
1405 use super::*;
1406 use crate::{
1407 Buf, Handle, IoBufsMut, Runner as _, Spawner as _, Storage as _, Supervisor as _,
1408 buffer::{paged::CHECKSUM_SLOT_SIZE, tests::SyncTrackingBlob},
1409 deterministic,
1410 mocks::{
1411 DelayedSyncBlob, RecordingContext, WriteFaultContext, WriteFaults, next_pending_sync,
1412 },
1413 };
1414 use commonware_codec::{Copying, FixedSize, ReadExt};
1415 use commonware_macros::test_traced;
1416 use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
1417 use futures::FutureExt as _;
1418 use std::{
1419 cell::Cell,
1420 num::NonZeroU16,
1421 sync::{
1422 Arc,
1423 atomic::{AtomicUsize, Ordering},
1424 },
1425 };
1426
1427 impl<B: Blob, Phase> Writer<B, Phase> {
1428 pub(in super::super) const fn cache_id(&self) -> u64 {
1430 self.id
1431 }
1432 }
1433
1434 const PAGE_SIZE: NonZeroU16 = NZU16!(103); const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
1436
1437 #[test_traced]
1440 fn test_try_append_capacity() {
1441 deterministic::Runner::default().start(|context| async move {
1442 let (blob, size) = context.open("try_append", b"blob").await.unwrap();
1443 let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
1444 let mut writer = Writer::new(blob, size, 32, cache).await.unwrap();
1445
1446 assert_eq!(writer.try_append(&[1; 20]), Some(0));
1448 assert_eq!(writer.try_append(&[2; 12]), Some(20));
1449 assert_eq!(writer.size(), 32);
1450
1451 assert_eq!(writer.try_append(&[3]), None);
1453 assert_eq!(writer.size(), 32);
1454
1455 let (writer, offset) = writer.append(&[3]).await.unwrap();
1457 assert_eq!(offset, 32);
1458 let read = writer.read_at(0, 33).await.unwrap().coalesce();
1459 assert_eq!(&read.as_ref()[..20], &[1; 20]);
1460 assert_eq!(&read.as_ref()[20..32], &[2; 12]);
1461 assert_eq!(read.as_ref()[32], 3);
1462 });
1463 }
1464
1465 #[test_traced]
1466 fn test_append_value_capacity_snapshot_and_recovery() {
1467 deterministic::Runner::default().start(|context| async move {
1468 struct MustNotEncode<const SIZE: usize>;
1469 impl<const SIZE: usize> FixedSize for MustNotEncode<SIZE> {
1470 const SIZE: usize = SIZE;
1471 }
1472 impl<const SIZE: usize> Write for MustNotEncode<SIZE> {
1473 fn write(&self, _: &mut impl BufMut) {
1474 panic!("insufficient capacity must not invoke the encoder");
1475 }
1476 }
1477
1478 let (blob, size) = context.open("append_value", b"blob").await.unwrap();
1479 let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
1480 let mut writer = Writer::new(blob, size, 32, cache.clone()).await.unwrap();
1481 assert_eq!(writer.try_append_value(&0u64), Some(0));
1482 assert_eq!(writer.try_append_value(&MustNotEncode::<25>), None);
1483 let (mut writer, snapshot) = writer.snapshot().await.unwrap();
1484 for i in 1..4u64 {
1485 assert_eq!(writer.try_append_value(&i), Some(i * 8));
1486 }
1487
1488 assert_eq!(writer.try_append_value(&MustNotEncode::<8>), None);
1489 assert_eq!(writer.size(), 32);
1490 assert_eq!(
1491 snapshot.read_at(0, 8).await.unwrap().coalesce().as_ref(),
1492 &0u64.to_be_bytes()
1493 );
1494
1495 let (writer, offset) = writer.append(&4u64.to_be_bytes()).await.unwrap();
1497 assert_eq!(offset, 32);
1498 let writer = writer.sync().await.unwrap();
1499
1500 drop(snapshot);
1503 drop(writer);
1504 let (blob, size) = context.open("append_value", b"blob").await.unwrap();
1505 let writer = Writer::new(blob, size, 32, cache).await.unwrap();
1506 assert_eq!(writer.size(), 40);
1507 let expected: Vec<_> = (0..5u64).flat_map(u64::to_be_bytes).collect();
1508 assert_eq!(
1509 writer.read_at(0, 40).await.unwrap().coalesce().as_ref(),
1510 expected
1511 );
1512 });
1513 }
1514
1515 #[test_traced]
1516 fn test_append_variable_value_encodes_in_place() {
1517 struct Value {
1519 size: usize,
1520 size_calls: Cell<usize>,
1521 writes: Cell<usize>,
1522 destination: Cell<usize>,
1523 }
1524
1525 impl EncodeSize for Value {
1526 fn encode_size(&self) -> usize {
1527 self.size_calls.set(self.size_calls.get() + 1);
1528 self.size
1529 }
1530 }
1531
1532 impl Write for Value {
1533 fn write(&self, buf: &mut impl BufMut) {
1534 self.writes.set(self.writes.get() + 1);
1535 if self.size > 0 {
1536 self.destination.set(buf.chunk_mut().as_mut_ptr() as usize);
1537 }
1538 buf.put_bytes(7, self.size);
1539 }
1540 }
1541
1542 deterministic::Runner::default().start(|context| async move {
1543 let (blob, size) = context.open("variable_value", b"blob").await.unwrap();
1544 let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
1545 let mut writer = Writer::new(blob, size, 64, cache.clone()).await.unwrap();
1546 let mut value = Value {
1547 size: 3,
1548 size_calls: Cell::new(0),
1549 writes: Cell::new(0),
1550 destination: Cell::new(0),
1551 };
1552
1553 assert_eq!(writer.try_append_value(&value), Some(0));
1556 let tail = writer.buffer.parts().1.as_ptr() as usize;
1557 assert_eq!(value.destination.get(), tail);
1558 value.size = 5;
1559 assert_eq!(writer.try_append_value(&value), Some(3));
1560 assert_eq!(value.destination.get(), tail + 3);
1561 value.size = 17;
1562 assert_eq!(writer.try_append_value(&value), Some(8));
1563 assert_eq!(value.destination.get(), tail + 8);
1564 assert_eq!(
1565 writer.buffer.parts().0.chunk_at(0).unwrap().as_ptr() as usize,
1566 tail
1567 );
1568
1569 value.size = 0;
1572 assert_eq!(writer.try_append_value(&value), Some(25));
1573 value.size = 39;
1574 assert_eq!(writer.try_append_value(&value), Some(25));
1575 for size in [1, usize::MAX] {
1576 value.size = size;
1577 assert_eq!(writer.try_append_value(&value), None);
1578 }
1579 value.size = 0;
1580 assert_eq!(writer.try_append_value(&value), Some(64));
1581 assert_eq!(value.size_calls.get(), 8);
1582 assert_eq!(value.writes.get(), 6);
1583 assert_eq!(writer.size(), 64);
1584
1585 writer = writer.sync().await.unwrap();
1587 drop(writer);
1588 let (blob, size) = context.open("variable_value", b"blob").await.unwrap();
1589 let writer = Writer::new(blob, size, 64, cache).await.unwrap();
1590 assert_eq!(writer.size(), 64);
1591 assert_eq!(
1592 writer.read_at(0, 64).await.unwrap().coalesce().as_ref(),
1593 &[7; 64]
1594 );
1595 });
1596 }
1597
1598 #[test_traced("DEBUG")]
1599 fn test_writes_use_uncached_hint() {
1600 let executor = deterministic::Runner::default();
1601 executor.start(|_: deterministic::Context| async move {
1602 let blob = SyncTrackingBlob::new();
1603 let blob = Arc::new(blob);
1604 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1605 let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
1606 .await
1607 .unwrap();
1608 writer = writer.sync().await.unwrap();
1609
1610 (writer, _) = writer.append(b"first").await.unwrap();
1611 writer = writer.sync().await.unwrap();
1612 assert_eq!(blob.uncached_snapshot(), (0, 1));
1613
1614 (writer, _) = writer.append(b"second").await.unwrap();
1615 let (_, sync) = writer.seal().await.unwrap();
1616 sync.await.unwrap();
1617 assert_eq!(blob.uncached_snapshot(), (1, 1));
1618 });
1619 }
1620
1621 #[test_traced("DEBUG")]
1626 fn test_unsynced_flushes_preserve_durable_checksum_slot() {
1627 let executor = deterministic::Runner::default();
1628 executor.start(|context: deterministic::Context| async move {
1629 let page_size = PAGE_SIZE.get() as usize;
1630 let physical_page = page_size + CHECKSUM_SIZE as usize;
1631 let (blob, blob_size) = context
1632 .open("test_partition", b"replay_torn")
1633 .await
1634 .unwrap();
1635 let blob = Arc::new(blob);
1636 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1637 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref.clone())
1638 .await
1639 .unwrap();
1640
1641 let synced: Vec<u8> = (1u8..=24).collect();
1643 (writer, _) = writer.append(&synced).await.unwrap();
1644 writer = writer.sync().await.unwrap();
1645 let durable_image = blob
1646 .read_at(0, physical_page, ReadOptions::default())
1647 .await
1648 .unwrap()
1649 .coalesce();
1650
1651 (writer, _) = writer.append(&[25u8; 8]).await.unwrap();
1653 (writer, _) = writer
1654 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1655 .await
1656 .unwrap();
1657 (writer, _) = writer.append(&[26u8; 8]).await.unwrap();
1658 writer
1659 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1660 .await
1661 .unwrap();
1662 let torn_image = blob
1663 .read_at(0, physical_page, ReadOptions::default())
1664 .await
1665 .unwrap()
1666 .coalesce();
1667
1668 let mut crash = durable_image.as_ref().to_vec();
1671 crash[page_size..].copy_from_slice(&torn_image.as_ref()[page_size..]);
1672 let (crashed, _) = context
1673 .open("test_partition", b"replay_crash")
1674 .await
1675 .unwrap();
1676 crashed
1677 .write_at(0, crash, WriteOptions::default())
1678 .await
1679 .unwrap();
1680 crashed.sync().await.unwrap();
1681
1682 let recovered = Writer::new(crashed, physical_page as u64, BUFFER_SIZE, cache_ref)
1684 .await
1685 .unwrap();
1686 assert_eq!(recovered.size(), synced.len() as u64);
1687 let read = recovered.read_at(0, synced.len()).await.unwrap().coalesce();
1688 assert_eq!(read.as_ref(), synced.as_slice());
1689 });
1690 }
1691
1692 #[test_traced("DEBUG")]
1694 fn test_recoverable_prefix_len_clean() {
1695 let executor = deterministic::Runner::default();
1696 executor.start(|context: deterministic::Context| async move {
1697 let (context, recordings) = RecordingContext::new(context);
1698 let (blob, blob_size) = context
1699 .open("test_partition", b"prefix_clean")
1700 .await
1701 .unwrap();
1702 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1703 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1704 .await
1705 .unwrap();
1706 assert_eq!(
1707 writer
1708 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1709 .await
1710 .unwrap(),
1711 0
1712 );
1713
1714 let total = PAGE_SIZE.get() as usize * 2 + 50;
1715 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1716 (writer, _) = writer.append(&data).await.unwrap();
1717 writer = writer.sync().await.unwrap();
1718
1719 recordings.clear();
1721 assert_eq!(
1722 writer
1723 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1724 .await
1725 .unwrap(),
1726 total as u64
1727 );
1728 let reads = recordings.snapshot().reads;
1729 assert!(!reads.is_empty());
1730 assert!(
1731 reads
1732 .iter()
1733 .all(|options| *options == ReadOptions::default())
1734 );
1735
1736 drop(writer);
1737 let (blob, blob_size) = context
1738 .open("test_partition", b"prefix_clean")
1739 .await
1740 .unwrap();
1741
1742 recordings.clear();
1744 let _recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1745 .await
1746 .unwrap();
1747 let reads = recordings.snapshot().reads;
1748 assert!(!reads.is_empty());
1749 assert!(
1750 reads
1751 .iter()
1752 .all(|options| *options == ReadOptions::default())
1753 );
1754 });
1755 }
1756
1757 #[test_traced("DEBUG")]
1760 fn test_recoverable_prefix_len_torn_interior_page() {
1761 let executor = deterministic::Runner::default();
1762 executor.start(|context: deterministic::Context| async move {
1763 let (blob, blob_size) = context
1764 .open("test_partition", b"prefix_torn")
1765 .await
1766 .unwrap();
1767 let blob = Arc::new(blob);
1768 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1769 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1770 .await
1771 .unwrap();
1772 let total = PAGE_SIZE.get() as usize * 3 + 10;
1773 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1774 (writer, _) = writer.append(&data).await.unwrap();
1775 writer = writer.sync().await.unwrap();
1776
1777 let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
1779 let offset = physical_page_size + 7;
1780 let byte = blob
1781 .read_at(offset, 1, ReadOptions::default())
1782 .await
1783 .unwrap()
1784 .coalesce();
1785 blob.write_at(
1786 offset,
1787 vec![byte.as_ref()[0] ^ 0xFF],
1788 WriteOptions::default(),
1789 )
1790 .await
1791 .unwrap();
1792 blob.sync().await.unwrap();
1793
1794 assert_eq!(
1795 writer
1796 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1797 .await
1798 .unwrap(),
1799 PAGE_SIZE.get() as u64
1800 );
1801 });
1802 }
1803
1804 #[test_traced("DEBUG")]
1807 fn test_recoverable_prefix_len_proven_prefix() {
1808 let executor = deterministic::Runner::default();
1809 executor.start(|context: deterministic::Context| async move {
1810 let (blob, blob_size) = context
1811 .open("test_partition", b"prefix_proven")
1812 .await
1813 .unwrap();
1814 let blob = Arc::new(blob);
1815 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1816 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1817 .await
1818 .unwrap();
1819 let total = PAGE_SIZE.get() as usize * 4;
1820 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1821 (writer, _) = writer.append(&data).await.unwrap();
1822 writer = writer.sync().await.unwrap();
1823
1824 let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
1826 let offset = physical_page_size + 7;
1827 let byte = blob
1828 .read_at(offset, 1, ReadOptions::default())
1829 .await
1830 .unwrap()
1831 .coalesce();
1832 blob.write_at(
1833 offset,
1834 vec![byte.as_ref()[0] ^ 0xFF],
1835 WriteOptions::default(),
1836 )
1837 .await
1838 .unwrap();
1839 blob.sync().await.unwrap();
1840
1841 let page = u64::from(PAGE_SIZE.get());
1844 for (proven, expected) in [
1845 (0, page),
1846 (page, page),
1847 (2 * page, 4 * page),
1848 (2 * page + 3, 4 * page),
1849 (total as u64, 4 * page),
1850 ] {
1851 assert_eq!(
1852 writer
1853 .recoverable_prefix_len(
1854 proven,
1855 NZUsize!(BUFFER_SIZE),
1856 ReadOptions::default()
1857 )
1858 .await
1859 .unwrap(),
1860 expected,
1861 "proven {proven}"
1862 );
1863 }
1864
1865 assert_eq!(
1868 writer
1869 .recoverable_prefix_len(
1870 100 * page,
1871 NZUsize!(BUFFER_SIZE),
1872 ReadOptions::default()
1873 )
1874 .await
1875 .unwrap(),
1876 4 * page
1877 );
1878
1879 (writer, _) = writer.append(&data[..20]).await.unwrap();
1882 writer = writer.sync().await.unwrap();
1883 for proven in [4 * page + 20, 5 * page, 100 * page] {
1884 assert_eq!(
1885 writer
1886 .recoverable_prefix_len(
1887 proven,
1888 NZUsize!(BUFFER_SIZE),
1889 ReadOptions::default()
1890 )
1891 .await
1892 .unwrap(),
1893 4 * page + 20,
1894 "proven {proven}"
1895 );
1896 }
1897 });
1898 }
1899
1900 #[test_traced("DEBUG")]
1903 fn test_recoverable_prefix_len_stale_short_interior_page() {
1904 let executor = deterministic::Runner::default();
1905 executor.start(|context: deterministic::Context| async move {
1906 let (blob, blob_size) = context
1907 .open("test_partition", b"prefix_stale")
1908 .await
1909 .unwrap();
1910 let blob = Arc::new(blob);
1911 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1912 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1913 .await
1914 .unwrap();
1915
1916 let total = PAGE_SIZE.get() as usize * 2;
1918 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1919 (writer, _) = writer.append(&data[..20]).await.unwrap();
1920 writer = writer.sync().await.unwrap();
1921 let physical_page_size = (PAGE_SIZE.get() as u64 + CHECKSUM_SIZE) as usize;
1922 let stale = blob
1923 .read_at(0, physical_page_size, ReadOptions::default())
1924 .await
1925 .unwrap()
1926 .coalesce();
1927 let stale = stale.as_ref().to_vec();
1928
1929 (writer, _) = writer.append(&data[20..]).await.unwrap();
1932 writer = writer.sync().await.unwrap();
1933 blob.write_at(0, stale, WriteOptions::default())
1934 .await
1935 .unwrap();
1936 blob.sync().await.unwrap();
1937
1938 assert_eq!(
1939 writer
1940 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1941 .await
1942 .unwrap(),
1943 20
1944 );
1945 });
1946 }
1947
1948 #[test_traced("DEBUG")]
1951 fn test_recoverable_prefix_len_batches_reads() {
1952 let executor = deterministic::Runner::default();
1953 executor.start(|context: deterministic::Context| async move {
1954 let (context, recordings) = RecordingContext::new(context);
1955 let (blob, blob_size) = context
1956 .open("test_partition", b"prefix_batches")
1957 .await
1958 .unwrap();
1959 let blob = Arc::new(blob);
1960 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1961 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1962 .await
1963 .unwrap();
1964
1965 let total = PAGE_SIZE.get() as usize * 8 + 10;
1967 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1968 (writer, _) = writer.append(&data).await.unwrap();
1969 writer = writer.sync().await.unwrap();
1970
1971 recordings.clear();
1973 assert_eq!(
1974 writer
1975 .recoverable_prefix_len(0, NZUsize!(1), ReadOptions::default())
1976 .await
1977 .unwrap(),
1978 total as u64
1979 );
1980 assert_eq!(recordings.snapshot().reads.len(), 9);
1981
1982 recordings.clear();
1984 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
1985 let scan_buffer = NonZeroUsize::new(physical_page_size * 3).unwrap();
1986 assert_eq!(
1987 writer
1988 .recoverable_prefix_len(0, scan_buffer, ReadOptions::default())
1989 .await
1990 .unwrap(),
1991 total as u64
1992 );
1993 assert_eq!(recordings.snapshot().reads.len(), 3);
1994 });
1995 }
1996
1997 #[test_traced("DEBUG")]
1998 fn test_recoverable_prefix_len_at_most() {
1999 deterministic::Runner::default().start(|context| async move {
2000 let (context, recordings) = RecordingContext::new(context);
2001 let (blob, size) = context.open("bounded_prefix", b"blob").await.unwrap();
2002 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2003 let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache).await.unwrap();
2004 let page = u64::from(PAGE_SIZE.get());
2005 let total = page * 8 + 10;
2006 (writer, _) = writer.append(&vec![7; total as usize]).await.unwrap();
2007 writer = writer.sync().await.unwrap();
2008
2009 for (cap, proven, expected_reads) in [
2011 (0, 0, 0),
2012 (1, 0, 1),
2013 (page, 0, 1),
2014 (page + 1, 0, 2),
2015 (page * 3, page, 2),
2016 (1, u64::MAX, 0),
2017 (total - 1, 0, 9),
2018 (total, 0, 9),
2019 (total + 1, 0, 9),
2020 (u64::MAX, 0, 9),
2021 (u64::MAX, u64::MAX, 1),
2022 ] {
2023 recordings.clear();
2024 assert_eq!(
2025 writer
2026 .recoverable_prefix_len_at_most(
2027 proven,
2028 cap,
2029 NZUsize!(1),
2030 ReadOptions::DONT_CACHE,
2031 )
2032 .await
2033 .unwrap(),
2034 cap.min(total),
2035 "cap {cap}, proven {proven}",
2036 );
2037 let reads = recordings.snapshot().reads;
2038 assert_eq!(reads.len(), expected_reads, "cap {cap}, proven {proven}");
2039 assert!(
2040 reads
2041 .iter()
2042 .all(|options| *options == ReadOptions::DONT_CACHE)
2043 );
2044 }
2045 });
2046 }
2047
2048 #[test_traced("DEBUG")]
2049 fn test_read_many_into_empty() {
2050 let executor = deterministic::Runner::default();
2051 executor.start(|context: deterministic::Context| async move {
2052 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2053 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2054 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2055 .await
2056 .unwrap();
2057
2058 (append, _) = append.append(&[0u8; 8]).await.unwrap();
2059 assert_eq!(append.size(), 8);
2060
2061 let mut buf = [];
2063 append
2064 .read_many_into(&mut buf, &[], NZUsize!(4))
2065 .await
2066 .unwrap();
2067 });
2068 }
2069
2070 #[test_traced("DEBUG")]
2071 fn test_read_many_into_all_in_tip() {
2072 let executor = deterministic::Runner::default();
2074 executor.start(|context: deterministic::Context| async move {
2075 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2076 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2077 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2078 .await
2079 .unwrap();
2080
2081 let data: Vec<u8> = (0..20).collect();
2082 (append, _) = append.append(&data).await.unwrap();
2083 assert_eq!(append.size(), 20);
2084
2085 let offsets = [0u64, 4, 8, 12, 16];
2087 let mut buf = vec![0u8; 5 * 4];
2088 append
2089 .read_many_into(&mut buf, &offsets, NZUsize!(4))
2090 .await
2091 .unwrap();
2092
2093 for (i, &off) in offsets.iter().enumerate() {
2094 assert_eq!(
2095 &buf[i * 4..(i + 1) * 4],
2096 &data[off as usize..off as usize + 4],
2097 );
2098 }
2099 });
2100 }
2101
2102 #[test_traced("DEBUG")]
2103 fn test_try_read_sync_all_in_tip() {
2104 let executor = deterministic::Runner::default();
2105 executor.start(|context: deterministic::Context| async move {
2106 let (blob, blob_size) = context
2107 .open("test_partition", b"try_read_sync_tip")
2108 .await
2109 .unwrap();
2110 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2111 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2112 .await
2113 .unwrap();
2114
2115 let data: Vec<u8> = (0..20).collect();
2116 (append, _) = append.append(&data).await.unwrap();
2117
2118 let mut buf = vec![0u8; data.len()];
2119 assert!(append.try_read_sync_into(&mut buf, 0));
2120 assert_eq!(buf, data);
2121 });
2122 }
2123
2124 #[test_traced("DEBUG")]
2125 fn test_try_read_sync_cache_miss() {
2126 let executor = deterministic::Runner::default();
2127 executor.start(|context: deterministic::Context| async move {
2128 let (blob, blob_size) = context
2129 .open("test_partition", b"try_read_sync_cache_miss")
2130 .await
2131 .unwrap();
2132
2133 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(1));
2135 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2136 .await
2137 .unwrap();
2138
2139 let page_size = PAGE_SIZE.get() as usize;
2140 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
2141 (append, _) = append.append(&data).await.unwrap();
2142 append = append.sync().await.unwrap();
2143
2144 let _ = append.read_at(0, page_size).await.unwrap();
2145
2146 let mut buf = vec![0xAA; 4];
2148 assert!(!append.try_read_sync_into(&mut buf, (page_size - 2) as u64));
2149 });
2150 }
2151
2152 #[test_traced("DEBUG")]
2153 fn test_read_many_into_all_from_cache() {
2154 let executor = deterministic::Runner::default();
2156 executor.start(|context: deterministic::Context| async move {
2157 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2158 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2159 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2160 .await
2161 .unwrap();
2162
2163 let data: Vec<u8> = (0..20).collect();
2164 (append, _) = append.append(&data).await.unwrap();
2165 append = append.sync().await.unwrap();
2166 assert_eq!(append.size(), 20);
2167
2168 let offsets = [0u64, 8, 16];
2169 let mut buf = vec![0u8; 3 * 4];
2170 append
2171 .read_many_into(&mut buf, &offsets, NZUsize!(4))
2172 .await
2173 .unwrap();
2174
2175 for (i, &off) in offsets.iter().enumerate() {
2176 assert_eq!(
2177 &buf[i * 4..(i + 1) * 4],
2178 &data[off as usize..off as usize + 4],
2179 );
2180 }
2181 });
2182 }
2183
2184 #[test_traced("DEBUG")]
2185 fn test_read_many_into_mixed_tip_and_cache() {
2186 let executor = deterministic::Runner::default();
2188 executor.start(|context: deterministic::Context| async move {
2189 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2190 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2191 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2192 .await
2193 .unwrap();
2194
2195 let first: Vec<u8> = (0..16).collect();
2196 (append, _) = append.append(&first).await.unwrap();
2197 append = append.sync().await.unwrap();
2198
2199 let second: Vec<u8> = (16..32).collect();
2200 (append, _) = append.append(&second).await.unwrap();
2201 assert_eq!(append.size(), 32);
2202
2203 let offsets = [0u64, 4, 16, 24];
2205 let mut buf = vec![0u8; 4 * 4];
2206 append
2207 .read_many_into(&mut buf, &offsets, NZUsize!(4))
2208 .await
2209 .unwrap();
2210
2211 let all: Vec<u8> = (0..32).collect();
2212 for (i, &off) in offsets.iter().enumerate() {
2213 assert_eq!(
2214 &buf[i * 4..(i + 1) * 4],
2215 &all[off as usize..off as usize + 4],
2216 );
2217 }
2218 });
2219 }
2220
2221 #[test_traced("DEBUG")]
2222 fn test_read_many_into_out_of_bounds() {
2223 let executor = deterministic::Runner::default();
2224 executor.start(|context: deterministic::Context| async move {
2225 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2226 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2227 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2228 .await
2229 .unwrap();
2230
2231 (append, _) = append.append(&[0u8; 8]).await.unwrap();
2232 assert_eq!(append.size(), 8);
2233
2234 let mut buf = vec![0u8; 4];
2236 let err = append
2237 .read_many_into(&mut buf, &[8], NZUsize!(4))
2238 .await
2239 .unwrap_err();
2240 assert!(matches!(err, Error::BlobInsufficientLength));
2241 });
2242 }
2243
2244 #[test_traced("DEBUG")]
2245 fn test_read_many_into_single_item() {
2246 let executor = deterministic::Runner::default();
2247 executor.start(|context: deterministic::Context| async move {
2248 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2249 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2250 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2251 .await
2252 .unwrap();
2253
2254 let data = vec![0xAA; 8];
2255 (append, _) = append.append(&data).await.unwrap();
2256 assert_eq!(append.size(), 8);
2257
2258 let mut buf = vec![0u8; 8];
2259 append
2260 .read_many_into(&mut buf, &[0], NZUsize!(8))
2261 .await
2262 .unwrap();
2263 assert_eq!(&buf, &data);
2264 });
2265 }
2266
2267 #[test_traced("DEBUG")]
2268 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
2269 fn test_read_many_into_rejects_invalid_buffer_len() {
2270 let executor = deterministic::Runner::default();
2271 executor.start(|context: deterministic::Context| async move {
2272 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2273 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2274 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2275 .await
2276 .unwrap();
2277
2278 let data: Vec<u8> = (0..16).collect();
2279 (append, _) = append.append(&data).await.unwrap();
2280
2281 let offsets = [0u64, 4];
2282 let mut buf = vec![0u8; 7];
2283 let _ = append.read_many_into(&mut buf, &offsets, NZUsize!(4)).await;
2284 });
2285 }
2286
2287 #[test_traced("DEBUG")]
2288 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
2289 fn test_read_many_into_rejects_unsorted_offsets() {
2290 let executor = deterministic::Runner::default();
2291 executor.start(|context: deterministic::Context| async move {
2292 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2293 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2294 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2295 .await
2296 .unwrap();
2297
2298 let data: Vec<u8> = (0..16).collect();
2299 (append, _) = append.append(&data).await.unwrap();
2300
2301 let mut buf = vec![0u8; 8];
2302 let _ = append.read_many_into(&mut buf, &[8, 4], NZUsize!(4)).await;
2303 });
2304 }
2305
2306 #[test_traced("DEBUG")]
2307 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
2308 fn test_read_many_into_rejects_overlapping_offsets() {
2309 let executor = deterministic::Runner::default();
2310 executor.start(|context: deterministic::Context| async move {
2311 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2312 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2313 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2314 .await
2315 .unwrap();
2316
2317 let data: Vec<u8> = (0..16).collect();
2318 (append, _) = append.append(&data).await.unwrap();
2319
2320 let mut buf = vec![0u8; 8];
2321 let _ = append.read_many_into(&mut buf, &[2, 4], NZUsize!(4)).await;
2322 });
2323 }
2324
2325 #[test_traced("DEBUG")]
2326 fn test_read_many_into_rejects_offset_overflow() {
2327 let executor = deterministic::Runner::default();
2328 executor.start(|context: deterministic::Context| async move {
2329 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2330 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2331 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2332 .await
2333 .unwrap();
2334
2335 let data: Vec<u8> = (0..16).collect();
2336 (append, _) = append.append(&data).await.unwrap();
2337
2338 let mut buf = vec![0u8; 8];
2339 let err = append
2340 .read_many_into(&mut buf, &[u64::MAX - 1, 4], NZUsize!(4))
2341 .await
2342 .unwrap_err();
2343 assert!(matches!(err, Error::OffsetOverflow));
2344 });
2345 }
2346
2347 #[test_traced("DEBUG")]
2348 fn test_read_many_into_matches_read_at() {
2349 let executor = deterministic::Runner::default();
2351 executor.start(|context: deterministic::Context| async move {
2352 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2353 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2354 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2355 .await
2356 .unwrap();
2357
2358 let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
2360 (append, _) = append.append(&data).await.unwrap();
2361 append = append.sync().await.unwrap();
2362
2363 let more: Vec<u8> = (0u8..50).collect();
2365 (append, _) = append.append(&more).await.unwrap();
2366 assert_eq!(append.size(), 350);
2367
2368 let item_size = 10;
2369 let offsets: Vec<u64> = (0..35).map(|i| i * item_size as u64).collect();
2370 let mut batch_buf = vec![0u8; offsets.len() * item_size];
2371 append
2372 .read_many_into(&mut batch_buf, &offsets, NZUsize!(item_size))
2373 .await
2374 .unwrap();
2375
2376 for (i, &off) in offsets.iter().enumerate() {
2378 let single = append.read_at(off, item_size).await.unwrap().coalesce();
2379 assert_eq!(
2380 &batch_buf[i * item_size..(i + 1) * item_size],
2381 single.as_ref(),
2382 "mismatch at offset {off}",
2383 );
2384 }
2385 });
2386 }
2387
2388 #[test_traced("DEBUG")]
2389 fn test_read_many_into_scattered_cache_misses() {
2390 let executor = deterministic::Runner::default();
2394 executor.start(|context: deterministic::Context| async move {
2395 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2396
2397 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(2));
2399 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2400 .await
2401 .unwrap();
2402
2403 let synced: Vec<u8> = (0u8..=255)
2405 .cycle()
2406 .take(PAGE_SIZE.get() as usize * 3)
2407 .collect();
2408 (append, _) = append.append(&synced).await.unwrap();
2409 append = append.sync().await.unwrap();
2410
2411 let item_size = 10;
2414 let tip_len = PAGE_SIZE.get() as usize / 2;
2415 let tip: Vec<u8> = (100u8..=255).cycle().take(tip_len).collect();
2416 (append, _) = append.append(&tip).await.unwrap();
2417
2418 let _ = append.read_at(0, item_size).await.unwrap();
2420 let _ = append
2421 .read_at(PAGE_SIZE.get() as u64 * 2, item_size)
2422 .await
2423 .unwrap();
2424
2425 let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
2428 let tip_off = synced.len() as u64 + item_size as u64;
2429 let offsets = [
2430 0u64, PAGE_SIZE.get() as u64, PAGE_SIZE.get() as u64 * 2, straddle_off, tip_off, ];
2436 let mut buf = vec![0u8; offsets.len() * item_size];
2437 append
2438 .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
2439 .await
2440 .unwrap();
2441
2442 let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
2443 for (i, &off) in offsets.iter().enumerate() {
2444 assert_eq!(
2445 &buf[i * item_size..(i + 1) * item_size],
2446 &read[off as usize..off as usize + item_size],
2447 );
2448 }
2449 });
2450 }
2451
2452 #[test_traced("DEBUG")]
2453 fn test_read_many_into_straddle_prefix_miss() {
2454 let executor = deterministic::Runner::default();
2458 executor.start(|context: deterministic::Context| async move {
2459 let (blob, blob_size) = context
2460 .open("test_partition", b"rmany_smiss")
2461 .await
2462 .unwrap();
2463
2464 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(1));
2466 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2467 .await
2468 .unwrap();
2469
2470 let synced: Vec<u8> = (0u8..=255)
2472 .cycle()
2473 .take(PAGE_SIZE.get() as usize * 3)
2474 .collect();
2475 (append, _) = append.append(&synced).await.unwrap();
2476 append = append.sync().await.unwrap();
2477 let item_size = 10;
2478 let tip: Vec<u8> = (100u8..=255)
2479 .cycle()
2480 .take(PAGE_SIZE.get() as usize / 2)
2481 .collect();
2482 (append, _) = append.append(&tip).await.unwrap();
2483
2484 let _ = append.read_at(0, item_size).await.unwrap();
2487
2488 let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
2489 let tip_off = synced.len() as u64 + item_size as u64;
2490 let offsets = [straddle_off, tip_off];
2491 let mut buf = vec![0u8; offsets.len() * item_size];
2492 let hits = append
2493 .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
2494 .await
2495 .unwrap();
2496
2497 assert_eq!(hits, 1);
2499 let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
2500 for (i, &off) in offsets.iter().enumerate() {
2501 assert_eq!(
2502 &buf[i * item_size..(i + 1) * item_size],
2503 &read[off as usize..off as usize + item_size],
2504 );
2505 }
2506 });
2507 }
2508
2509 #[test_traced("DEBUG")]
2510 fn test_append_crc_empty() {
2511 let executor = deterministic::Runner::default();
2512 executor.start(|context: deterministic::Context| async move {
2513 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2515 assert_eq!(blob_size, 0);
2516
2517 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2519
2520 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2522 .await
2523 .unwrap();
2524
2525 assert_eq!(append.size(), 0);
2527
2528 append = append.sync().await.unwrap();
2530 drop(append);
2531
2532 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2533 assert_eq!(blob_size, 0); let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2536 .await
2537 .unwrap();
2538
2539 assert_eq!(append.size(), 0);
2540 });
2541 }
2542
2543 #[test_traced("DEBUG")]
2544 fn test_append_crc_basic() {
2545 let executor = deterministic::Runner::default();
2546 executor.start(|context: deterministic::Context| async move {
2547 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2549 assert_eq!(blob_size, 0);
2550
2551 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2553
2554 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2556 .await
2557 .unwrap();
2558
2559 assert_eq!(append.size(), 0);
2561
2562 let data = vec![1, 2, 3, 4, 5];
2564 (append, _) = append.append(&data).await.unwrap();
2565
2566 assert_eq!(append.size(), 5);
2568
2569 let more_data = vec![6, 7, 8, 9, 10];
2571 (append, _) = append.append(&more_data).await.unwrap();
2572
2573 assert_eq!(append.size(), 10);
2575
2576 let read_buf = append.read_at(0, 5).await.unwrap().coalesce();
2578 assert_eq!(read_buf, &data[..]);
2579
2580 let read_buf = append.read_at(5, 5).await.unwrap().coalesce();
2582 assert_eq!(read_buf, &more_data[..]);
2583
2584 let read_buf = append.read_at(0, 10).await.unwrap().coalesce();
2586 assert_eq!(read_buf, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
2587
2588 append = append.sync().await.unwrap();
2591 drop(append);
2592
2593 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2594 assert_eq!(blob_size, 115);
2596 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2597 .await
2598 .unwrap();
2599 assert_eq!(append.size(), 10); let spanning_data: Vec<u8> = (11..=110).collect();
2605 (append, _) = append.append(&spanning_data).await.unwrap();
2606 assert_eq!(append.size(), 110);
2607
2608 let read_buf = append.read_at(10, 100).await.unwrap().coalesce();
2610 assert_eq!(read_buf, &spanning_data[..]);
2611
2612 let read_buf = append.read_at(0, 110).await.unwrap().coalesce();
2614 let expected: Vec<u8> = (1..=110).collect();
2615 assert_eq!(read_buf, &expected[..]);
2616
2617 append = append.sync().await.unwrap();
2619 drop(append);
2620
2621 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2622 assert_eq!(blob_size, 230);
2624 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2625 .await
2626 .unwrap();
2627 assert_eq!(append.size(), 110);
2628
2629 let boundary_data: Vec<u8> = (111..=206).collect();
2633 assert_eq!(boundary_data.len(), 96);
2634 (append, _) = append.append(&boundary_data).await.unwrap();
2635 assert_eq!(append.size(), 206);
2636
2637 let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
2639 let expected: Vec<u8> = (1..=206).collect();
2640 assert_eq!(read_buf, &expected[..]);
2641
2642 append = append.sync().await.unwrap();
2644 drop(append);
2645
2646 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2647 assert_eq!(blob_size, 230);
2649 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2650 .await
2651 .unwrap();
2652 assert_eq!(append.size(), 206);
2653
2654 let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
2656 assert_eq!(read_buf, &expected[..]);
2657 });
2658 }
2659
2660 #[test_traced("DEBUG")]
2661 fn test_append_owned_bypass_from_empty_tip() {
2662 let executor = deterministic::Runner::default();
2665 executor.start(|context: deterministic::Context| async move {
2666 let (blob, blob_size) = context
2667 .open("test_partition", b"owned_empty")
2668 .await
2669 .unwrap();
2670 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2671 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2672 .await
2673 .unwrap();
2674
2675 let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2677 let src = IoBuf::from(data.clone());
2678 let src_start = src.as_ptr() as usize;
2679 let src_range = src_start..src_start + src.len();
2680 (append, _) = append.append_owned(src.clone()).await.unwrap();
2681 assert_eq!(append.size(), 500);
2682
2683 let tip_ptr = append.buffer.parts().1.as_ptr() as usize;
2685 assert!(!src_range.contains(&tip_ptr));
2686
2687 let mut probe = vec![0u8; PAGE_SIZE.get() as usize];
2690 assert_eq!(
2691 append.cache_ref.read_cached(append.id, &mut probe, 0),
2692 PAGE_SIZE.get() as usize
2693 );
2694 assert_eq!(probe, &data[..PAGE_SIZE.get() as usize]);
2695
2696 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2698 assert_eq!(read_buf, &data[..]);
2699
2700 append = append.sync().await.unwrap();
2702 drop(append);
2703
2704 let (blob, blob_size) = context
2705 .open("test_partition", b"owned_empty")
2706 .await
2707 .unwrap();
2708 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2709 .await
2710 .unwrap();
2711 assert_eq!(append.size(), 500);
2712 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2713 assert_eq!(read_buf, &data[..]);
2714 });
2715 }
2716
2717 #[test_traced("DEBUG")]
2718 fn test_append_owned_bypass_with_synced_partial_page() {
2719 let executor = deterministic::Runner::default();
2722 executor.start(|context: deterministic::Context| async move {
2723 let (blob, blob_size) = context
2724 .open("test_partition", b"owned_partial")
2725 .await
2726 .unwrap();
2727 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2728 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2729 .await
2730 .unwrap();
2731
2732 let all: Vec<u8> = (0..500).map(|i| (i % 247) as u8).collect();
2734 (append, _) = append.append(&all[..50]).await.unwrap();
2735 append = append.sync().await.unwrap();
2736
2737 (append, _) = append
2740 .append_owned(IoBuf::from(all[50..].to_vec()))
2741 .await
2742 .unwrap();
2743 assert_eq!(append.size(), 500);
2744 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2745 assert_eq!(read_buf, &all[..]);
2746
2747 drop(append);
2750 let (blob, blob_size) = context
2751 .open("test_partition", b"owned_partial")
2752 .await
2753 .unwrap();
2754 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2755 .await
2756 .unwrap();
2757 assert_eq!(append.size(), 50);
2758 let read_buf = append.read_at(0, 50).await.unwrap().coalesce();
2759 assert_eq!(read_buf, &all[..50]);
2760
2761 (append, _) = append
2764 .append_owned(IoBuf::from(all[50..].to_vec()))
2765 .await
2766 .unwrap();
2767 append = append.sync().await.unwrap();
2768 drop(append);
2769
2770 let (blob, blob_size) = context
2771 .open("test_partition", b"owned_partial")
2772 .await
2773 .unwrap();
2774 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2775 .await
2776 .unwrap();
2777 assert_eq!(append.size(), 500);
2778 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2779 assert_eq!(read_buf, &all[..]);
2780 });
2781 }
2782
2783 #[test_traced("DEBUG")]
2784 fn test_append_owned_bypass_with_buffered_tip() {
2785 let executor = deterministic::Runner::default();
2788 executor.start(|context: deterministic::Context| async move {
2789 let (blob, blob_size) = context
2790 .open("test_partition", b"owned_buffered")
2791 .await
2792 .unwrap();
2793 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2794 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2795 .await
2796 .unwrap();
2797
2798 let all: Vec<u8> = (0..430).map(|i| (i % 239) as u8).collect();
2799 (append, _) = append.append(&all[..30]).await.unwrap();
2800 (append, _) = append
2801 .append_owned(IoBuf::from(all[30..].to_vec()))
2802 .await
2803 .unwrap();
2804 assert_eq!(append.size(), 430);
2805 let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
2806 assert_eq!(read_buf, &all[..]);
2807
2808 append = append.sync().await.unwrap();
2809 drop(append);
2810
2811 let (blob, blob_size) = context
2812 .open("test_partition", b"owned_buffered")
2813 .await
2814 .unwrap();
2815 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2816 .await
2817 .unwrap();
2818 assert_eq!(append.size(), 430);
2819 let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
2820 assert_eq!(read_buf, &all[..]);
2821 });
2822 }
2823
2824 #[test_traced("DEBUG")]
2825 fn test_append_owned_exact_page_multiple_and_small() {
2826 let executor = deterministic::Runner::default();
2829 executor.start(|context: deterministic::Context| async move {
2830 let (blob, blob_size) = context
2831 .open("test_partition", b"owned_exact")
2832 .await
2833 .unwrap();
2834 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2835 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2836 .await
2837 .unwrap();
2838
2839 let bulk: Vec<u8> = (0..412).map(|i| (i % 233) as u8).collect();
2841 (append, _) = append
2842 .append_owned(IoBuf::from(bulk.clone()))
2843 .await
2844 .unwrap();
2845 assert_eq!(append.size(), 412);
2846
2847 let small: Vec<u8> = (0..10).map(|i| (i % 229) as u8).collect();
2849 (append, _) = append
2850 .append_owned(IoBuf::from(small.clone()))
2851 .await
2852 .unwrap();
2853 assert_eq!(append.size(), 422);
2854
2855 let read_buf = append.read_at(0, 422).await.unwrap().coalesce();
2856 assert_eq!(&read_buf.as_ref()[..412], &bulk[..]);
2857 assert_eq!(&read_buf.as_ref()[412..], &small[..]);
2858
2859 append = append.sync().await.unwrap();
2860 drop(append);
2861
2862 let (blob, blob_size) = context
2863 .open("test_partition", b"owned_exact")
2864 .await
2865 .unwrap();
2866 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2867 .await
2868 .unwrap();
2869 assert_eq!(append.size(), 422);
2870 });
2871 }
2872
2873 #[test_traced("DEBUG")]
2874 fn test_append_owned_physical_bytes_match_buffered() {
2875 let executor = deterministic::Runner::default();
2878 executor.start(|context: deterministic::Context| async move {
2879 let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2880 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2881
2882 let (blob, size) = context
2883 .open("test_partition", b"phys_direct")
2884 .await
2885 .unwrap();
2886 let mut direct = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
2887 .await
2888 .unwrap();
2889 (direct, _) = direct
2890 .append_owned(IoBuf::from(data.clone()))
2891 .await
2892 .unwrap();
2893 direct = direct.sync().await.unwrap();
2894 drop(direct);
2895
2896 let (blob, size) = context
2898 .open("test_partition", b"phys_buffered")
2899 .await
2900 .unwrap();
2901 let mut buffered = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2902 .await
2903 .unwrap();
2904 for chunk in data.chunks(10) {
2905 (buffered, _) = buffered.append(chunk).await.unwrap();
2906 }
2907 buffered = buffered.sync().await.unwrap();
2908 drop(buffered);
2909
2910 let (blob_a, size_a) = context
2911 .open("test_partition", b"phys_direct")
2912 .await
2913 .unwrap();
2914 let (blob_b, size_b) = context
2915 .open("test_partition", b"phys_buffered")
2916 .await
2917 .unwrap();
2918 assert_eq!(size_a, size_b);
2919 let bytes_a = blob_a
2920 .read_at(0, size_a as usize, ReadOptions::default())
2921 .await
2922 .unwrap()
2923 .coalesce();
2924 let bytes_b = blob_b
2925 .read_at(0, size_b as usize, ReadOptions::default())
2926 .await
2927 .unwrap()
2928 .coalesce();
2929 assert_eq!(bytes_a.as_ref(), bytes_b.as_ref());
2930 });
2931 }
2932
2933 #[test_traced("DEBUG")]
2934 fn test_append_borrowed_large_takes_direct_path() {
2935 let executor = deterministic::Runner::default();
2938 executor.start(|context: deterministic::Context| async move {
2939 let (blob, blob_size) = context
2940 .open("test_partition", b"borrowed_large")
2941 .await
2942 .unwrap();
2943 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2944 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2945 .await
2946 .unwrap();
2947
2948 let all: Vec<u8> = (0..530).map(|i| (i % 241) as u8).collect();
2950 (append, _) = append.append(&all[..30]).await.unwrap();
2951
2952 (append, _) = append.append(&all[30..]).await.unwrap();
2954 assert_eq!(append.size(), 530);
2955
2956 assert_eq!(append.buffer.len(), 15);
2958
2959 let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2960 assert_eq!(read_buf, &all[..]);
2961
2962 append = append.sync().await.unwrap();
2963 drop(append);
2964
2965 let (blob, blob_size) = context
2966 .open("test_partition", b"borrowed_large")
2967 .await
2968 .unwrap();
2969 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2970 .await
2971 .unwrap();
2972 assert_eq!(append.size(), 530);
2973 let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2974 assert_eq!(read_buf, &all[..]);
2975 });
2976 }
2977
2978 #[test]
2979 fn test_sync_preserves_partial_tip_allocation() {
2980 const PAGE_SIZE: NonZeroU16 = NZU16!(4096);
2981 const BUFFER_SIZE: usize = 8192;
2982 deterministic::Runner::default().start(|context| async move {
2983 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(1));
2984 let (blob, size) = context
2985 .open("test_partition", b"partial_seal")
2986 .await
2987 .unwrap();
2988 let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache).await.unwrap();
2989 (writer, _) = writer.append(&[7; 16]).await.unwrap();
2990 let ptr = writer.buffer.parts().1.as_ptr();
2991
2992 writer = writer.sync().await.unwrap();
2994 assert_eq!(writer.buffer.parts().1.as_ptr(), ptr);
2995 (writer, _) = writer.append(&[7; 16]).await.unwrap();
2996 assert_eq!(writer.buffer.parts().1.as_ptr(), ptr);
2997
2998 let (sealed, sync) = writer.seal().await.unwrap();
2999 assert_eq!(
3000 sealed.read_at(0, 32).await.unwrap().coalesce().as_ref(),
3001 &[7; 32]
3002 );
3003 sync.await.unwrap();
3004 });
3005 }
3006
3007 #[test]
3008 fn test_recovery_sync_preserves_truncate_and_append() {
3009 deterministic::Runner::default().start(|context| async move {
3010 let page_size = NZU16!(64);
3011 let cache = CacheRef::new(page_size, NZUsize!(4));
3012 let (blob, size) = context
3013 .open("test_partition", b"compact_recovery")
3014 .await
3015 .unwrap();
3016 let mut recovery = Recovery::open(blob, size, 256, cache.clone())
3017 .await
3018 .unwrap();
3019 let mut expected: Vec<u8> = (0..129).collect();
3020 (recovery, _) = recovery.append(&expected).await.unwrap();
3021 recovery = recovery.sync().await.unwrap();
3022 assert_eq!(recovery.buffer.len(), 1);
3023 assert_eq!(recovery.buffer.capacity, 256);
3024
3025 recovery = recovery.truncate(95).await.unwrap();
3027 expected.truncate(95);
3028 let suffix = [7; 80];
3029 (recovery, _) = recovery.append(&suffix).await.unwrap();
3030 expected.extend_from_slice(&suffix);
3031 recovery = recovery.sync().await.unwrap();
3032 assert_eq!(recovery.buffer.capacity, 256);
3033 drop(recovery);
3034
3035 let (blob, size) = context
3036 .open("test_partition", b"compact_recovery")
3037 .await
3038 .unwrap();
3039 let writer = Writer::new(blob, size, 256, cache).await.unwrap();
3040 assert_eq!(writer.size(), expected.len() as u64);
3041 assert_eq!(
3042 writer
3043 .read_at(0, expected.len())
3044 .await
3045 .unwrap()
3046 .coalesce()
3047 .as_ref(),
3048 expected
3049 );
3050 });
3051 }
3052
3053 #[test_traced("DEBUG")]
3054 fn test_sync_uses_range_sync_for_single_flush() {
3055 let executor = deterministic::Runner::default();
3056 executor.start(|_: deterministic::Context| async move {
3057 let blob = SyncTrackingBlob::new();
3058 let blob = Arc::new(blob);
3059 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3060 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3061 .await
3062 .unwrap();
3063
3064 append = append.sync().await.unwrap();
3066 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3067 assert_eq!(writes, 0);
3068 assert_eq!(full_syncs, 1);
3069 assert_eq!(range_syncs, 0);
3070
3071 let data = b"hello world";
3073 (append, _) = append.append(data).await.unwrap();
3074 append = append.sync().await.unwrap();
3075
3076 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3077 assert_eq!(writes, 1);
3078 assert_eq!(full_syncs, 1);
3079 assert_eq!(range_syncs, 1);
3080
3081 append.sync().await.unwrap();
3083 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3084 assert_eq!(writes, 1);
3085 assert_eq!(full_syncs, 1);
3086 assert_eq!(range_syncs, 1);
3087
3088 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3089 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3090 .await
3091 .unwrap();
3092 let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
3093 assert_eq!(read.as_ref(), data);
3094 });
3095 }
3096
3097 #[test_traced("DEBUG")]
3099 fn test_start_sync_persists_and_marks_clean() {
3100 let executor = deterministic::Runner::default();
3101 executor.start(|_: deterministic::Context| async move {
3102 let blob = SyncTrackingBlob::new();
3103 let blob = Arc::new(blob);
3104 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3105 let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3106 .await
3107 .unwrap();
3108
3109 let (mut writer, handle) = writer.start_sync().await.unwrap();
3111 handle.await.unwrap();
3112 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3113 assert_eq!(writes, 0);
3114 assert_eq!(full_syncs, 1);
3115 assert_eq!(range_syncs, 0);
3116
3117 let data = b"hello world";
3119 (writer, _) = writer.append(data).await.unwrap();
3120 writer = writer.sync().await.unwrap();
3121 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3122 assert_eq!(writes, 1);
3123 assert_eq!(full_syncs, 1);
3124 assert_eq!(range_syncs, 1);
3125
3126 let (_, handle) = writer.start_sync().await.unwrap();
3128 handle.await.unwrap();
3129 let (_, _, full_syncs, range_syncs) = blob.snapshot();
3130 assert_eq!(full_syncs, 1);
3131 assert_eq!(range_syncs, 1);
3132
3133 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3135 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3136 .await
3137 .unwrap();
3138 let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
3139 assert_eq!(read.as_ref(), data);
3140 });
3141 }
3142
3143 #[test_traced("DEBUG")]
3145 fn test_start_sync_flush_failure_returns_err() {
3146 let executor = deterministic::Runner::default();
3147 executor.start(|context: deterministic::Context| async move {
3148 let faults = WriteFaults::default();
3149 let context = WriteFaultContext {
3150 inner: context,
3151 faults: faults.clone(),
3152 };
3153 let (blob, size) = context
3154 .open("test_partition", b"start_sync_flush_failure")
3155 .await
3156 .unwrap();
3157 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3158 let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3159 .await
3160 .unwrap();
3161
3162 (writer, _) = writer.append(&[1u8; 24]).await.unwrap();
3164 writer = writer.sync().await.unwrap();
3165 (writer, _) = writer.append(&[2u8; 8]).await.unwrap();
3166
3167 faults.arm();
3169 assert!(writer.start_sync().await.is_err());
3170 });
3171 }
3172
3173 #[test_traced("DEBUG")]
3176 fn test_needs_sync_tracks_sync_work() {
3177 let executor = deterministic::Runner::default();
3178 executor.start(|context: deterministic::Context| async move {
3179 let (blob, size) = context.open("test_partition", b"needs_sync").await.unwrap();
3180 let (blob, pending) = DelayedSyncBlob::new(blob);
3181 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3182 let writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3183 .await
3184 .unwrap();
3185
3186 assert!(writer.needs_sync());
3190 let mut writer = writer.sync().await.unwrap();
3191 assert!(!writer.needs_sync());
3192
3193 (writer, _) = writer.append(&[1u8; 20]).await.unwrap();
3195 assert!(writer.needs_sync());
3196 writer = writer.sync().await.unwrap();
3197 assert!(!writer.needs_sync());
3198
3199 (writer, _) = writer.append(&[2u8; 4]).await.unwrap();
3201 let (writer, handle) = writer.start_sync().await.unwrap();
3202 assert!(writer.needs_sync());
3203 next_pending_sync(&pending).release.send(Ok(())).unwrap();
3204 handle.await.unwrap();
3205 assert!(writer.needs_sync());
3206 let mut writer = writer.wait_for_sync().await.unwrap();
3207 assert!(!writer.needs_sync());
3208
3209 (writer, _) = writer.append(&[3u8; 8]).await.unwrap();
3212 let (writer, handle) = writer.start_sync().await.unwrap();
3213 next_pending_sync(&pending)
3214 .release
3215 .send(Err(Error::Closed))
3216 .unwrap();
3217 assert!(handle.await.is_err());
3218 assert!(writer.needs_sync());
3219 assert!(writer.sync().await.is_err());
3220 });
3221 }
3222
3223 #[test_traced("DEBUG")]
3225 fn test_sync_waits_for_outstanding_start_sync() {
3226 let executor = deterministic::Runner::default();
3227 executor.start(|_: deterministic::Context| async move {
3228 let inner = SyncTrackingBlob::new();
3229 let inner = Arc::new(inner);
3230 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3231 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3232 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3233
3234 let (writer, handle) = writer.start_sync().await.unwrap();
3235 let deferred = next_pending_sync(&pending);
3236
3237 let mut sync = Box::pin(writer.sync());
3239 assert!(
3240 sync.as_mut().now_or_never().is_none(),
3241 "sync must wait for the outstanding start_sync handle"
3242 );
3243 let (_, _, full_syncs, range_syncs) = inner.snapshot();
3244 assert_eq!(full_syncs, 0);
3245 assert_eq!(range_syncs, 0);
3246
3247 deferred.release.send(Ok(())).unwrap();
3249 sync.await.unwrap();
3250 handle.await.unwrap();
3251 let (_, _, full_syncs, range_syncs) = inner.snapshot();
3252 assert_eq!(full_syncs, 1);
3253 assert_eq!(range_syncs, 0);
3254 });
3255 }
3256
3257 #[test_traced("DEBUG")]
3259 fn test_sync_after_start_sync_and_small_write_waits_before_range_sync() {
3260 let executor = deterministic::Runner::default();
3261 executor.start(|_: deterministic::Context| async move {
3262 let inner = SyncTrackingBlob::new();
3263 let inner = Arc::new(inner);
3264 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3265 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3266 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3267
3268 let (writer, handle) = writer.start_sync().await.unwrap();
3269 let deferred = next_pending_sync(&pending);
3270 let (writer, _) = writer.append(b"hello world").await.unwrap();
3271
3272 let mut sync = Box::pin(writer.sync());
3274 assert!(
3275 sync.as_mut().now_or_never().is_none(),
3276 "sync must join the outstanding barrier before flushing the small write"
3277 );
3278 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3279 assert_eq!(writes, 0);
3280 assert_eq!(full_syncs, 0);
3281 assert_eq!(range_syncs, 0);
3282
3283 deferred.release.send(Ok(())).unwrap();
3285 sync.await.unwrap();
3286 handle.await.unwrap();
3287 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3288 assert_eq!(writes, 1);
3289 assert_eq!(full_syncs, 1);
3290 assert_eq!(range_syncs, 1);
3291 });
3292 }
3293
3294 #[test_traced("DEBUG")]
3296 fn test_write_flush_waits_for_outstanding_start_sync() {
3297 let executor = deterministic::Runner::default();
3298 executor.start(|context: deterministic::Context| async move {
3299 let inner = SyncTrackingBlob::new();
3300 let inner = Arc::new(inner);
3301 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3302 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3303 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3304
3305 let (mut writer, handle) = writer.start_sync().await.unwrap();
3306 let deferred = next_pending_sync(&pending);
3307
3308 let data = vec![7; BUFFER_SIZE + PAGE_SIZE.get() as usize];
3309 let append = context.child("append").spawn(move |_| async move {
3310 (writer, _) = writer.append(&data).await.unwrap();
3311 writer
3312 });
3313
3314 deferred
3316 .blocked
3317 .await
3318 .expect("append never waited on start_sync");
3319 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3320 assert_eq!(writes, 0);
3321 assert_eq!(full_syncs, 0);
3322 assert_eq!(range_syncs, 0);
3323
3324 deferred.release.send(Ok(())).unwrap();
3326 let writer = append.await.unwrap();
3327 handle.await.unwrap();
3328 writer.sync().await.unwrap();
3329 let (_, writes, full_syncs, _) = inner.snapshot();
3330 assert!(writes > 0);
3331 assert!(full_syncs > 0);
3332 });
3333 }
3334
3335 #[test_traced("DEBUG")]
3337 fn test_seal_waits_for_outstanding_start_sync_before_flushing() {
3338 let executor = deterministic::Runner::default();
3339 executor.start(|context: deterministic::Context| async move {
3340 let inner = SyncTrackingBlob::new();
3341 let inner = Arc::new(inner);
3342 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3343 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3344 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3345
3346 let (mut writer, prior) = writer.start_sync().await.unwrap();
3347 let deferred = next_pending_sync(&pending);
3348 (writer, _) = writer.append(b"hello world").await.unwrap();
3349
3350 let seal = context
3351 .child("seal")
3352 .spawn(move |_| async move { writer.seal().await });
3353
3354 deferred
3356 .blocked
3357 .await
3358 .expect("seal never waited on start_sync");
3359 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3360 assert_eq!(writes, 0);
3361 assert_eq!(full_syncs, 0);
3362 assert_eq!(range_syncs, 0);
3363
3364 deferred.release.send(Ok(())).unwrap();
3366 let (sealed, sync) = seal.await.unwrap().unwrap();
3367 prior.await.unwrap();
3368
3369 let deferred = next_pending_sync(&pending);
3371 deferred.release.send(Ok(())).unwrap();
3372 sync.await.unwrap();
3373 let read = sealed
3374 .read_at(0, b"hello world".len())
3375 .await
3376 .unwrap()
3377 .coalesce();
3378 assert_eq!(read.as_ref(), b"hello world");
3379 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3380 assert_eq!(writes, 1);
3381 assert_eq!(full_syncs, 2);
3382 assert_eq!(range_syncs, 0);
3383 });
3384 }
3385
3386 #[test_traced("DEBUG")]
3388 fn test_snapshot_waits_for_outstanding_start_sync_before_flushing() {
3389 let executor = deterministic::Runner::default();
3390 executor.start(|context: deterministic::Context| async move {
3391 let inner = SyncTrackingBlob::new();
3392 let inner = Arc::new(inner);
3393 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3394 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3395 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3396
3397 let data = [7u8; PAGE_SIZE.get() as usize + 47];
3399 let (mut writer, prior) = writer.start_sync().await.unwrap();
3400 let deferred = next_pending_sync(&pending);
3401 (writer, _) = writer.append(&data).await.unwrap();
3402
3403 let snapshot = context.child("snapshot").spawn(move |_| async move {
3404 let (writer, snapshot) = writer.snapshot().await.unwrap();
3405 let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
3406 assert_eq!(read.as_ref(), data.as_slice());
3407 writer
3408 });
3409
3410 deferred
3412 .blocked
3413 .await
3414 .expect("snapshot never waited on start_sync");
3415 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3416 assert_eq!(writes, 0);
3417 assert_eq!(full_syncs, 0);
3418 assert_eq!(range_syncs, 0);
3419
3420 deferred.release.send(Ok(())).unwrap();
3422 let _writer = snapshot.await.unwrap();
3423 prior.await.unwrap();
3424 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3425 assert_eq!(writes, 1);
3426 assert_eq!(full_syncs, 1);
3427 assert_eq!(range_syncs, 0);
3428 });
3429 }
3430
3431 #[test_traced("DEBUG")]
3434 fn test_snapshot_copies_partial_page_without_writing() {
3435 let executor = deterministic::Runner::default();
3436 executor.start(|_: deterministic::Context| async move {
3437 let blob = Arc::new(SyncTrackingBlob::new());
3438 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3439 let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3440 .await
3441 .unwrap();
3442
3443 let (writer, _) = writer.append(b"hello").await.unwrap();
3445 let (writer, snapshot) = writer.snapshot().await.unwrap();
3446 let (_, writes, _, _) = blob.snapshot();
3447 assert_eq!(writes, 0);
3448
3449 let (writer, _) = writer.append(b" world").await.unwrap();
3451 let _writer = writer.sync().await.unwrap();
3452 let (_, writes, _, _) = blob.snapshot();
3453 assert_eq!(writes, 1);
3454 assert_eq!(snapshot.size(), 5);
3455 let read = snapshot.read_at(0, 5).await.unwrap().coalesce();
3456 assert_eq!(read.as_ref(), b"hello");
3457 });
3458 }
3459
3460 #[test_traced]
3462 fn test_try_snapshot_requires_written_full_pages() {
3463 let executor = deterministic::Runner::default();
3464 executor.start(|_: deterministic::Context| async move {
3465 let blob = Arc::new(SyncTrackingBlob::new());
3466 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3467 let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3468 .await
3469 .unwrap();
3470
3471 let (writer, _) = writer.append(b"hello").await.unwrap();
3473 let view = writer.try_snapshot().expect("partial page needs no write");
3474 assert_eq!(view.size(), 5);
3475 let read = view.read_at(0, 5).await.unwrap().coalesce();
3476 assert_eq!(read.as_ref(), b"hello");
3477 let (_, writes, _, _) = blob.snapshot();
3478 assert_eq!(writes, 0);
3479
3480 let page = vec![b'x'; PAGE_SIZE.get() as usize];
3482 let (writer, _) = writer.append(&page).await.unwrap();
3483 assert!(writer.try_snapshot().is_none());
3484
3485 let (writer, snapshot) = writer.snapshot().await.unwrap();
3487 let (_, writes, _, _) = blob.snapshot();
3488 assert_eq!(writes, 1);
3489 let view = writer.try_snapshot().expect("full pages are written");
3490 assert_eq!(view.size(), snapshot.size());
3491 let mut replay = view
3492 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
3493 .unwrap();
3494 let total = 5 + page.len();
3495 assert!(replay.ensure(total).await.unwrap());
3496 let bytes = replay.copy_to_bytes(total);
3497 assert_eq!(&bytes[..5], b"hello");
3498 assert!(bytes[5..].iter().all(|byte| *byte == b'x'));
3499 assert!(!replay.ensure(1).await.unwrap());
3500 });
3501 }
3502
3503 #[test_traced("DEBUG")]
3505 fn test_replay_waits_for_outstanding_start_sync_before_flushing() {
3506 let executor = deterministic::Runner::default();
3507 executor.start(|context: deterministic::Context| async move {
3508 let inner = SyncTrackingBlob::new();
3509 let inner = Arc::new(inner);
3510 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3511 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3512 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3513
3514 let (mut writer, prior) = writer.start_sync().await.unwrap();
3516 let deferred = next_pending_sync(&pending);
3517 (writer, _) = writer.append(b"hello world").await.unwrap();
3518
3519 let replay = context.child("replay").spawn(move |_| async move {
3520 let (writer, mut replay) = writer
3521 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
3522 .await
3523 .unwrap();
3524 assert!(replay.ensure(1).await.unwrap());
3525 assert_eq!(replay.chunk()[0], b'h');
3526 writer
3527 });
3528
3529 deferred
3531 .blocked
3532 .await
3533 .expect("replay never waited on start_sync");
3534 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3535 assert_eq!(writes, 0);
3536 assert_eq!(full_syncs, 0);
3537 assert_eq!(range_syncs, 0);
3538
3539 deferred.release.send(Ok(())).unwrap();
3541 let _writer = replay.await.unwrap();
3542 prior.await.unwrap();
3543 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3544 assert_eq!(writes, 1);
3545 assert_eq!(full_syncs, 1);
3546 assert_eq!(range_syncs, 0);
3547 });
3548 }
3549
3550 #[test_traced("DEBUG")]
3551 fn test_append_waits_for_outstanding_start_sync_before_writing() {
3552 let executor = deterministic::Runner::default();
3553 executor.start(|context: deterministic::Context| async move {
3554 let inner = SyncTrackingBlob::new();
3555 let inner = Arc::new(inner);
3556 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3557 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3558 let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3559
3560 let (mut writer, prior) = writer.start_sync().await.unwrap();
3562 let deferred = next_pending_sync(&pending);
3563
3564 let target_size = (BUFFER_SIZE + PAGE_SIZE.get() as usize) as u64;
3565 let append = context.child("append").spawn(move |_| async move {
3566 (writer, _) = writer.append(&vec![0; target_size as usize]).await.unwrap();
3567 writer
3568 });
3569
3570 deferred
3572 .blocked
3573 .await
3574 .expect("append never waited on start_sync");
3575 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3576 assert_eq!(writes, 0);
3577 assert_eq!(full_syncs, 0);
3578 assert_eq!(range_syncs, 0);
3579
3580 deferred.release.send(Ok(())).unwrap();
3582 let writer = append.await.unwrap();
3583 prior.await.unwrap();
3584 assert_eq!(writer.size(), target_size);
3585 writer.sync().await.unwrap();
3586 let (_, writes, full_syncs, _) = inner.snapshot();
3587 assert!(writes > 0);
3588 assert!(full_syncs > 0);
3589 });
3590 }
3591
3592 #[test_traced("DEBUG")]
3594 fn test_recovery_truncate_shrink_waits_for_outstanding_start_sync_before_resizing() {
3595 let executor = deterministic::Runner::default();
3596 executor.start(|context: deterministic::Context| async move {
3597 let inner = SyncTrackingBlob::new();
3598 let inner = Arc::new(inner);
3599 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3600 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3601 let mut writer = Recovery::open(blob, 0, BUFFER_SIZE, cache_ref)
3602 .await
3603 .unwrap();
3604
3605 let data = vec![3; PAGE_SIZE.get() as usize * 2];
3607 (writer, _) = writer.append(&data).await.unwrap();
3608 writer = writer.sync().await.unwrap();
3609 (writer, _) = writer.append(b"x").await.unwrap();
3610 let (mut writer, prior) = writer.start_sync().await.unwrap();
3611 let deferred = next_pending_sync(&pending);
3612 let physical_size = inner.size();
3613
3614 let resize = context.child("resize_shrink").spawn(move |_| async move {
3615 writer = writer.truncate(PAGE_SIZE.get() as u64).await.unwrap();
3616 writer
3617 });
3618
3619 deferred
3621 .blocked
3622 .await
3623 .expect("resize shrink never waited on start_sync");
3624 assert_eq!(
3625 inner.size(),
3626 physical_size,
3627 "resize must not shrink the blob before the pending sync finishes"
3628 );
3629
3630 deferred.release.send(Ok(())).unwrap();
3632 let writer = resize.await.unwrap();
3633 prior.await.unwrap();
3634 assert_eq!(writer.size(), PAGE_SIZE.get() as u64);
3635 assert!(inner.size() < physical_size);
3636 });
3637 }
3638
3639 #[test_traced("DEBUG")]
3640 fn test_sync_uses_full_sync_after_prior_plain_flush() {
3641 let executor = deterministic::Runner::default();
3642 executor.start(|_: deterministic::Context| async move {
3643 let blob = SyncTrackingBlob::new();
3644 let blob = Arc::new(blob);
3645 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3646 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3647 .await
3648 .unwrap();
3649
3650 let data = vec![7u8; BUFFER_SIZE + 1];
3653 (append, _) = append.append(&data).await.unwrap();
3654 append = append.sync().await.unwrap();
3655
3656 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3657 assert_eq!(writes, 2);
3658 assert_eq!(full_syncs, 1);
3659 assert_eq!(range_syncs, 0);
3660
3661 append = append.sync().await.unwrap();
3663 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3664 assert_eq!(writes, 2);
3665 assert_eq!(full_syncs, 1);
3666 assert_eq!(range_syncs, 0);
3667
3668 (append, _) = append.append(b"tip").await.unwrap();
3671 append.sync().await.unwrap();
3672
3673 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3674 assert_eq!(writes, 3);
3675 assert_eq!(full_syncs, 1);
3676 assert_eq!(range_syncs, 1);
3677
3678 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3679 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3680 .await
3681 .unwrap();
3682 let mut expected = data;
3683 expected.extend_from_slice(b"tip");
3684 let read = reopened
3685 .read_at(0, expected.len())
3686 .await
3687 .unwrap()
3688 .coalesce();
3689 assert_eq!(read.as_ref(), expected.as_slice());
3690 });
3691 }
3692
3693 #[test_traced("DEBUG")]
3694 fn test_sync_uses_full_sync_after_replay_plain_flush() {
3695 let executor = deterministic::Runner::default();
3696 executor.start(|_: deterministic::Context| async move {
3697 let blob = SyncTrackingBlob::new();
3698 let blob = Arc::new(blob);
3699 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3700 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3701 .await
3702 .unwrap();
3703
3704 (append, _) = append.append(b"replayed").await.unwrap();
3706
3707 let (append, mut replay) = append
3709 .replay(NZUsize!(1024), ReadOptions::default())
3710 .await
3711 .unwrap();
3712 assert!(replay.ensure(b"replayed".len()).await.unwrap());
3713 assert_eq!(replay.remaining(), b"replayed".len());
3714 assert_eq!(replay.chunk(), b"replayed");
3715
3716 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3717 assert_eq!(writes, 1);
3718 assert_eq!(full_syncs, 0);
3719 assert_eq!(range_syncs, 0);
3720
3721 append.sync().await.unwrap();
3723 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3724 assert_eq!(writes, 1);
3725 assert_eq!(full_syncs, 1);
3726 assert_eq!(range_syncs, 0);
3727 });
3728 }
3729
3730 #[test_traced("DEBUG")]
3731 fn test_replay_uses_read_options_for_refills_and_seek() {
3732 let executor = deterministic::Runner::default();
3733 executor.start(|context: deterministic::Context| async move {
3734 let (blob, size) = context
3735 .open("test_partition", b"replay_read_options")
3736 .await
3737 .unwrap();
3738 let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
3739 let read_options = blob.read_options();
3740 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3741 let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3742 .await
3743 .unwrap();
3744
3745 let page_size = PAGE_SIZE.get() as usize;
3746 let data = vec![3; page_size * 2];
3747 (writer, _) = writer.append(&data).await.unwrap();
3748 writer = writer.sync().await.unwrap();
3749 read_options.lock().clear();
3750
3751 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
3752 let (_, mut replay) = writer
3753 .replay(NZUsize!(physical_page_size), ReadOptions::DONT_CACHE)
3754 .await
3755 .unwrap();
3756
3757 assert!(replay.ensure(1).await.unwrap());
3760 replay.advance(page_size);
3761 assert!(replay.ensure(1).await.unwrap());
3762 assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 2]);
3763
3764 replay.seek_to(0).unwrap();
3766 assert!(replay.ensure(1).await.unwrap());
3767 assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 4]);
3768 });
3769 }
3770
3771 #[test_traced("DEBUG")]
3772 fn test_recreated_sync_preserves_replay_plain_flush_barrier() {
3773 let executor = deterministic::Runner::default();
3774 executor.start(|_: deterministic::Context| async move {
3775 let blob = SyncTrackingBlob::new();
3776 let blob = Arc::new(blob);
3777 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3778 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3779 .await
3780 .unwrap();
3781
3782 (append, _) = append.append(b"replayed").await.unwrap();
3783 let (append, mut replay) = append
3784 .replay(NZUsize!(1024), ReadOptions::default())
3785 .await
3786 .unwrap();
3787 assert!(replay.ensure(b"replayed".len()).await.unwrap());
3788 assert_eq!(replay.remaining(), b"replayed".len());
3789 assert_eq!(replay.chunk(), b"replayed");
3790 drop(replay);
3791 drop(append);
3792
3793 let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
3794 assert!(durable.is_empty());
3795 assert_eq!(writes, 1);
3796 assert_eq!(full_syncs, 0);
3797 assert_eq!(range_syncs, 0);
3798
3799 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3800 let reopened = Recovery::open(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3801 .await
3802 .unwrap();
3803 assert_eq!(reopened.size(), b"replayed".len() as u64);
3804 reopened.sync().await.unwrap();
3805
3806 let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
3807 assert_eq!(durable.len(), blob.size() as usize);
3808 assert_eq!(writes, 1);
3809 assert_eq!(full_syncs, 1);
3810 assert_eq!(range_syncs, 0);
3811 });
3812 }
3813
3814 #[test_traced("DEBUG")]
3815 fn test_recreated_sync_skips_barrier_after_invalid_truncation() {
3816 let executor = deterministic::Runner::default();
3817 executor.start(|_: deterministic::Context| async move {
3818 let blob = SyncTrackingBlob::new();
3819 let blob = Arc::new(blob);
3820 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3821 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3822 .await
3823 .unwrap();
3824 append = append.sync().await.unwrap();
3825 (append, _) = append.append(b"valid").await.unwrap();
3826 append = append.sync().await.unwrap();
3827 drop(append);
3828
3829 blob.write_at(blob.size(), b"junk", WriteOptions::default())
3830 .await
3831 .unwrap();
3832
3833 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3834 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3835 .await
3836 .unwrap();
3837 assert_eq!(reopened.size(), b"valid".len() as u64);
3838
3839 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3840 assert_eq!(writes, 2);
3841 assert_eq!(full_syncs, 2);
3842 assert_eq!(range_syncs, 1);
3843
3844 reopened.sync().await.unwrap();
3845
3846 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3847 assert_eq!(writes, 2);
3848 assert_eq!(full_syncs, 2);
3849 assert_eq!(range_syncs, 1);
3850 });
3851 }
3852
3853 #[test_traced("DEBUG")]
3854 fn test_sync_fuses_partial_page_rewrite() {
3855 let executor = deterministic::Runner::default();
3856 executor.start(|_: deterministic::Context| async move {
3857 let blob = SyncTrackingBlob::new();
3858 let blob = Arc::new(blob);
3859 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3860 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3861 .await
3862 .unwrap();
3863 append = append.sync().await.unwrap();
3864
3865 (append, _) = append.append(b"abc").await.unwrap();
3867 append = append.sync().await.unwrap();
3868
3869 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3870 assert_eq!(writes, 1);
3871 assert_eq!(full_syncs, 1);
3872 assert_eq!(range_syncs, 1);
3873
3874 let slot0_offset = PAGE_SIZE.get() as u64;
3875 let slot1_offset = slot0_offset + CHECKSUM_SLOT_SIZE as u64;
3876 let slot0_before: Vec<u8> = blob
3877 .read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3878 .await
3879 .unwrap()
3880 .coalesce()
3881 .freeze()
3882 .into();
3883
3884 (append, _) = append.append(b"de").await.unwrap();
3888 append = append.sync().await.unwrap();
3889
3890 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3891 assert_eq!(writes, 2);
3892 assert_eq!(full_syncs, 1);
3893 assert_eq!(range_syncs, 2);
3894
3895 let slot0_after: Vec<u8> = blob
3896 .read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3897 .await
3898 .unwrap()
3899 .coalesce()
3900 .freeze()
3901 .into();
3902 assert_eq!(
3903 slot0_before, slot0_after,
3904 "protected slot must be resubmitted byte-identically"
3905 );
3906 let slot1_between: Vec<u8> = blob
3907 .read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3908 .await
3909 .unwrap()
3910 .coalesce()
3911 .freeze()
3912 .into();
3913
3914 (append, _) = append.append(b"fg").await.unwrap();
3916 append.sync().await.unwrap();
3917
3918 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3919 assert_eq!(writes, 3);
3920 assert_eq!(full_syncs, 1);
3921 assert_eq!(range_syncs, 3);
3922
3923 let slot1_after: Vec<u8> = blob
3924 .read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3925 .await
3926 .unwrap()
3927 .coalesce()
3928 .freeze()
3929 .into();
3930 assert_eq!(
3931 slot1_between, slot1_after,
3932 "protected slot must be resubmitted byte-identically"
3933 );
3934
3935 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3936 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3937 .await
3938 .unwrap();
3939 let read = reopened.read_at(0, 7).await.unwrap().coalesce();
3940 assert_eq!(read.as_ref(), b"abcdefg");
3941 });
3942 }
3943
3944 #[test_traced("DEBUG")]
3945 fn test_read_up_to_zero_len_truncates_buffer() {
3946 let executor = deterministic::Runner::default();
3947 executor.start(|context: deterministic::Context| async move {
3948 let (blob, blob_size) = context
3950 .open("test_partition", b"read_up_to_zero_len")
3951 .await
3952 .unwrap();
3953 assert_eq!(blob_size, 0);
3954
3955 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3957
3958 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
3960 .await
3961 .unwrap();
3962 (append, _) = append.append(&[1, 2, 3, 4]).await.unwrap();
3963
3964 let stale = vec![9u8, 8, 7, 6];
3966 let (buf, read) = append.read_up_to(0, 0, stale).await.unwrap();
3967
3968 assert_eq!(read, 0);
3969 assert_eq!(buf.len(), 0, "read_up_to must truncate returned buffer");
3970 assert_eq!(buf.freeze().as_ref(), b"");
3971 });
3972 }
3973
3974 fn read_crc_record_from_page(page_bytes: &[u8]) -> Checksum {
3976 let crc_start = page_bytes.len() - CHECKSUM_SIZE as usize;
3977 Checksum::read(&mut Copying(&page_bytes[crc_start..])).unwrap()
3978 }
3979
3980 struct PartialWriteBlob<B: Blob> {
3982 inner: B,
3983 writes: Arc<AtomicUsize>,
3984 read_options: Arc<Mutex<Vec<ReadOptions>>>,
3985 failed_write_len: Arc<AtomicUsize>,
3986 fail_on: usize,
3987 partial_len: usize,
3988 }
3989
3990 impl<B: Blob> PartialWriteBlob<B> {
3991 fn new(inner: B, fail_on: usize, partial_len: usize) -> Self {
3992 Self {
3993 inner,
3994 writes: Arc::new(AtomicUsize::new(0)),
3995 read_options: Arc::new(Mutex::new(Vec::new())),
3996 failed_write_len: Arc::new(AtomicUsize::new(0)),
3997 fail_on,
3998 partial_len,
3999 }
4000 }
4001
4002 fn failed_write_len(&self) -> Arc<AtomicUsize> {
4003 self.failed_write_len.clone()
4004 }
4005
4006 fn write_count(&self) -> Arc<AtomicUsize> {
4007 self.writes.clone()
4008 }
4009
4010 fn read_options(&self) -> Arc<Mutex<Vec<ReadOptions>>> {
4011 self.read_options.clone()
4012 }
4013 }
4014
4015 impl<B: Blob> crate::Blob for PartialWriteBlob<B> {
4016 async fn read_at(
4017 &self,
4018 offset: u64,
4019 len: usize,
4020 options: ReadOptions,
4021 ) -> Result<IoBufsMut, Error> {
4022 self.read_options.lock().push(options);
4023 self.inner.read_at(offset, len, options).await
4024 }
4025
4026 async fn read_at_buf(
4027 &self,
4028 offset: u64,
4029 len: usize,
4030 bufs: impl Into<IoBufsMut> + Send,
4031 options: ReadOptions,
4032 ) -> Result<IoBufsMut, Error> {
4033 self.read_options.lock().push(options);
4034 self.inner.read_at_buf(offset, len, bufs, options).await
4035 }
4036
4037 async fn write_at(
4038 &self,
4039 offset: u64,
4040 bufs: impl Into<IoBufs> + Send,
4041 options: WriteOptions,
4042 ) -> Result<(), Error> {
4043 let bufs = bufs.into();
4044 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
4045 if write == self.fail_on {
4046 let bytes = bufs.coalesce();
4047 self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
4048 let partial_len = self.partial_len.min(bytes.len());
4049 self.inner
4050 .write_at(offset, bytes.slice(..partial_len), options)
4051 .await?;
4052 if !options.contains(WriteOptions::SYNC) {
4053 self.inner.sync().await?;
4054 }
4055 return Err(Error::Io(
4056 std::io::Error::other("injected partial write").into(),
4057 ));
4058 }
4059
4060 self.inner.write_at(offset, bufs, options).await
4061 }
4062
4063 async fn resize(&self, len: u64) -> Result<(), Error> {
4064 self.inner.resize(len).await
4065 }
4066
4067 async fn sync(&self) -> Result<(), Error> {
4068 self.inner.sync().await
4069 }
4070
4071 async fn start_sync(&self) -> Handle<()> {
4072 self.inner.start_sync().await
4073 }
4074 }
4075
4076 struct TornExtensionBlob<B: Blob> {
4078 inner: B,
4079 writes: Arc<AtomicUsize>,
4080 fail_on: usize,
4081 durable_payload_len: usize,
4082 }
4083
4084 impl<B: Blob> crate::Blob for TornExtensionBlob<B> {
4085 async fn read_at(
4086 &self,
4087 offset: u64,
4088 len: usize,
4089 options: ReadOptions,
4090 ) -> Result<IoBufsMut, Error> {
4091 self.inner.read_at(offset, len, options).await
4092 }
4093
4094 async fn read_at_buf(
4095 &self,
4096 offset: u64,
4097 len: usize,
4098 bufs: impl Into<IoBufsMut> + Send,
4099 options: ReadOptions,
4100 ) -> Result<IoBufsMut, Error> {
4101 self.inner.read_at_buf(offset, len, bufs, options).await
4102 }
4103
4104 async fn write_at(
4105 &self,
4106 offset: u64,
4107 bufs: impl Into<IoBufs> + Send,
4108 options: WriteOptions,
4109 ) -> Result<(), Error> {
4110 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
4111 if write != self.fail_on {
4112 return self.inner.write_at(offset, bufs, options).await;
4113 }
4114
4115 let bytes = bufs.into().coalesce();
4116 let footer_start = bytes
4117 .len()
4118 .checked_sub(CHECKSUM_SIZE as usize)
4119 .expect("physical page must contain a checksum footer");
4120 assert!(self.durable_payload_len <= footer_start);
4121 let footer_offset = offset
4122 .checked_add(footer_start as u64)
4123 .ok_or(Error::OffsetOverflow)?;
4124 let write_options = options.without(WriteOptions::SYNC);
4125
4126 self.inner
4127 .write_at(
4128 offset,
4129 bytes.slice(..self.durable_payload_len),
4130 write_options,
4131 )
4132 .await?;
4133 self.inner
4134 .write_at(footer_offset, bytes.slice(footer_start..), write_options)
4135 .await?;
4136 self.inner.sync().await?;
4137
4138 Err(Error::Io(
4139 std::io::Error::other("injected torn extension").into(),
4140 ))
4141 }
4142
4143 async fn resize(&self, len: u64) -> Result<(), Error> {
4144 self.inner.resize(len).await
4145 }
4146
4147 async fn sync(&self) -> Result<(), Error> {
4148 self.inner.sync().await
4149 }
4150
4151 async fn start_sync(&self) -> Handle<()> {
4152 self.inner.start_sync().await
4153 }
4154 }
4155
4156 #[test_traced("DEBUG")]
4157 fn test_torn_extension_with_new_footer_recovers_previous_prefix() {
4158 let executor = deterministic::Runner::default();
4159 executor.start(|context: deterministic::Context| async move {
4160 let (blob, blob_size) = context
4161 .open("test_partition", b"torn_extension_footer")
4162 .await
4163 .unwrap();
4164 let blob = Arc::new(blob);
4165 let write_count = Arc::new(AtomicUsize::new(0));
4166 let faulty_blob = TornExtensionBlob {
4167 inner: blob.clone(),
4168 writes: write_count.clone(),
4169 fail_on: 2,
4170 durable_payload_len: 4,
4171 };
4172 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4173 let mut writer = Writer::new(faulty_blob, blob_size, BUFFER_SIZE, cache_ref)
4174 .await
4175 .unwrap();
4176
4177 (writer, _) = writer.append(b"abc").await.unwrap();
4178 writer = writer.sync().await.unwrap();
4179 assert_eq!(write_count.load(Ordering::SeqCst), 1);
4180
4181 (writer, _) = writer.append(b"def").await.unwrap();
4182 assert!(writer.sync().await.is_err(), "extension write should fail");
4183 assert_eq!(write_count.load(Ordering::SeqCst), 2);
4184
4185 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4186 let page = blob
4187 .read_at(0, physical_page_size, ReadOptions::default())
4188 .await
4189 .unwrap()
4190 .coalesce();
4191 assert_eq!(&page.as_ref()[..6], b"abcd\0\0");
4192 let checksum = read_crc_record_from_page(page.as_ref());
4193 assert_eq!(checksum.len1, 3);
4194 assert_eq!(checksum.len2, 6);
4195
4196 drop(blob);
4197 let (blob, blob_size) = context
4198 .open("test_partition", b"torn_extension_footer")
4199 .await
4200 .unwrap();
4201 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4202 let recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4203 .await
4204 .unwrap();
4205 assert_eq!(recovered.size(), 3);
4206
4207 assert_eq!(
4210 recovered
4211 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
4212 .await
4213 .unwrap(),
4214 3
4215 );
4216 let data = recovered.read_at(0, 3).await.unwrap().coalesce();
4217 assert_eq!(data.as_ref(), b"abc");
4218 });
4219 }
4220
4221 struct DelayedReadBlob<B: Blob> {
4223 inner: B,
4224 offset: u64,
4225 len: usize,
4226 reads: AtomicUsize,
4227 started: Mutex<Option<oneshot::Sender<()>>>,
4228 release: Mutex<Option<oneshot::Receiver<()>>>,
4229 }
4230
4231 impl<B: Blob> DelayedReadBlob<B> {
4232 const fn new(
4233 inner: B,
4234 offset: u64,
4235 len: usize,
4236 started: oneshot::Sender<()>,
4237 release: oneshot::Receiver<()>,
4238 ) -> Self {
4239 Self {
4240 inner,
4241 offset,
4242 len,
4243 reads: AtomicUsize::new(0),
4244 started: Mutex::new(Some(started)),
4245 release: Mutex::new(Some(release)),
4246 }
4247 }
4248 }
4249
4250 impl<B: Blob> crate::Blob for DelayedReadBlob<B> {
4251 async fn read_at(
4252 &self,
4253 offset: u64,
4254 len: usize,
4255 options: ReadOptions,
4256 ) -> Result<IoBufsMut, Error> {
4257 if offset == self.offset
4258 && len == self.len
4259 && self.reads.fetch_add(1, Ordering::SeqCst) == 0
4260 {
4261 let bytes = self.inner.read_at(offset, len, options).await?;
4262
4263 let sender = self
4264 .started
4265 .lock()
4266 .take()
4267 .expect("delayed read start signal consumed more than once");
4268 let _ = sender.send(());
4269
4270 let release = self
4271 .release
4272 .lock()
4273 .take()
4274 .expect("delayed read release receiver consumed more than once");
4275 release.await.expect("release signal dropped");
4276
4277 return Ok(bytes);
4278 }
4279
4280 self.inner.read_at(offset, len, options).await
4281 }
4282
4283 async fn read_at_buf(
4284 &self,
4285 offset: u64,
4286 len: usize,
4287 bufs: impl Into<IoBufsMut> + Send,
4288 options: ReadOptions,
4289 ) -> Result<IoBufsMut, Error> {
4290 if offset == self.offset
4291 && len == self.len
4292 && self.reads.fetch_add(1, Ordering::SeqCst) == 0
4293 {
4294 let bytes = self.inner.read_at_buf(offset, len, bufs, options).await?;
4295
4296 let sender = self
4297 .started
4298 .lock()
4299 .take()
4300 .expect("delayed read start signal consumed more than once");
4301 let _ = sender.send(());
4302
4303 let release = self
4304 .release
4305 .lock()
4306 .take()
4307 .expect("delayed read release receiver consumed more than once");
4308 release.await.expect("release signal dropped");
4309
4310 return Ok(bytes);
4311 }
4312
4313 self.inner.read_at_buf(offset, len, bufs, options).await
4314 }
4315
4316 async fn write_at(
4317 &self,
4318 offset: u64,
4319 bufs: impl Into<IoBufs> + Send,
4320 options: WriteOptions,
4321 ) -> Result<(), Error> {
4322 self.inner.write_at(offset, bufs, options).await
4323 }
4324
4325 async fn resize(&self, len: u64) -> Result<(), Error> {
4326 self.inner.resize(len).await
4327 }
4328
4329 async fn sync(&self) -> Result<(), Error> {
4330 self.inner.sync().await
4331 }
4332
4333 async fn start_sync(&self) -> Handle<()> {
4334 self.inner.start_sync().await
4335 }
4336 }
4337
4338 const DUMMY_MARKER: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
4341
4342 #[rstest::rstest]
4344 #[case(103)]
4345 #[case(4084)]
4346 #[case(4096)]
4347 fn test_to_physical_pages_preserves_chunk_owners(#[case] page_size: u16) {
4348 deterministic::Runner::default().start(|context| async move {
4349 let page_size = NonZeroU16::new(page_size).unwrap();
4350 let cache = CacheRef::new(page_size, NZUsize!(4));
4351 let (blob, size) = context
4352 .open("test_partition", b"physical_chunks")
4353 .await
4354 .unwrap();
4355 let page_size = usize::from(page_size.get());
4356 let capacity = page_size * 32;
4357 let writer = Writer::new(blob, size, capacity, cache).await.unwrap();
4358
4359 let data: Vec<_> = (0..page_size * 17 + 17).map(|i| (i % 251) as u8).collect();
4361 let mut buffer = Buffer::from(0, &[], capacity, page_size);
4362 for chunk in data.chunks(page_size) {
4363 assert!(!buffer.append(chunk));
4364 }
4365 assert!(buffer.parts().0.chunk_count() >= 4);
4366
4367 let (prefix, tail) = buffer.parts();
4369 let pointers: Vec<_> = prefix
4370 .iter()
4371 .map(AsRef::as_ref)
4372 .chain(std::iter::once(tail))
4373 .flat_map(|chunk| chunk.chunks_exact(page_size))
4374 .map(|page| page.as_ptr())
4375 .collect();
4376
4377 let full_pages = buffer.drain_full_pages();
4380 let durable = ActiveChecksum::new(Slot::Second, 7, Crc32::checksum(&data[..7]));
4381 let (physical, partial) =
4382 writer.to_physical_pages(&full_pages, buffer.parts().1, true, None, Some(&durable));
4383 assert_eq!(physical.chunk_count(), 17 * 2 + 1);
4384 assert_eq!(partial.unwrap().len, 17);
4385 for (page, ptr) in pointers.into_iter().enumerate() {
4386 assert_eq!(physical.chunk_at(page * 2).unwrap().as_ptr(), ptr);
4387 }
4388
4389 let physical = physical.coalesce();
4392 let physical_size = page_size + CHECKSUM_SIZE as usize;
4393 for (idx, page) in physical.as_ref().chunks_exact(physical_size).enumerate() {
4394 let logical = &data[idx * page_size..data.len().min((idx + 1) * page_size)];
4395 assert_eq!(&page[..logical.len()], logical);
4396 assert!(page[logical.len()..page_size].iter().all(|byte| *byte == 0));
4397 assert_eq!(
4398 Checksum::validate_page(page).unwrap().len as usize,
4399 logical.len()
4400 );
4401 let record = read_crc_record_from_page(page);
4402 if idx == 0 {
4403 assert_eq!(record.get_slot(Slot::Second), (durable.len, durable.crc));
4404 } else {
4405 assert_eq!(record.get_slot(Slot::Second), (0, 0));
4406 }
4407 }
4408 });
4409 }
4410
4411 #[test_traced("DEBUG")]
4417 fn test_crc_slot1_protected() {
4418 let executor = deterministic::Runner::default();
4419 executor.start(|context: deterministic::Context| async move {
4420 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4421 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4422 let slot0_offset = PAGE_SIZE.get() as u64;
4423 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4424
4425 let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
4427 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4428 .await
4429 .unwrap();
4430 (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
4431 append = append.sync().await.unwrap();
4432 drop(append);
4433
4434 let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
4436 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4437 .await
4438 .unwrap();
4439 (append, _) = append
4440 .append(&(11..=30).collect::<Vec<u8>>())
4441 .await
4442 .unwrap();
4443 append = append.sync().await.unwrap();
4444 drop(append);
4445
4446 let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
4448 let page = blob
4449 .read_at(0, physical_page_size, ReadOptions::default())
4450 .await
4451 .unwrap()
4452 .coalesce();
4453 let crc = read_crc_record_from_page(page.as_ref());
4454 assert!(
4455 crc.len2 > crc.len1,
4456 "Slot 1 should be authoritative (len2={} > len1={})",
4457 crc.len2,
4458 crc.len1
4459 );
4460
4461 let slot1_before: Vec<u8> = blob
4463 .read_at(slot1_offset, 6, ReadOptions::default())
4464 .await
4465 .unwrap()
4466 .coalesce()
4467 .freeze()
4468 .into();
4469
4470 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4472 .await
4473 .unwrap();
4474 blob.sync().await.unwrap();
4475
4476 let slot0_mangled: Vec<u8> = blob
4478 .read_at(slot0_offset, 6, ReadOptions::default())
4479 .await
4480 .unwrap()
4481 .coalesce()
4482 .freeze()
4483 .into();
4484 assert_eq!(slot0_mangled, DUMMY_MARKER, "Mangle failed");
4485
4486 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4488 .await
4489 .unwrap();
4490 (append, _) = append
4491 .append(&(31..=50).collect::<Vec<u8>>())
4492 .await
4493 .unwrap();
4494 append = append.sync().await.unwrap();
4495 drop(append);
4496
4497 let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
4499
4500 let slot0_after: Vec<u8> = blob
4502 .read_at(slot0_offset, 6, ReadOptions::default())
4503 .await
4504 .unwrap()
4505 .coalesce()
4506 .freeze()
4507 .into();
4508 assert_ne!(
4509 slot0_after, DUMMY_MARKER,
4510 "Slot 0 should have been overwritten with new CRC"
4511 );
4512
4513 let slot1_after: Vec<u8> = blob
4515 .read_at(slot1_offset, 6, ReadOptions::default())
4516 .await
4517 .unwrap()
4518 .coalesce()
4519 .freeze()
4520 .into();
4521 assert_eq!(
4522 slot1_before, slot1_after,
4523 "Slot 1 was modified! Protected region violated."
4524 );
4525
4526 let page = blob
4528 .read_at(0, physical_page_size, ReadOptions::default())
4529 .await
4530 .unwrap()
4531 .coalesce();
4532 let crc = read_crc_record_from_page(page.as_ref());
4533 assert_eq!(crc.len1, 50, "Slot 0 should have len=50");
4534 });
4535 }
4536
4537 #[test_traced("DEBUG")]
4543 fn test_crc_slot0_protected() {
4544 let executor = deterministic::Runner::default();
4545 executor.start(|context: deterministic::Context| async move {
4546 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4547 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4548 let slot0_offset = PAGE_SIZE.get() as u64;
4549 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4550
4551 let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
4553 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4554 .await
4555 .unwrap();
4556 (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
4557 append = append.sync().await.unwrap();
4558 drop(append);
4559
4560 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4562 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4563 .await
4564 .unwrap();
4565 (append, _) = append
4566 .append(&(11..=30).collect::<Vec<u8>>())
4567 .await
4568 .unwrap();
4569 append = append.sync().await.unwrap();
4570 drop(append);
4571
4572 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4574 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4575 .await
4576 .unwrap();
4577 (append, _) = append
4578 .append(&(31..=50).collect::<Vec<u8>>())
4579 .await
4580 .unwrap();
4581 append = append.sync().await.unwrap();
4582 drop(append);
4583
4584 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4586 let page = blob
4587 .read_at(0, physical_page_size, ReadOptions::default())
4588 .await
4589 .unwrap()
4590 .coalesce();
4591 let crc = read_crc_record_from_page(page.as_ref());
4592 assert!(
4593 crc.len1 > crc.len2,
4594 "Slot 0 should be authoritative (len1={} > len2={})",
4595 crc.len1,
4596 crc.len2
4597 );
4598
4599 let slot0_before: Vec<u8> = blob
4601 .read_at(slot0_offset, 6, ReadOptions::default())
4602 .await
4603 .unwrap()
4604 .coalesce()
4605 .freeze()
4606 .into();
4607
4608 blob.write_at(slot1_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4610 .await
4611 .unwrap();
4612 blob.sync().await.unwrap();
4613
4614 let slot1_mangled: Vec<u8> = blob
4616 .read_at(slot1_offset, 6, ReadOptions::default())
4617 .await
4618 .unwrap()
4619 .coalesce()
4620 .freeze()
4621 .into();
4622 assert_eq!(slot1_mangled, DUMMY_MARKER, "Mangle failed");
4623
4624 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4626 .await
4627 .unwrap();
4628 (append, _) = append
4629 .append(&(51..=70).collect::<Vec<u8>>())
4630 .await
4631 .unwrap();
4632 append = append.sync().await.unwrap();
4633 drop(append);
4634
4635 let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
4637
4638 let slot1_after: Vec<u8> = blob
4640 .read_at(slot1_offset, 6, ReadOptions::default())
4641 .await
4642 .unwrap()
4643 .coalesce()
4644 .freeze()
4645 .into();
4646 assert_ne!(
4647 slot1_after, DUMMY_MARKER,
4648 "Slot 1 should have been overwritten with new CRC"
4649 );
4650
4651 let slot0_after: Vec<u8> = blob
4653 .read_at(slot0_offset, 6, ReadOptions::default())
4654 .await
4655 .unwrap()
4656 .coalesce()
4657 .freeze()
4658 .into();
4659 assert_eq!(
4660 slot0_before, slot0_after,
4661 "Slot 0 was modified! Protected region violated."
4662 );
4663
4664 let page = blob
4666 .read_at(0, physical_page_size, ReadOptions::default())
4667 .await
4668 .unwrap()
4669 .coalesce();
4670 let crc = read_crc_record_from_page(page.as_ref());
4671 assert_eq!(crc.len2, 70, "Slot 1 should have len=70");
4672 });
4673 }
4674
4675 #[test_traced("DEBUG")]
4682 fn test_data_prefix_preserved_when_extending() {
4683 let executor = deterministic::Runner::default();
4684 executor.start(|context: deterministic::Context| async move {
4685 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4686 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4687
4688 let (blob, _) = context
4690 .open("test_partition", b"prefix_test")
4691 .await
4692 .unwrap();
4693 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4694 .await
4695 .unwrap();
4696 let data1: Vec<u8> = (1..=20).collect();
4697 (append, _) = append.append(&data1).await.unwrap();
4698 append = append.sync().await.unwrap();
4699 drop(append);
4700
4701 let (blob, size) = context
4703 .open("test_partition", b"prefix_test")
4704 .await
4705 .unwrap();
4706 assert_eq!(size, physical_page_size as u64);
4707
4708 let prefix_before: Vec<u8> = blob
4709 .read_at(0, 20, ReadOptions::default())
4710 .await
4711 .unwrap()
4712 .coalesce()
4713 .freeze()
4714 .into();
4715
4716 blob.write_at(25, DUMMY_MARKER.to_vec(), WriteOptions::default())
4718 .await
4719 .unwrap();
4720 blob.sync().await.unwrap();
4721
4722 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4724 .await
4725 .unwrap();
4726 (append, _) = append
4727 .append(&(21..=40).collect::<Vec<u8>>())
4728 .await
4729 .unwrap();
4730 append = append.sync().await.unwrap();
4731 drop(append);
4732
4733 let (blob, _) = context
4735 .open("test_partition", b"prefix_test")
4736 .await
4737 .unwrap();
4738
4739 let prefix_after: Vec<u8> = blob
4741 .read_at(0, 20, ReadOptions::default())
4742 .await
4743 .unwrap()
4744 .coalesce()
4745 .freeze()
4746 .into();
4747 assert_eq!(prefix_before, prefix_after, "Data prefix was modified!");
4748
4749 let overwritten: Vec<u8> = blob
4751 .read_at(25, 6, ReadOptions::default())
4752 .await
4753 .unwrap()
4754 .coalesce()
4755 .freeze()
4756 .into();
4757 assert_eq!(
4758 overwritten,
4759 vec![26, 27, 28, 29, 30, 31],
4760 "New data should overwrite padding area"
4761 );
4762 });
4763 }
4764
4765 #[test_traced("DEBUG")]
4771 fn test_crc_slot_protection_across_page_boundary() {
4772 let executor = deterministic::Runner::default();
4773 executor.start(|context: deterministic::Context| async move {
4774 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4775 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4776 let slot0_offset = PAGE_SIZE.get() as u64;
4777 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4778
4779 let (blob, _) = context.open("test_partition", b"boundary").await.unwrap();
4781 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4782 .await
4783 .unwrap();
4784 (append, _) = append.append(&(1..=50).collect::<Vec<u8>>()).await.unwrap();
4785 append = append.sync().await.unwrap();
4786 drop(append);
4787
4788 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4790 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4791 .await
4792 .unwrap();
4793 (append, _) = append
4794 .append(&(51..=80).collect::<Vec<u8>>())
4795 .await
4796 .unwrap();
4797 append = append.sync().await.unwrap();
4798 drop(append);
4799
4800 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4802 let page = blob
4803 .read_at(0, physical_page_size, ReadOptions::default())
4804 .await
4805 .unwrap()
4806 .coalesce();
4807 let crc = read_crc_record_from_page(page.as_ref());
4808 assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
4809
4810 let slot1_before: Vec<u8> = blob
4812 .read_at(slot1_offset, 6, ReadOptions::default())
4813 .await
4814 .unwrap()
4815 .coalesce()
4816 .freeze()
4817 .into();
4818
4819 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4821 .await
4822 .unwrap();
4823 blob.sync().await.unwrap();
4824
4825 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4827 .await
4828 .unwrap();
4829 (append, _) = append
4830 .append(&(81..=120).collect::<Vec<u8>>())
4831 .await
4832 .unwrap();
4833 append = append.sync().await.unwrap();
4834 drop(append);
4835
4836 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4838 assert_eq!(size, (physical_page_size * 2) as u64, "Should have 2 pages");
4839
4840 let slot0_after: Vec<u8> = blob
4842 .read_at(slot0_offset, 6, ReadOptions::default())
4843 .await
4844 .unwrap()
4845 .coalesce()
4846 .freeze()
4847 .into();
4848 assert_ne!(
4849 slot0_after, DUMMY_MARKER,
4850 "Slot 0 should have full-page CRC"
4851 );
4852
4853 let slot1_after: Vec<u8> = blob
4855 .read_at(slot1_offset, 6, ReadOptions::default())
4856 .await
4857 .unwrap()
4858 .coalesce()
4859 .freeze()
4860 .into();
4861 assert_eq!(
4862 slot1_before, slot1_after,
4863 "Slot 1 was modified during page boundary crossing!"
4864 );
4865
4866 let page0 = blob
4868 .read_at(0, physical_page_size, ReadOptions::default())
4869 .await
4870 .unwrap()
4871 .coalesce();
4872 let crc0 = read_crc_record_from_page(page0.as_ref());
4873 assert_eq!(
4874 crc0.len1,
4875 PAGE_SIZE.get(),
4876 "Slot 0 should have full page length"
4877 );
4878
4879 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4881 .await
4882 .unwrap();
4883 assert_eq!(append.size(), 120);
4884 let all_data: Vec<u8> = append.read_at(0, 120).await.unwrap().coalesce().into();
4885 let expected: Vec<u8> = (1..=120).collect();
4886 assert_eq!(all_data, expected);
4887 });
4888 }
4889
4890 #[test_traced("DEBUG")]
4899 fn test_crc_fallback_on_corrupted_primary() {
4900 let executor = deterministic::Runner::default();
4901 executor.start(|context: deterministic::Context| async move {
4902 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4903 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4904
4905 let crc2_offset = PAGE_SIZE.get() as u64 + 8;
4907
4908 let (blob, _) = context
4910 .open("test_partition", b"crc_fallback")
4911 .await
4912 .unwrap();
4913 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4914 .await
4915 .unwrap();
4916 let data1: Vec<u8> = (1..=10).collect();
4917 (append, _) = append.append(&data1).await.unwrap();
4918 append = append.sync().await.unwrap();
4919 drop(append);
4920
4921 let (blob, size) = context
4923 .open("test_partition", b"crc_fallback")
4924 .await
4925 .unwrap();
4926 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4927 .await
4928 .unwrap();
4929 (append, _) = append
4930 .append(&(11..=30).collect::<Vec<u8>>())
4931 .await
4932 .unwrap();
4933 append = append.sync().await.unwrap();
4934 drop(append);
4935
4936 let (blob, size) = context
4938 .open("test_partition", b"crc_fallback")
4939 .await
4940 .unwrap();
4941 let blob = Arc::new(blob);
4942 assert_eq!(size, physical_page_size as u64);
4943
4944 let page = blob
4945 .read_at(0, physical_page_size, ReadOptions::default())
4946 .await
4947 .unwrap()
4948 .coalesce();
4949 let crc = read_crc_record_from_page(page.as_ref());
4950 assert!(
4951 crc.len2 > crc.len1,
4952 "Slot 1 should be authoritative (len2={} > len1={})",
4953 crc.len2,
4954 crc.len1
4955 );
4956 assert_eq!(crc.len2, 30, "Slot 1 should have len=30");
4957 assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
4958
4959 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
4961 .await
4962 .unwrap();
4963 assert_eq!(append.size(), 30);
4964 let all_data: Vec<u8> = append.read_at(0, 30).await.unwrap().coalesce().into();
4965 let expected: Vec<u8> = (1..=30).collect();
4966 assert_eq!(all_data, expected);
4967 drop(append);
4968
4969 blob.write_at(
4972 crc2_offset,
4973 vec![0xDE, 0xAD, 0xBE, 0xEF],
4974 WriteOptions::default(),
4975 )
4976 .await
4977 .unwrap();
4978 blob.sync().await.unwrap();
4979
4980 let page = blob
4982 .read_at(0, physical_page_size, ReadOptions::default())
4983 .await
4984 .unwrap()
4985 .coalesce();
4986 let crc = read_crc_record_from_page(page.as_ref());
4987 assert_eq!(crc.len2, 30, "len2 should still be 30 after corruption");
4988 assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be our corrupted value");
4989
4990 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4992 .await
4993 .unwrap();
4994
4995 assert_eq!(
4997 append.size(),
4998 10,
4999 "Should fall back to slot 0's 10 bytes after primary CRC corruption"
5000 );
5001
5002 let fallback_data: Vec<u8> = append.read_at(0, 10).await.unwrap().coalesce().into();
5004 assert_eq!(
5005 fallback_data, data1,
5006 "Fallback data should match original 10 bytes"
5007 );
5008
5009 let result = append.read_at(0, 11).await;
5011 assert!(result.is_err(), "Reading beyond fallback size should fail");
5012 });
5013 }
5014
5015 #[test_traced("DEBUG")]
5027 fn test_non_last_page_rejects_partial_fallback() {
5028 let executor = deterministic::Runner::default();
5029 executor.start(|context: deterministic::Context| async move {
5030 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5031 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
5032
5033 let page0_crc2_offset = PAGE_SIZE.get() as u64 + 8;
5035
5036 let (blob, _) = context
5038 .open("test_partition", b"non_last_page")
5039 .await
5040 .unwrap();
5041 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
5042 .await
5043 .unwrap();
5044 (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
5045 append = append.sync().await.unwrap();
5046 drop(append);
5047
5048 let (blob, size) = context
5050 .open("test_partition", b"non_last_page")
5051 .await
5052 .unwrap();
5053 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5054 .await
5055 .unwrap();
5056 (append, _) = append
5058 .append(&(11..=PAGE_SIZE.get() as u8).collect::<Vec<u8>>())
5059 .await
5060 .unwrap();
5061 append = append.sync().await.unwrap();
5062 drop(append);
5063
5064 let (blob, size) = context
5066 .open("test_partition", b"non_last_page")
5067 .await
5068 .unwrap();
5069 let page = blob
5070 .read_at(0, physical_page_size, ReadOptions::default())
5071 .await
5072 .unwrap()
5073 .coalesce();
5074 let crc = read_crc_record_from_page(page.as_ref());
5075 assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
5076 assert_eq!(
5077 crc.len2,
5078 PAGE_SIZE.get(),
5079 "Slot 1 should have len=103 (full page)"
5080 );
5081 assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
5082
5083 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5085 .await
5086 .unwrap();
5087 (append, _) = append
5089 .append(&(104..=113).collect::<Vec<u8>>())
5090 .await
5091 .unwrap();
5092 append = append.sync().await.unwrap();
5093 drop(append);
5094
5095 let (blob, size) = context
5097 .open("test_partition", b"non_last_page")
5098 .await
5099 .unwrap();
5100 let blob = Arc::new(blob);
5101 assert_eq!(
5102 size,
5103 (physical_page_size * 2) as u64,
5104 "Should have 2 physical pages"
5105 );
5106
5107 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
5109 .await
5110 .unwrap();
5111 assert_eq!(append.size(), 113);
5112 let all_data: Vec<u8> = append.read_at(0, 113).await.unwrap().coalesce().into();
5113 let expected: Vec<u8> = (1..=113).collect();
5114 assert_eq!(all_data, expected);
5115 drop(append);
5116
5117 blob.write_at(
5119 page0_crc2_offset,
5120 vec![0xDE, 0xAD, 0xBE, 0xEF],
5121 WriteOptions::default(),
5122 )
5123 .await
5124 .unwrap();
5125 blob.sync().await.unwrap();
5126
5127 let page = blob
5129 .read_at(0, physical_page_size, ReadOptions::default())
5130 .await
5131 .unwrap()
5132 .coalesce();
5133 let crc = read_crc_record_from_page(page.as_ref());
5134 assert_eq!(crc.len2, PAGE_SIZE.get(), "len2 should still be 103");
5135 assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be corrupted");
5136 assert_eq!(crc.len1, 10, "Fallback slot 0 has partial length");
5138
5139 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5145 .await
5146 .unwrap();
5147
5148 assert_eq!(append.size(), 113);
5151
5152 let result = append.read_at(0, 10).await;
5155 assert!(
5156 result.is_err(),
5157 "Reading from corrupted non-last page via Append should fail, but got: {:?}",
5158 result
5159 );
5160 drop(append);
5161
5162 let (blob, size) = context
5164 .open("test_partition", b"non_last_page")
5165 .await
5166 .unwrap();
5167 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5168 .await
5169 .unwrap();
5170 let (_, mut replay) = append
5171 .replay(NZUsize!(1024), ReadOptions::default())
5172 .await
5173 .unwrap();
5174
5175 let result = replay.ensure(1).await;
5177 assert!(
5178 result.is_err(),
5179 "Reading from corrupted non-last page via Replay should fail, but got: {:?}",
5180 result
5181 );
5182 });
5183 }
5184
5185 #[test]
5190 fn test_replay_rejects_short_interior_page() {
5191 let executor = deterministic::Runner::default();
5192 executor.start(|context: deterministic::Context| async move {
5193 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5194 let page_size: u64 = PAGE_SIZE.widen();
5195
5196 let (blob, _) = context
5199 .open("test_partition", b"short_bound")
5200 .await
5201 .unwrap();
5202 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
5203 .await
5204 .unwrap();
5205 (append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
5206 append = append.sync().await.unwrap();
5207 (append, _) = append
5208 .append(&(11..=113).collect::<Vec<u8>>())
5209 .await
5210 .unwrap();
5211 append = append.sync().await.unwrap();
5212 drop(append);
5213
5214 let (blob, size) = context
5215 .open("test_partition", b"short_bound")
5216 .await
5217 .unwrap();
5218 let blob = Arc::new(blob);
5219
5220 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
5222 .await
5223 .unwrap();
5224 let (_, mut replay) = append
5225 .replay(NZUsize!(1024), ReadOptions::default())
5226 .await
5227 .unwrap();
5228 assert!(replay.ensure(113).await.unwrap());
5229 assert_eq!(replay.remaining(), 113);
5230 assert_eq!(
5231 replay.copy_to_bytes(113).as_ref(),
5232 (1..=113).collect::<Vec<u8>>()
5233 );
5234 drop(replay);
5235
5236 let page = blob
5239 .read_at(
5240 0,
5241 usize::from(PAGE_SIZE.get()) + CHECKSUM_SIZE as usize,
5242 ReadOptions::default(),
5243 )
5244 .await
5245 .unwrap()
5246 .coalesce();
5247 let crc = read_crc_record_from_page(page.as_ref());
5248 assert_eq!((crc.len1, crc.len2), (10, PAGE_SIZE.get()));
5249 blob.write_at(
5250 page_size + 8,
5251 vec![0xDE, 0xAD, 0xBE, 0xEF],
5252 WriteOptions::default(),
5253 )
5254 .await
5255 .unwrap();
5256 blob.sync().await.unwrap();
5257
5258 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5260 .await
5261 .unwrap();
5262 assert_eq!(append.size(), 113);
5263 let (_, mut replay) = append
5264 .replay(NZUsize!(1024), ReadOptions::default())
5265 .await
5266 .unwrap();
5267 assert_eq!(replay.blob_size(), 113);
5268 assert!(
5269 matches!(replay.ensure(50).await, Err(Error::InvalidChecksum)),
5270 "an interior page covering fewer bytes than a page must fail validation"
5271 );
5272
5273 replay.seek_to(40).unwrap();
5275 assert!(matches!(
5276 replay.ensure(1).await,
5277 Err(Error::InvalidChecksum)
5278 ));
5279 assert!(replay.chunk().is_empty());
5280 assert_eq!(replay.remaining(), 0);
5281 });
5282 }
5283
5284 #[test]
5285 fn test_recovery_truncate_partial_shrink_uses_uncached_read_hint() {
5286 let executor = deterministic::Runner::default();
5287 executor.start(|context| async move {
5288 let (blob, size) = context
5289 .open("test_partition", b"resize_read_options")
5290 .await
5291 .unwrap();
5292 let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
5293 let read_options = blob.read_options();
5294 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5295 let mut writer = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
5296 .await
5297 .unwrap();
5298
5299 let data = vec![7; PAGE_SIZE.get() as usize];
5300 (writer, _) = writer.append(&data).await.unwrap();
5301 writer = writer.sync().await.unwrap();
5302
5303 writer.truncate(50).await.unwrap();
5306
5307 assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE]);
5308 });
5309 }
5310
5311 #[test]
5312 fn test_recovery_truncate_shrink_validates_crc() {
5313 let executor = deterministic::Runner::default();
5316 executor.start(|context| async move {
5317 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5318 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
5319
5320 let (blob, size) = context
5321 .open("test_partition", b"resize_crc_test")
5322 .await
5323 .unwrap();
5324
5325 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5326 .await
5327 .unwrap();
5328
5329 let data: Vec<u8> = (0..=249).collect();
5332 (append, _) = append.append(&data).await.unwrap();
5333 append = append.sync().await.unwrap();
5334 assert_eq!(append.size(), 250);
5335 drop(append);
5336
5337 let (blob, size) = context
5339 .open("test_partition", b"resize_crc_test")
5340 .await
5341 .unwrap();
5342 assert_eq!(size as usize, physical_page_size * 3);
5343
5344 let page1_crc_offset = (physical_page_size * 2 - CHECKSUM_SIZE as usize) as u64;
5346 blob.write_at(
5347 page1_crc_offset,
5348 vec![0xFF; CHECKSUM_SIZE as usize],
5349 WriteOptions::default(),
5350 )
5351 .await
5352 .unwrap();
5353 blob.sync().await.unwrap();
5354
5355 let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5357 .await
5358 .unwrap();
5359 assert_eq!(append.size(), 250);
5360
5361 let result = append.truncate(150).await.err();
5365 assert!(
5366 matches!(result, Some(crate::Error::InvalidChecksum)),
5367 "Expected InvalidChecksum when shrinking to corrupted page, got: {:?}",
5368 result
5369 );
5370 });
5371 }
5372
5373 #[test]
5374 fn test_recovery_preserves_cached_prefix_on_publication() {
5375 let page = PAGE_SIZE.get() as u64;
5377 for end in [None, Some(page * 2 + 10), Some(page + 10)] {
5378 deterministic::Runner::default().start(|context| async move {
5379 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5381 let (blob, size) = context.open("recovery-cache", b"blob").await.unwrap();
5382 let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache)
5383 .await
5384 .unwrap();
5385 let page = PAGE_SIZE.get() as usize;
5386 (recovery, _) = recovery.append(&vec![0xA5; page * 2 + 20]).await.unwrap();
5387 recovery = recovery.sync().await.unwrap();
5388 recovery.read_at(0, page).await.unwrap();
5389
5390 if let Some(end) = end {
5392 recovery = recovery.truncate(end).await.unwrap();
5393 }
5394
5395 let writer: Writer<_> = recovery.into();
5397 let mut cached = vec![0; page];
5398 assert!(writer.try_read_sync_into(&mut cached, 0));
5399 assert_eq!(cached, vec![0xA5; page]);
5400 });
5401 }
5402 }
5403
5404 #[test]
5405 fn test_recovery_truncate_direct_append_replaces_cached_pages() {
5406 deterministic::Runner::default().start(|context| async move {
5407 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(2));
5409 let (blob, size) = context.open("recovery-cache", b"direct").await.unwrap();
5410 let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache.clone())
5411 .await
5412 .unwrap();
5413 let page = PAGE_SIZE.get() as usize;
5414 let mut model: Vec<u8> = (0..5 * page + 20).map(|i| (i % 251) as u8).collect();
5415 (recovery, _) = recovery.append(&model).await.unwrap();
5416 recovery = recovery.sync().await.unwrap();
5417 cache.clear();
5418 recovery.read_at(page as u64, page).await.unwrap();
5419 recovery.read_at((2 * page) as u64, page).await.unwrap();
5420
5421 recovery = recovery.truncate((page + 10) as u64).await.unwrap();
5423 model.truncate(page + 10);
5424 let replacement = vec![0xCC; 3 * page + 7];
5425 (recovery, _) = recovery
5426 .append_owned(replacement.clone().into())
5427 .await
5428 .unwrap();
5429 model.extend_from_slice(&replacement);
5430
5431 assert_eq!(
5433 recovery
5434 .read_at((2 * page) as u64, page)
5435 .await
5436 .unwrap()
5437 .coalesce()
5438 .as_ref(),
5439 &model[2 * page..3 * page]
5440 );
5441 assert_eq!(
5442 recovery
5443 .read_at(0, model.len())
5444 .await
5445 .unwrap()
5446 .coalesce()
5447 .as_ref(),
5448 model
5449 );
5450 recovery = recovery.sync().await.unwrap();
5451
5452 let writer: Writer<_> = recovery.into();
5454 let (mut writer, snapshot) = writer.snapshot().await.unwrap();
5455 (writer, _) = writer.append(&vec![0xDD; page - 17]).await.unwrap();
5456 writer = writer.sync().await.unwrap();
5457 assert_eq!(
5458 snapshot
5459 .read_at(0, model.len())
5460 .await
5461 .unwrap()
5462 .coalesce()
5463 .as_ref(),
5464 model
5465 );
5466 model.extend_from_slice(&vec![0xDD; page - 17]);
5467 assert_eq!(
5468 writer
5469 .read_at(0, model.len())
5470 .await
5471 .unwrap()
5472 .coalesce()
5473 .as_ref(),
5474 model
5475 );
5476 });
5477 }
5478
5479 #[test]
5480 fn test_cancelled_recovery_read_cannot_repopulate_after_truncate() {
5481 let cfg =
5482 deterministic::Config::default().with_timeout(Some(std::time::Duration::from_secs(5)));
5483 deterministic::Runner::new(cfg).start(|context| async move {
5484 let page = PAGE_SIZE.get() as usize;
5486 let physical = page + CHECKSUM_SIZE as usize;
5487 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(1));
5488 let (inner, size) = context.open("recovery-cache", b"cancelled").await.unwrap();
5489 let (started_tx, started_rx) = oneshot::channel();
5490 let (release_tx, release_rx) = oneshot::channel();
5491 let blob =
5492 DelayedReadBlob::new(inner, physical as u64, physical, started_tx, release_rx);
5493 let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache.clone())
5494 .await
5495 .unwrap();
5496 (recovery, _) = recovery.append(&vec![0xAA; 3 * page + 20]).await.unwrap();
5497 recovery = recovery.sync().await.unwrap();
5498 cache.clear();
5499
5500 {
5503 let mut read = Box::pin(recovery.read_at(page as u64, page));
5504 commonware_macros::select! {
5505 _ = started_rx => {},
5506 _ = read.as_mut() => panic!("read completed before release"),
5507 }
5508 }
5509
5510 recovery = recovery.truncate((page + 10) as u64).await.unwrap();
5512 (recovery, _) = recovery.append(&vec![0xBB; page - 10]).await.unwrap();
5513 recovery = recovery.sync().await.unwrap();
5514 recovery.read_at(0, page).await.unwrap();
5515
5516 let _ = release_tx.send(());
5518 let actual = recovery
5519 .read_at(page as u64, page)
5520 .await
5521 .unwrap()
5522 .coalesce();
5523 assert_eq!(&actual.as_ref()[..10], &[0xAA; 10]);
5524 assert_eq!(&actual.as_ref()[10..], vec![0xBB; page - 10]);
5525 });
5526 }
5527
5528 #[test]
5529 fn test_recovery_truncate_replaces_cached_tail() {
5530 deterministic::Runner::default().start(|context| async move {
5531 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5532 let (blob, size) = context.open("recovery-cache", b"truncate").await.unwrap();
5533 let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache)
5534 .await
5535 .unwrap();
5536 let page = PAGE_SIZE.get() as usize;
5537 (recovery, _) = recovery.append(&vec![0xAA; page * 2 + 16]).await.unwrap();
5538 recovery = recovery.sync().await.unwrap();
5539 recovery.read_at(0, page * 2).await.unwrap();
5540 let mut cached = vec![0; page * 2];
5541 assert!(recovery.try_read_sync_into(&mut cached, 0));
5542 assert_eq!(cached, vec![0xAA; page * 2]);
5543
5544 recovery = recovery.truncate((page + 16) as u64).await.unwrap();
5545 recovery.read_at(0, page).await.unwrap();
5546 recovery = recovery.truncate((page + 8) as u64).await.unwrap();
5547 assert!(recovery.try_read_sync_into(&mut cached[..page], 0));
5548
5549 (recovery, _) = recovery.append(&vec![0xBB; page - 8]).await.unwrap();
5550 recovery = recovery.sync().await.unwrap();
5551
5552 let mut rewritten = vec![0; page];
5554 assert!(recovery.try_read_sync_into(&mut rewritten, page as u64));
5555 assert_eq!(&rewritten[..8], &[0xAA; 8]);
5556 assert_eq!(&rewritten[8..], vec![0xBB; page - 8]);
5557 });
5558 }
5559
5560 #[test]
5561 fn test_snapshot_fetch_populates_shared_cache_across_append() {
5562 let executor = deterministic::Runner::default();
5563 executor.start(|context: deterministic::Context| async move {
5564 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5566 let page_size = PAGE_SIZE.get() as usize;
5567 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
5568 let (inner, blob_size) = context
5569 .open("test_partition", b"snapshot_resize_cache")
5570 .await
5571 .unwrap();
5572 let (started_tx, started_rx) = oneshot::channel();
5573 let (release_tx, release_rx) = oneshot::channel();
5574 let blob = DelayedReadBlob::new(
5575 inner,
5576 physical_page_size as u64,
5577 physical_page_size,
5578 started_tx,
5579 release_rx,
5580 );
5581 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5582 .await
5583 .unwrap();
5584
5585 let old_page0 = vec![0x11u8; page_size];
5587 let old_page1 = vec![0x22u8; page_size];
5588 (writer, _) = writer.append(&old_page0).await.unwrap();
5589 (writer, _) = writer.append(&old_page1).await.unwrap();
5590 writer = writer.sync().await.unwrap();
5591
5592 writer.cache_ref.clear();
5593
5594 let (mut writer, snapshot) = writer.snapshot().await.unwrap();
5596 let snapshot_task = context
5597 .child("snapshot")
5598 .spawn(move |_| async move { snapshot.read_at(page_size as u64, page_size).await });
5599 started_rx.await.expect("snapshot read never started");
5600
5601 let new_page1 = vec![0x33u8; page_size];
5603 (writer, _) = writer.append(&new_page1).await.unwrap();
5604 writer = writer.sync().await.unwrap();
5605
5606 let _ = release_tx.send(());
5608 let stale = snapshot_task
5609 .await
5610 .expect("snapshot task failed")
5611 .expect("snapshot read failed")
5612 .coalesce();
5613 assert_eq!(stale.as_ref(), old_page1.as_slice());
5614
5615 let mut probe = vec![0u8; page_size];
5616 assert!(writer.try_read_sync_into(&mut probe, page_size as u64));
5617 assert_eq!(probe, old_page1);
5618 });
5619 }
5620
5621 #[test]
5622 fn test_append_preserves_snapshot_partial_page() {
5623 let executor = deterministic::Runner::default();
5624 executor.start(|context: deterministic::Context| async move {
5625 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5626 let (blob, blob_size) = context
5627 .open("test_partition", b"snapshot_blocks_shrink")
5628 .await
5629 .unwrap();
5630 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5631 .await
5632 .unwrap();
5633
5634 let page_size = PAGE_SIZE.get() as usize;
5635 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2 + 7).collect();
5636 (append, _) = append.append(&data).await.unwrap();
5637 append = append.sync().await.unwrap();
5638
5639 let tail_ptr = append.buffer.parts().1.as_ptr();
5640 let (mut append, snapshot) = append.snapshot().await.unwrap();
5641 let snapshot_clone = snapshot.clone();
5642 let snapshot_size = snapshot.size();
5643
5644 let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
5645 assert_eq!(read.as_ref(), data.as_slice());
5646
5647 (append, _) = append.append(&[0; 3]).await.unwrap();
5650 assert_eq!(append.size(), snapshot_size + 3);
5651 assert_eq!(append.buffer.parts().1.as_ptr(), tail_ptr);
5652
5653 let original = snapshot_clone
5654 .read_at(0, data.len())
5655 .await
5656 .unwrap()
5657 .coalesce();
5658 assert_eq!(original.as_ref(), data.as_slice());
5659 assert_eq!(snapshot_clone.size(), snapshot_size);
5660 });
5661 }
5662
5663 #[test]
5664 fn test_snapshot_read_many_into_matches_read_at() {
5665 let executor = deterministic::Runner::default();
5666 executor.start(|context: deterministic::Context| async move {
5667 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5668 let (blob, blob_size) = context
5669 .open("test_partition", b"snapshot_read_many")
5670 .await
5671 .unwrap();
5672 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5673 .await
5674 .unwrap();
5675
5676 let item_size = 4;
5677 let page_size = PAGE_SIZE.get() as usize;
5678 let total = page_size * 3 + 13;
5679 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
5680 (append, _) = append.append(&data).await.unwrap();
5681 append = append.sync().await.unwrap();
5682
5683 let (_, snapshot) = append.snapshot().await.unwrap();
5684
5685 let offsets = [
5687 0,
5688 (page_size - item_size) as u64,
5689 (page_size + 5) as u64,
5690 (page_size * 3 + 2) as u64,
5691 ];
5692 let mut batch = vec![0u8; offsets.len() * item_size];
5693 snapshot
5694 .read_many_into(&mut batch, &offsets, NZUsize!(item_size))
5695 .await
5696 .unwrap();
5697
5698 for (item, offset) in batch.chunks_exact(item_size).zip(offsets) {
5699 let single = snapshot
5700 .read_at(offset, item_size)
5701 .await
5702 .unwrap()
5703 .coalesce();
5704 assert_eq!(item, single.as_ref());
5705 assert_eq!(item, &data[offset as usize..offset as usize + item_size]);
5706 }
5707 });
5708 }
5709
5710 #[test]
5711 fn test_snapshot_try_read_sync_prefers_snapshot_tail() {
5712 let executor = deterministic::Runner::default();
5713 executor.start(|context: deterministic::Context| async move {
5714 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5715 let (blob, blob_size) = context
5716 .open("test_partition", b"snapshot_try_read_tail")
5717 .await
5718 .unwrap();
5719 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5720 .await
5721 .unwrap();
5722
5723 let page_size = PAGE_SIZE.get() as usize;
5724 (append, _) = append.append(&vec![0xAA; page_size]).await.unwrap();
5725 append = append.sync().await.unwrap();
5726
5727 let tail = b"oldtail";
5728 (append, _) = append.append(tail).await.unwrap();
5729 let (append, snapshot) = append.snapshot().await.unwrap();
5730
5731 let poison = vec![0xBB; page_size];
5733 assert_eq!(
5734 append.cache_ref.cache(append.id, &poison, page_size as u64),
5735 0
5736 );
5737
5738 let mut read = vec![0; tail.len()];
5739 assert!(snapshot.try_read_sync_into(&mut read, page_size as u64));
5740 assert_eq!(read.as_slice(), tail);
5741 });
5742 }
5743
5744 #[test]
5745 fn test_recovery_truncate_same_size_preserves_contents() {
5746 let executor = deterministic::Runner::default();
5747 executor.start(|context: deterministic::Context| async move {
5748 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5749 let (blob, blob_size) = context
5750 .open("test_partition", b"resize_same_size")
5751 .await
5752 .unwrap();
5753 let mut append = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
5754 .await
5755 .unwrap();
5756
5757 (append, _) = append.append(b"hello world").await.unwrap();
5758 assert_eq!(append.size(), 11);
5759
5760 append = append.truncate(11).await.unwrap();
5762 assert_eq!(append.size(), 11);
5763
5764 let read = append.read_at(0, 11).await.unwrap().coalesce();
5766 assert_eq!(read.as_ref(), b"hello world");
5767 });
5768 }
5769
5770 #[test]
5771 fn test_recovery_truncate_same_page_shrink_reopens_at_shorter_size() {
5772 let executor = deterministic::Runner::default();
5773 executor.start(|context| async move {
5774 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5775 let data: Vec<u8> = (0..50).collect();
5776
5777 let (blob, size) = context
5778 .open("test_partition", b"same_page_shrink")
5779 .await
5780 .unwrap();
5781 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5782 .await
5783 .unwrap();
5784
5785 (append, _) = append.append(&data).await.unwrap();
5787 append = append.sync().await.unwrap();
5788
5789 append = append.truncate(45).await.unwrap();
5791 drop(append);
5792
5793 let (blob, size) = context
5795 .open("test_partition", b"same_page_shrink")
5796 .await
5797 .unwrap();
5798 let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
5799 .await
5800 .unwrap();
5801 assert_eq!(append.size(), 45);
5802 let read = append.read_at(0, 45).await.unwrap().coalesce();
5803 assert_eq!(read.as_ref(), &data[..45]);
5804 });
5805 }
5806
5807 #[test_traced("DEBUG")]
5811 fn test_shrink_then_torn_rewrite_does_not_resurrect() {
5812 let executor = deterministic::Runner::default();
5813 executor.start(|context: deterministic::Context| async move {
5814 let (blob, blob_size) = context
5815 .open("test_partition", b"shrink_torn")
5816 .await
5817 .unwrap();
5818 let blob = Arc::new(blob);
5819 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5820 let mut writer = Recovery::open(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
5821 .await
5822 .unwrap();
5823
5824 let data: Vec<u8> = (1u8..=80).collect();
5826 (writer, _) = writer.append(&data).await.unwrap();
5827 writer = writer.sync().await.unwrap();
5828 writer = writer.truncate(50).await.unwrap();
5829 drop(writer);
5830
5831 let page_size = u64::from(PAGE_SIZE.get());
5834 blob.write_at(
5835 page_size,
5836 80u16.to_be_bytes().to_vec(),
5837 WriteOptions::default(),
5838 )
5839 .await
5840 .unwrap();
5841 blob.sync().await.unwrap();
5842
5843 drop(blob);
5844 let (blob, blob_size) = context
5845 .open("test_partition", b"shrink_torn")
5846 .await
5847 .unwrap();
5848 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5849 let recovered = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
5850 .await
5851 .unwrap();
5852 assert_eq!(recovered.size(), 50);
5853 let read = recovered.read_at(0, 50).await.unwrap().coalesce();
5854 assert_eq!(read.as_ref(), &data[..50]);
5855 });
5856 }
5857
5858 #[test]
5859 fn test_recovery_truncate_same_page_shrink_survives_interrupted_crc_stage() {
5860 let executor = deterministic::Runner::default();
5861 executor.start(|context| async move {
5862 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5864 let data: Vec<u8> = (0..50).collect();
5865
5866 let (blob, size) = context
5867 .open("test_partition", b"same_page_shrink_interrupted")
5868 .await
5869 .unwrap();
5870 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
5871 .await
5872 .unwrap();
5873 (append, _) = append.append(&data[..40]).await.unwrap();
5874 append = append.sync().await.unwrap();
5875 (append, _) = append.append(&data[40..]).await.unwrap();
5876 append = append.sync().await.unwrap();
5877 drop(append);
5878
5879 let (blob, size) = context
5881 .open("test_partition", b"same_page_shrink_interrupted")
5882 .await
5883 .unwrap();
5884 let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
5885 let write_count = faulty_blob.write_count();
5886 let failed_write_len = faulty_blob.failed_write_len();
5887 let append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5888 .await
5889 .unwrap();
5890
5891 assert!(
5892 append.truncate(45).await.is_err(),
5893 "phase-1 partial write should fail"
5894 );
5895 assert_eq!(write_count.load(Ordering::SeqCst), 1);
5896 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
5897
5898 let (blob, size) = context
5900 .open("test_partition", b"same_page_shrink_interrupted")
5901 .await
5902 .unwrap();
5903 let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
5904 .await
5905 .unwrap();
5906 assert_eq!(append.size(), 50);
5907 let read = append.read_at(0, 50).await.unwrap().coalesce();
5908 assert_eq!(read.as_ref(), &data);
5909 });
5910 }
5911
5912 #[test]
5913 fn test_recovery_truncate_same_page_shrink_survives_interrupted_len_stage() {
5914 let executor = deterministic::Runner::default();
5915 executor.start(|context| async move {
5916 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
5918 const LARGE_BUFFER_SIZE: usize = 1_200;
5919
5920 let cache_ref = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
5921 let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
5922
5923 let (blob, size) = context
5924 .open("test_partition", b"same_page_shrink_len_stage")
5925 .await
5926 .unwrap();
5927 let mut append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5928 .await
5929 .unwrap();
5930 (append, _) = append.append(&data[..255]).await.unwrap();
5931 append = append.sync().await.unwrap();
5932 (append, _) = append.append(&data[255..]).await.unwrap();
5933 append = append.sync().await.unwrap();
5934 drop(append);
5935
5936 let (blob, size) = context
5938 .open("test_partition", b"same_page_shrink_len_stage")
5939 .await
5940 .unwrap();
5941 let faulty_blob = PartialWriteBlob::new(blob, 2, 1);
5942 let write_count = faulty_blob.write_count();
5943 let failed_write_len = faulty_blob.failed_write_len();
5944 let append = Recovery::open(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5945 .await
5946 .unwrap();
5947
5948 assert!(
5949 append.truncate(257).await.is_err(),
5950 "length-stage partial write should fail"
5951 );
5952 assert_eq!(write_count.load(Ordering::SeqCst), 2);
5953 assert_eq!(failed_write_len.load(Ordering::SeqCst), 2);
5954
5955 let (blob, size) = context
5957 .open("test_partition", b"same_page_shrink_len_stage")
5958 .await
5959 .unwrap();
5960 let append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref)
5961 .await
5962 .unwrap();
5963 assert_eq!(append.size(), 300);
5964 let read = append.read_at(0, 300).await.unwrap().coalesce();
5965 assert_eq!(read.as_ref(), &data);
5966 });
5967 }
5968
5969 #[test]
5970 fn test_recovery_truncate_same_page_shrink_preserves_validated_fallback_slot() {
5971 let executor = deterministic::Runner::default();
5972 executor.start(|context| async move {
5973 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5975 let data: Vec<u8> = (0..52).collect();
5976
5977 let (blob, size) = context
5978 .open("test_partition", b"same_page_shrink_fallback_slot")
5979 .await
5980 .unwrap();
5981 let blob = Arc::new(blob);
5982 let faulty_blob = PartialWriteBlob::new(blob.clone(), 4, 3);
5983 let write_count = faulty_blob.write_count();
5984 let failed_write_len = faulty_blob.failed_write_len();
5985 let mut append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5986 .await
5987 .unwrap();
5988 (append, _) = append.append(&data[..48]).await.unwrap();
5989 append = append.sync().await.unwrap();
5990 assert_eq!(write_count.load(Ordering::SeqCst), 1);
5991
5992 (append, _) = append.append(&data[48..50]).await.unwrap();
5993 append = append.sync().await.unwrap();
5994 assert_eq!(write_count.load(Ordering::SeqCst), 2);
5995
5996 (append, _) = append.append(&data[50..]).await.unwrap();
5997 append = append.sync().await.unwrap();
5998 assert_eq!(write_count.load(Ordering::SeqCst), 3);
5999
6000 let slot0_offset = PAGE_SIZE.get() as u64;
6004 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
6005 .await
6006 .unwrap();
6007 blob.sync().await.unwrap();
6008
6009 assert!(
6010 append.truncate(45).await.is_err(),
6011 "phase-1 partial write should fail"
6012 );
6013 assert_eq!(write_count.load(Ordering::SeqCst), 4);
6014 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
6015
6016 drop(blob);
6018 let (blob, size) = context
6019 .open("test_partition", b"same_page_shrink_fallback_slot")
6020 .await
6021 .unwrap();
6022 let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
6023 .await
6024 .unwrap();
6025 assert_eq!(append.size(), 50);
6026 let read = append.read_at(0, 50).await.unwrap().coalesce();
6027 assert_eq!(read.as_ref(), &data[..50]);
6028 });
6029 }
6030
6031 #[test]
6032 fn test_recovery_truncate_full_page_to_partial_reopens_at_shorter_size() {
6033 let executor = deterministic::Runner::default();
6034 executor.start(|context| async move {
6035 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6037 let page_size = PAGE_SIZE.get() as u64;
6038 let target = page_size + 45;
6039 let data: Vec<u8> = (0..page_size * 2).map(|i| (i % 251) as u8).collect();
6040
6041 let (blob, size) = context
6042 .open("test_partition", b"full_page_to_partial")
6043 .await
6044 .unwrap();
6045 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6046 .await
6047 .unwrap();
6048 (append, _) = append.append(&data).await.unwrap();
6049 append = append.sync().await.unwrap();
6050
6051 append = append.truncate(target).await.unwrap();
6053 drop(append);
6054
6055 let (blob, size) = context
6057 .open("test_partition", b"full_page_to_partial")
6058 .await
6059 .unwrap();
6060 let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
6061 .await
6062 .unwrap();
6063 assert_eq!(append.size(), target);
6064 let read = append.read_at(0, target as usize).await.unwrap().coalesce();
6065 assert_eq!(read.as_ref(), &data[..target as usize]);
6066 });
6067 }
6068
6069 #[test]
6070 fn test_recovery_truncate_full_page_to_partial_survives_interrupted_crc_stage() {
6071 let executor = deterministic::Runner::default();
6072 executor.start(|context| async move {
6073 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6075 let page_size = PAGE_SIZE.get() as u64;
6076 let target = page_size + 45;
6077 let data: Vec<u8> = (0..page_size * 3).map(|i| (i % 251) as u8).collect();
6078
6079 let (blob, size) = context
6080 .open("test_partition", b"full_page_to_partial_interrupted")
6081 .await
6082 .unwrap();
6083 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6084 .await
6085 .unwrap();
6086 (append, _) = append.append(&data).await.unwrap();
6087 append = append.sync().await.unwrap();
6088 drop(append);
6089
6090 let (blob, size) = context
6092 .open("test_partition", b"full_page_to_partial_interrupted")
6093 .await
6094 .unwrap();
6095 let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
6096 let write_count = faulty_blob.write_count();
6097 let failed_write_len = faulty_blob.failed_write_len();
6098 let append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
6099 .await
6100 .unwrap();
6101
6102 assert!(
6103 append.truncate(target).await.is_err(),
6104 "phase-1 partial write should fail"
6105 );
6106 assert_eq!(write_count.load(Ordering::SeqCst), 1);
6107 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
6108
6109 let (blob, size) = context
6111 .open("test_partition", b"full_page_to_partial_interrupted")
6112 .await
6113 .unwrap();
6114 let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
6115 .await
6116 .unwrap();
6117 assert_eq!(append.size(), page_size * 2);
6118 let read = append
6119 .read_at(0, (page_size * 2) as usize)
6120 .await
6121 .unwrap()
6122 .coalesce();
6123 assert_eq!(read.as_ref(), &data[..(page_size * 2) as usize]);
6124 });
6125 }
6126
6127 #[test]
6128 fn test_recovery_truncate_same_page_shrink_survives_interrupted_length_invalidation() {
6129 let executor = deterministic::Runner::default();
6130 executor.start(|context| async move {
6131 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
6133 const LARGE_BUFFER_SIZE: usize = 1_200;
6134
6135 let cache_ref = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
6136 let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
6137
6138 let (blob, size) = context
6139 .open(
6140 "test_partition",
6141 b"same_page_shrink_interrupted_len_invalidation",
6142 )
6143 .await
6144 .unwrap();
6145 let mut append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
6146 .await
6147 .unwrap();
6148
6149 (append, _) = append.append(&data[..255]).await.unwrap();
6151 append = append.sync().await.unwrap();
6152 (append, _) = append.append(&data[255..]).await.unwrap();
6153 append = append.sync().await.unwrap();
6154 drop(append);
6155
6156 let (blob, size) = context
6158 .open(
6159 "test_partition",
6160 b"same_page_shrink_interrupted_len_invalidation",
6161 )
6162 .await
6163 .unwrap();
6164 let faulty_blob = PartialWriteBlob::new(blob, 3, 1);
6165 let write_count = faulty_blob.write_count();
6166 let failed_write_len = faulty_blob.failed_write_len();
6167 let append = Recovery::open(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
6168 .await
6169 .unwrap();
6170
6171 assert!(
6172 append.truncate(40).await.is_err(),
6173 "old-slot length invalidation should fail"
6174 );
6175 assert_eq!(write_count.load(Ordering::SeqCst), 3);
6176 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
6177
6178 let (blob, size) = context
6180 .open(
6181 "test_partition",
6182 b"same_page_shrink_interrupted_len_invalidation",
6183 )
6184 .await
6185 .unwrap();
6186 let append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref)
6187 .await
6188 .unwrap();
6189 assert_eq!(append.size(), 40);
6190 let read = append.read_at(0, 40).await.unwrap().coalesce();
6191 assert_eq!(read.as_ref(), &data[..40]);
6192 });
6193 }
6194
6195 #[test_traced("DEBUG")]
6196 fn test_recovery_truncate_partial_shrink_without_physical_resize_uses_range_sync() {
6197 let executor = deterministic::Runner::default();
6198 executor.start(|_: deterministic::Context| async move {
6199 let blob = SyncTrackingBlob::new();
6200 let blob = Arc::new(blob);
6201 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6202 let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
6203 .await
6204 .unwrap();
6205 append = append.sync().await.unwrap();
6206
6207 let data = vec![5u8; PAGE_SIZE.get() as usize];
6208 (append, _) = append.append(&data).await.unwrap();
6209 append = append.sync().await.unwrap();
6210
6211 append = append.truncate(50).await.unwrap();
6213 append.sync().await.unwrap();
6214
6215 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6216 assert_eq!(writes, 4);
6217 assert_eq!(full_syncs, 1);
6218 assert_eq!(range_syncs, 4);
6219 });
6220 }
6221
6222 #[test_traced("DEBUG")]
6223 fn test_recovery_truncate_partial_shrink_with_physical_resize_clears_full_sync_requirement() {
6224 let executor = deterministic::Runner::default();
6225 executor.start(|_: deterministic::Context| async move {
6226 let blob = SyncTrackingBlob::new();
6227 let blob = Arc::new(blob);
6228 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6229 let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
6230 .await
6231 .unwrap();
6232 append = append.sync().await.unwrap();
6233
6234 let data = vec![9u8; PAGE_SIZE.get() as usize * 2];
6235 (append, _) = append.append(&data).await.unwrap();
6236 append = append.sync().await.unwrap();
6237
6238 append = append.truncate(50).await.unwrap();
6241 append = append.sync().await.unwrap();
6242
6243 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6244 assert_eq!(writes, 4);
6245 assert_eq!(full_syncs, 2);
6246 assert_eq!(range_syncs, 3);
6247
6248 (append, _) = append.append(b"x").await.unwrap();
6250 append = append.sync().await.unwrap();
6251
6252 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6253 assert_eq!(writes, 5);
6254 assert_eq!(full_syncs, 2);
6255 assert_eq!(range_syncs, 4);
6256
6257 let mut expected = data[..50].to_vec();
6258 expected.push(b'x');
6259 let read = append.read_at(0, expected.len()).await.unwrap().coalesce();
6260 assert_eq!(read.as_ref(), expected.as_slice());
6261 });
6262 }
6263
6264 #[test_traced("DEBUG")]
6265 fn test_recovery_truncate_page_boundary_shrink_uses_full_sync() {
6266 let executor = deterministic::Runner::default();
6267 executor.start(|_: deterministic::Context| async move {
6268 let blob = SyncTrackingBlob::new();
6269 let blob = Arc::new(blob);
6270 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6271 let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
6272 .await
6273 .unwrap();
6274 append = append.sync().await.unwrap();
6275
6276 let page_size = PAGE_SIZE.get() as usize;
6279 let data = vec![11u8; page_size * 2];
6280 (append, _) = append.append(&data).await.unwrap();
6281 append = append.sync().await.unwrap();
6282
6283 append = append.truncate(PAGE_SIZE.get() as u64).await.unwrap();
6286 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6287 assert_eq!(writes, 1);
6288 assert_eq!(full_syncs, 2);
6289 assert_eq!(range_syncs, 1);
6290
6291 append.sync().await.unwrap();
6293 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
6294 assert_eq!(writes, 1);
6295 assert_eq!(full_syncs, 2);
6296 assert_eq!(range_syncs, 1);
6297
6298 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6299 let reopened = Recovery::open(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
6300 .await
6301 .unwrap();
6302 assert_eq!(reopened.size(), PAGE_SIZE.get() as u64);
6303 let read = reopened.read_at(0, page_size).await.unwrap().coalesce();
6304 assert_eq!(read.as_ref(), &data[..page_size]);
6305 });
6306 }
6307
6308 #[test]
6309 fn test_recovery_partial_tail_append_and_truncate() {
6310 let executor = deterministic::Runner::default();
6311 executor.start(|context| async move {
6312 const PAGE_SIZE: NonZeroU16 = NZU16!(64);
6313 const BUFFER_SIZE: usize = 256;
6314
6315 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(4));
6316
6317 let (blob, size) = context
6318 .open("test_partition", b"partial_tail_test")
6319 .await
6320 .unwrap();
6321
6322 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6323 .await
6324 .unwrap();
6325
6326 (append, _) = append.append(&[1, 2, 3, 4, 5]).await.unwrap();
6328 append = append.sync().await.unwrap();
6329 assert_eq!(append.size(), 5);
6330 drop(append);
6331
6332 let (blob, size) = context
6333 .open("test_partition", b"partial_tail_test")
6334 .await
6335 .unwrap();
6336
6337 let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
6338 .await
6339 .unwrap();
6340 assert_eq!(append.size(), 5);
6341
6342 (append, _) = append.append(&[6, 7, 8]).await.unwrap();
6343 append = append.truncate(6).await.unwrap();
6344 append = append.sync().await.unwrap();
6345
6346 let data: Vec<u8> = append.read_at(0, 6).await.unwrap().coalesce().into();
6347 assert_eq!(data, vec![1, 2, 3, 4, 5, 6]);
6348 });
6349 }
6350
6351 #[test]
6352 fn test_corrupted_crc_len_too_large() {
6353 let executor = deterministic::Runner::default();
6354 executor.start(|context| async move {
6355 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6356 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
6357
6358 let (blob, size) = context
6360 .open("test_partition", b"crc_len_test")
6361 .await
6362 .unwrap();
6363
6364 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
6365 .await
6366 .unwrap();
6367
6368 (append, _) = append.append(&[0x42; 50]).await.unwrap();
6369 append = append.sync().await.unwrap();
6370 drop(append);
6371
6372 let (blob, size) = context
6374 .open("test_partition", b"crc_len_test")
6375 .await
6376 .unwrap();
6377 assert_eq!(size as usize, physical_page_size);
6378
6379 let crc_offset = PAGE_SIZE.get() as u64;
6381
6382 let bad_crc_record: [u8; 12] = [
6385 0xFF, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
6390 blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
6391 .await
6392 .unwrap();
6393 blob.sync().await.unwrap();
6394
6395 let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
6397
6398 match result {
6401 Ok(append) => {
6402 let recovered_size = append.size();
6404 assert_eq!(
6405 recovered_size, 0,
6406 "Corrupted page should be truncated, size should be 0"
6407 );
6408 }
6409 Err(e) => {
6410 assert!(
6412 matches!(e, crate::Error::InvalidChecksum),
6413 "Expected InvalidChecksum error, got: {:?}",
6414 e
6415 );
6416 }
6417 }
6418 });
6419 }
6420
6421 #[test]
6422 fn test_corrupted_crc_both_slots_len_too_large() {
6423 let executor = deterministic::Runner::default();
6424 executor.start(|context| async move {
6425 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6426
6427 let (blob, size) = context
6429 .open("test_partition", b"crc_both_bad")
6430 .await
6431 .unwrap();
6432
6433 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
6434 .await
6435 .unwrap();
6436
6437 (append, _) = append.append(&[0x42; 50]).await.unwrap();
6438 append = append.sync().await.unwrap();
6439 drop(append);
6440
6441 let (blob, size) = context
6443 .open("test_partition", b"crc_both_bad")
6444 .await
6445 .unwrap();
6446
6447 let crc_offset = PAGE_SIZE.get() as u64;
6448
6449 let bad_crc_record: [u8; 12] = [
6451 0x01, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x02, 0x00, 0xCA, 0xFE, 0xBA, 0xBE, ];
6456 blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
6457 .await
6458 .unwrap();
6459 blob.sync().await.unwrap();
6460
6461 let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
6463
6464 match result {
6465 Ok(append) => {
6466 assert_eq!(append.size(), 0);
6468 }
6469 Err(e) => {
6470 assert!(
6471 matches!(e, crate::Error::InvalidChecksum),
6472 "Expected InvalidChecksum, got: {:?}",
6473 e
6474 );
6475 }
6476 }
6477 });
6478 }
6479
6480 #[test_traced("DEBUG")]
6482 fn test_reader_sees_buffered_bytes() {
6483 let executor = deterministic::Runner::default();
6484 executor.start(|context: deterministic::Context| async move {
6485 let (blob, blob_size) = context.open("test_partition", b"rdr_buf").await.unwrap();
6486 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6487 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
6488 .await
6489 .unwrap();
6490
6491 let data: Vec<u8> = (0u8..50).collect();
6492 (writer, _) = writer.append(&data).await.unwrap();
6493
6494 assert_eq!(writer.size(), 50);
6496 let read = writer.read_at(0, 50).await.unwrap().coalesce();
6497 assert_eq!(read.as_ref(), data.as_slice());
6498
6499 let mut buf = vec![0u8; 10];
6500 assert!(writer.try_read_sync_into(&mut buf, 20));
6501 assert_eq!(buf, data[20..30]);
6502 });
6503 }
6504
6505 #[test_traced("DEBUG")]
6507 fn test_recovery_read_past_truncated_end_errors_cleanly() {
6508 let executor = deterministic::Runner::default();
6509 executor.start(|context: deterministic::Context| async move {
6510 let (blob, blob_size) = context.open("test_partition", b"rdr_rsz").await.unwrap();
6511 let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
6512 let mut writer = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
6513 .await
6514 .unwrap();
6515
6516 let page_size = PAGE_SIZE.get() as usize;
6517 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
6518 (writer, _) = writer.append(&data).await.unwrap();
6519 writer = writer.sync().await.unwrap();
6520 assert_eq!(writer.size(), (page_size * 2) as u64);
6521
6522 let new_size = (page_size / 2) as u64;
6524 writer = writer.truncate(new_size).await.unwrap();
6525
6526 let err = writer
6528 .read_at(new_size, page_size)
6529 .await
6530 .expect_err("read past truncated end must fail");
6531 assert!(matches!(err, Error::BlobInsufficientLength));
6532
6533 let read = writer
6535 .read_at(0, new_size as usize)
6536 .await
6537 .unwrap()
6538 .coalesce();
6539 assert_eq!(read.as_ref(), &data[..new_size as usize]);
6540 });
6541 }
6542}