1use super::{
41 read::{PageReader, Replay},
42 view::View,
43};
44use crate::{
45 Blob, Error, Handle, IoBuf, IoBufMut, IoBufs, ReadOptions, WriteOptions,
46 buffer::{
47 SyncState,
48 paged::{ActiveChecksum, CHECKSUM_SIZE, CacheRef, Checksum, Slot},
49 tip::Buffer,
50 },
51};
52use bytes::BufMut;
53use commonware_cryptography::Crc32;
54use commonware_utils::Widen;
55use std::num::{NonZeroU16, NonZeroUsize};
56use tracing::warn;
57
58fn adjusted_capacity(capacity: usize, page_size: u64) -> usize {
66 let page_size = page_size as usize;
67 let rounded = capacity.next_multiple_of(page_size);
68 let floor = page_size * 2;
69 if rounded < floor {
70 warn!(
71 floor,
72 "requested buffer capacity is too low, increasing it to floor"
73 );
74 }
75 rounded.max(floor)
76}
77
78const fn too_big_for_buffer(
86 buffer_len: usize,
87 buffer_capacity: usize,
88 append_len: usize,
89 page_size: usize,
90) -> bool {
91 let fill = buffer_len.next_multiple_of(page_size) - buffer_len;
92 let overflows_capacity = buffer_len + append_len > buffer_capacity;
93 let has_full_page_after_fill = append_len >= fill + page_size;
94
95 overflows_capacity && has_full_page_after_fill
96}
97
98pub struct Writer<B: Blob> {
100 blob: B,
102
103 current_page: u64,
105
106 partial_page_state: Option<ActiveChecksum>,
108
109 durable_page_state: Option<ActiveChecksum>,
117
118 sync_state: SyncState,
120
121 id: u64,
123
124 cache_ref: CacheRef,
126
127 buffer: Buffer,
130}
131
132impl<B: Blob> Writer<B> {
133 pub async fn new(
142 blob: B,
143 original_blob_size: u64,
144 capacity: usize,
145 cache_ref: CacheRef,
146 ) -> Result<Self, Error> {
147 let page_size: u64 = cache_ref.page_size().widen();
148 let (partial_page_state, pages, invalid_data_found) =
149 Self::read_last_valid_page(&blob, original_blob_size, page_size).await?;
150 if invalid_data_found {
151 let new_blob_size = pages * (page_size + CHECKSUM_SIZE);
153 warn!(
154 original_blob_size,
155 new_blob_size, "truncating blob to remove invalid data"
156 );
157 blob.resize(new_blob_size).await?;
158 blob.sync().await?;
159 }
160
161 let capacity = adjusted_capacity(capacity, page_size);
162 let needs_sync = !invalid_data_found; let (current_page, partial_page_state, partial_data) = match partial_page_state {
165 Some((partial_page, crc_record)) => (pages - 1, Some(crc_record), Some(partial_page)),
166 None => (pages, None, None),
167 };
168
169 let buffer = Buffer::from(
170 current_page * page_size,
171 partial_data.unwrap_or_default(),
172 capacity,
173 cache_ref.pool().clone(),
174 );
175
176 Ok(Self {
177 blob,
178 current_page,
179 partial_page_state,
180 durable_page_state: partial_page_state,
181 sync_state: if needs_sync {
182 SyncState::Dirty
183 } else {
184 SyncState::Clean
185 },
186 id: cache_ref.next_id(),
187 cache_ref,
188 buffer,
189 })
190 }
191
192 async fn read_last_valid_page(
211 blob: &B,
212 blob_size: u64,
213 page_size: u64,
214 ) -> Result<(Option<(IoBuf, ActiveChecksum)>, u64, bool), Error> {
215 let physical_page_size = page_size + CHECKSUM_SIZE;
216 let partial_bytes = blob_size % physical_page_size;
217 let mut last_page_end = blob_size - partial_bytes;
218
219 let mut invalid_data_found = partial_bytes != 0;
222
223 while last_page_end != 0 {
224 let page_start = last_page_end - physical_page_size;
227 let buf = blob
228 .read_at(
229 page_start,
230 physical_page_size as usize,
231 ReadOptions::default(),
232 )
233 .await?
234 .coalesce()
235 .freeze();
236
237 match Checksum::validate_page(buf.as_ref()) {
238 Some(checksum) => {
239 let len = checksum.len as u64;
241 if len != page_size {
242 let logical_bytes = buf.slice(..len as usize);
244 return Ok((
245 Some((logical_bytes, checksum)),
246 last_page_end / physical_page_size,
247 invalid_data_found,
248 ));
249 }
250 return Ok((None, last_page_end / physical_page_size, invalid_data_found));
252 }
253 None => {
254 last_page_end = page_start;
256 invalid_data_found = true;
257 }
258 }
259 }
260
261 Ok((None, 0, invalid_data_found))
263 }
264
265 pub async fn append(&mut self, buf: &[u8]) -> Result<u64, Error> {
268 let page_size: usize = self.cache_ref.page_size().widen();
269
270 if too_big_for_buffer(
272 self.buffer.len(),
273 self.buffer.capacity,
274 buf.len(),
275 page_size,
276 ) {
277 return self.append_owned(IoBuf::copy_from_slice(buf)).await;
278 }
279
280 let offset = self.buffer.size();
281 if self.buffer.append(buf) {
282 self.flush_internal(false, false).await?;
283 }
284 Ok(offset)
285 }
286
287 pub async fn append_owned(&mut self, buf: IoBuf) -> Result<u64, Error> {
293 let page_size: usize = self.cache_ref.page_size().widen();
294 let offset = self.buffer.size();
295
296 if !too_big_for_buffer(
298 self.buffer.len(),
299 self.buffer.capacity,
300 buf.len(),
301 page_size,
302 ) {
303 if self.buffer.append(buf.as_ref()) {
304 self.flush_internal(false, false).await?;
305 }
306 return Ok(offset);
307 }
308
309 let fill = self.buffer.len().next_multiple_of(page_size) - self.buffer.len();
311
312 if fill > 0 {
315 self.buffer.append(&buf.as_ref()[..fill]);
316 }
317 let boundary = self.buffer.size();
318 if !self.buffer.is_empty() {
319 self.flush_internal(false, false).await?;
320 assert!(
321 self.buffer.size() == boundary && self.buffer.is_empty(),
322 "flush left unexpected buffered bytes before a direct-path append"
323 );
324 }
325
326 let bulk_len = (buf.len() - fill) / page_size * page_size;
329 let bulk = buf.slice(fill..fill + bulk_len);
330 let mut physical_pages = IoBufs::default();
331 self.append_full_pages(&bulk, None, &mut physical_pages);
332
333 assert!(
334 self.partial_page_state.is_none(),
335 "an empty tip implies no partial page state"
336 );
337
338 self.sync_state.wait_for_pending().await?;
340
341 let chunk_len = self.buffer.capacity;
346 let mut cache_offset = boundary;
347 for chunk in bulk.as_ref().chunks(chunk_len) {
348 let remaining = self.cache_ref.cache(self.id, chunk, cache_offset);
349 assert_eq!(remaining, 0, "cached bulk pages must be page-aligned");
350 cache_offset += chunk.len() as u64;
351 }
352
353 self.current_page += (bulk_len / page_size) as u64;
358 let suffix = buf.slice(fill + bulk_len..);
359 let suffix = if suffix.is_empty() {
360 suffix
361 } else {
362 let mut copied = self.cache_ref.pool().alloc(suffix.len());
363 copied.put_slice(suffix.as_ref());
364 copied.freeze()
365 };
366 self.buffer.replace(boundary + bulk_len as u64, suffix);
367
368 let page_size: u64 = self.cache_ref.page_size().widen();
370 assert_eq!(self.current_page * page_size, self.buffer.offset);
371
372 let physical_page_size = page_size + CHECKSUM_SIZE;
373 let write_at_offset = boundary / page_size * physical_page_size;
374 self.sync_state
375 .write_at(
376 &self.blob,
377 write_at_offset,
378 physical_pages,
379 WriteOptions::DONT_CACHE,
380 )
381 .await?;
382
383 Ok(offset)
384 }
385
386 fn partial_page_dirty(
391 full_pages: usize,
392 partial_len: usize,
393 flushed: Option<&ActiveChecksum>,
394 ) -> bool {
395 partial_len != 0
396 && (full_pages > 0 || flushed.is_none_or(|state| state.len as usize != partial_len))
397 }
398
399 fn has_flush_work(&self, write_partial_page: bool) -> bool {
401 let page_size: usize = self.cache_ref.page_size().widen();
402 let full_pages = self.buffer.len() / page_size;
403 if full_pages > 0 {
404 return true;
405 }
406 write_partial_page
407 && Self::partial_page_dirty(
408 full_pages,
409 self.buffer.len() % page_size,
410 self.partial_page_state.as_ref(),
411 )
412 }
413
414 async fn flush_internal(
428 &mut self,
429 write_partial_page: bool,
430 sync: bool,
431 ) -> Result<bool, Error> {
432 if !self.has_flush_work(write_partial_page) {
435 return Ok(false);
436 }
437
438 self.sync_state.wait_for_pending().await?;
442 if self.sync_state.is_clean() {
443 self.durable_page_state = self.partial_page_state;
444 }
445
446 let (physical_pages, partial_page_state) = self.to_physical_pages(
451 &self.buffer,
452 write_partial_page,
453 self.partial_page_state.as_ref(),
454 self.durable_page_state.as_ref(),
455 );
456 assert!(
457 !physical_pages.is_empty(),
458 "flush work predicate must match physical page construction"
459 );
460
461 let page_size: usize = self.cache_ref.page_size().widen();
464 let pages_to_cache = self.buffer.len() / page_size;
465 let bytes_to_drain = pages_to_cache * page_size;
466
467 let cache_pages = if pages_to_cache > 0 {
469 Some((self.buffer.offset, self.buffer.slice(..bytes_to_drain)))
470 } else {
471 None
472 };
473
474 if bytes_to_drain == self.buffer.len() && bytes_to_drain != 0 {
477 let _ = self
478 .buffer
479 .take()
480 .expect("take must succeed when flush drains all buffered bytes");
481 } else if bytes_to_drain != 0 {
482 self.buffer.drop_prefix(bytes_to_drain);
483 self.buffer.offset += bytes_to_drain as u64;
484 }
485 let new_offset = self.buffer.offset;
486
487 if let Some((cache_offset, pages)) = cache_pages {
490 let remaining = self.cache_ref.cache(self.id, pages.as_ref(), cache_offset);
491 assert_eq!(remaining, 0, "cached full-page prefix must be page-aligned");
492 }
493
494 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
495 let write_at_offset = self.current_page * physical_page_size as u64;
496
497 self.current_page += pages_to_cache as u64;
501 self.partial_page_state = partial_page_state;
502 self.durable_page_state = if sync {
503 partial_page_state
505 } else if pages_to_cache > 0 {
506 None
508 } else {
509 self.durable_page_state
510 };
511
512 let page_size: u64 = self.cache_ref.page_size().widen();
514 assert_eq!(self.current_page * page_size, new_offset);
515
516 if sync {
519 self.sync_state
520 .write_at(
521 &self.blob,
522 write_at_offset,
523 physical_pages,
524 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
525 )
526 .await?;
527 } else {
528 self.sync_state
529 .write_at(
530 &self.blob,
531 write_at_offset,
532 physical_pages,
533 WriteOptions::DONT_CACHE,
534 )
535 .await?;
536 }
537 Ok(sync)
538 }
539
540 pub const fn size(&self) -> u64 {
542 self.buffer.size()
543 }
544
545 fn view(&self) -> View<'_, B> {
547 View {
548 blob: &self.blob,
549 cache_ref: &self.cache_ref,
550 id: self.id,
551 size: self.buffer.size(),
552 tail_offset: self.buffer.offset,
553 tail: self.buffer.as_ref(),
554 }
555 }
556
557 pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
561 self.view().try_read_sync_into(buf, offset)
562 }
563
564 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
566 self.view().read_at(offset, len).await
567 }
568
569 pub async fn read_up_to(
574 &self,
575 offset: u64,
576 len: usize,
577 bufs: impl Into<IoBufMut> + Send,
578 ) -> Result<(IoBufMut, usize), Error> {
579 self.view().read_up_to(offset, len, bufs).await
580 }
581
582 pub async fn read_many_into(
590 &self,
591 buf: &mut [u8],
592 offsets: &[u64],
593 item_size: NonZeroUsize,
594 ) -> Result<usize, Error> {
595 self.view().read_many_into(buf, offsets, item_size).await
596 }
597
598 pub fn try_read_many_sync_into(
601 &self,
602 buf: &mut [u8],
603 offsets: &[u64],
604 item_size: NonZeroUsize,
605 ) -> Vec<usize> {
606 self.view().try_read_many_sync_into(buf, offsets, item_size)
607 }
608
609 pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
612 self.view().try_read_ranges_sync_into(buf, ranges)
613 }
614
615 pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
617 self.view().read_into(buf, offset).await
618 }
619
620 fn to_physical_pages(
638 &self,
639 buffer: &Buffer,
640 include_partial_page: bool,
641 flushed: Option<&ActiveChecksum>,
642 durable: Option<&ActiveChecksum>,
643 ) -> (IoBufs, Option<ActiveChecksum>) {
644 let page_size: usize = self.cache_ref.page_size().widen();
645 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
646 let pages_to_write = buffer.len() / page_size;
647 let mut write_buffer = IoBufs::default();
648 let buffer_data = buffer.as_ref();
649
650 if pages_to_write > 0 {
651 self.append_full_pages(
652 &buffer.slice(..pages_to_write * page_size),
653 durable,
654 &mut write_buffer,
655 );
656 }
657
658 if !include_partial_page {
659 return (write_buffer, None);
660 }
661
662 let partial_page = &buffer_data[pages_to_write * page_size..];
663 if !Self::partial_page_dirty(pages_to_write, partial_page.len(), flushed) {
664 return (write_buffer, None);
665 }
666 let partial_len = partial_page.len();
667 let crc = Crc32::checksum(partial_page);
668
669 let durable = if pages_to_write == 0 { durable } else { None };
672 let (crc_record, active_checksum) =
673 Self::build_crc_record(partial_len as u16, crc, durable);
674
675 let mut padded = self.cache_ref.pool().alloc(physical_page_size);
678 padded.put_slice(partial_page);
679 let zero_count = page_size - partial_len;
680 if zero_count > 0 {
681 padded.put_bytes(0, zero_count);
682 }
683 padded.put_slice(&crc_record.to_bytes());
684 write_buffer.append(padded.freeze());
685
686 (write_buffer, Some(active_checksum))
687 }
688
689 fn append_full_pages(
695 &self,
696 data: &IoBuf,
697 old_checksum: Option<&ActiveChecksum>,
698 write_buffer: &mut IoBufs,
699 ) {
700 let page_size: usize = self.cache_ref.page_size().widen();
701 let pages = data.len() / page_size;
702 debug_assert!(pages > 0);
703 debug_assert_eq!(data.len() % page_size, 0);
704 let page_size_u16 =
705 u16::try_from(page_size).expect("page size must fit in u16 for CRC record");
706
707 let mut crcs = self.cache_ref.pool().alloc(CHECKSUM_SIZE as usize * pages);
710 let data_bytes = data.as_ref();
711 for page in 0..pages {
712 let start_read_idx = page * page_size;
713 let end_read_idx = start_read_idx + page_size;
714 let logical_page = &data_bytes[start_read_idx..end_read_idx];
715 let crc = Crc32::checksum(logical_page);
716
717 let old_checksum = if page == 0 { old_checksum } else { None };
720 let (crc_record, _) = Self::build_crc_record(page_size_u16, crc, old_checksum);
721 crcs.put_slice(&crc_record.to_bytes());
722 }
723 let crc_blob = crcs.freeze();
724
725 for page in 0..pages {
727 let start_read_idx = page * page_size;
728 let end_read_idx = start_read_idx + page_size;
729 write_buffer.append(data.slice(start_read_idx..end_read_idx));
730
731 let crc_start = page * CHECKSUM_SIZE as usize;
732 write_buffer.append(crc_blob.slice(crc_start..crc_start + CHECKSUM_SIZE as usize));
733 }
734 }
735
736 const fn build_crc_record(
739 new_len: u16,
740 new_crc: u32,
741 old_checksum: Option<&ActiveChecksum>,
742 ) -> (Checksum, ActiveChecksum) {
743 let Some(old_checksum) = old_checksum else {
744 return (
745 Checksum::new(new_len, new_crc),
746 ActiveChecksum::new(Slot::First, new_len, new_crc),
747 );
748 };
749
750 let new_slot = old_checksum.slot.other();
751 let record = match old_checksum.slot {
752 Slot::First => Checksum {
753 len1: old_checksum.len,
754 crc1: old_checksum.crc,
755 len2: new_len,
756 crc2: new_crc,
757 },
758 Slot::Second => Checksum {
759 len1: new_len,
760 crc1: new_crc,
761 len2: old_checksum.len,
762 crc2: old_checksum.crc,
763 },
764 };
765 (record, ActiveChecksum::new(new_slot, new_len, new_crc))
766 }
767
768 async fn sync_partial_page_shrink(
770 &mut self,
771 page: u64,
772 page_size: u64,
773 new_len: u16,
774 new_crc: u32,
775 old_checksum: &ActiveChecksum,
776 ) -> Result<ActiveChecksum, Error> {
777 let physical_page_size = page_size
782 .checked_add(CHECKSUM_SIZE)
783 .ok_or(Error::OffsetOverflow)?;
784 let crc_start = page
785 .checked_mul(physical_page_size)
786 .and_then(|start| start.checked_add(page_size))
787 .ok_or(Error::OffsetOverflow)?;
788 let old_slot = old_checksum.slot;
789 let new_slot = old_slot.other();
790
791 let new_slot_offset = crc_start
794 .checked_add(new_slot.offset() as u64)
795 .ok_or(Error::OffsetOverflow)?;
796 let staged_slot = Checksum::slot_bytes(0, new_crc);
797 self.sync_state
798 .write_at(
799 &self.blob,
800 new_slot_offset,
801 staged_slot.to_vec(),
802 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
803 )
804 .await?;
805
806 let published_len = Checksum::slot_len_bytes(new_len);
809 self.sync_state
810 .write_at(
811 &self.blob,
812 new_slot_offset,
813 published_len.to_vec(),
814 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
815 )
816 .await?;
817
818 let old_slot_offset = crc_start
823 .checked_add(old_slot.offset() as u64)
824 .ok_or(Error::OffsetOverflow)?;
825 self.sync_state
826 .write_at(
827 &self.blob,
828 old_slot_offset,
829 Checksum::slot_bytes(0, 0).to_vec(),
830 WriteOptions::SYNC | WriteOptions::DONT_CACHE,
831 )
832 .await?;
833
834 Ok(ActiveChecksum::new(new_slot, new_len, new_crc))
835 }
836
837 pub async fn replay(
847 &mut self,
848 buffer_size: NonZeroUsize,
849 read_options: ReadOptions,
850 ) -> Result<Replay<B>, Error> {
851 let page_size_nz = self.cache_ref.page_size();
852 let page_size: u64 = page_size_nz.widen();
853
854 self.flush_internal(true, false).await?;
856
857 let physical_page_size = page_size + CHECKSUM_SIZE;
859 let prefetch_pages = buffer_size.get() / physical_page_size as usize;
860 let prefetch_pages = prefetch_pages.max(1); let (physical_blob_size, logical_blob_size) = self.partial_page_state.as_ref().map_or_else(
864 || {
865 let physical = physical_page_size * self.current_page;
867 let logical = page_size * self.current_page;
868 (physical, logical)
869 },
870 |checksum| {
871 let partial_len = checksum.len as u64;
873 let physical = physical_page_size * (self.current_page + 1);
875 let logical = page_size * self.current_page + partial_len;
877 (physical, logical)
878 },
879 );
880
881 let reader = PageReader::new(
882 self.blob.clone(),
883 physical_blob_size,
884 logical_blob_size,
885 prefetch_pages,
886 page_size_nz,
887 read_options,
888 );
889 Ok(Replay::new(reader))
890 }
891
892 pub async fn snapshot(&mut self) -> Result<super::Sealed<B>, Error> {
901 self.flush_internal(true, false).await?;
902 Ok(self.sealed_handle(self.cache_ref.next_id()))
903 }
904
905 pub async fn sync(&mut self) -> Result<(), Error> {
910 if self.flush_internal(true, true).await? {
913 return Ok(());
914 }
915
916 self.sync_state.sync(&self.blob).await?;
919 self.durable_page_state = self.partial_page_state;
920 Ok(())
921 }
922
923 pub async fn start_sync(&mut self) -> Handle<()> {
930 if let Err(err) = self.flush_internal(true, false).await {
931 return Handle::ready(Err(err));
932 }
933 self.sync_state.start_sync(&self.blob).await
934 }
935
936 pub async fn recoverable_prefix_len(
952 &self,
953 proven: u64,
954 buffer_size: NonZeroUsize,
955 read_options: ReadOptions,
956 ) -> Result<u64, Error> {
957 let logical_page_size: u64 = self.cache_ref.page_size().widen();
958 let total_pages = self.current_page + u64::from(self.partial_page_state.is_some());
959 let physical_page_size = logical_page_size
960 .checked_add(CHECKSUM_SIZE)
961 .ok_or(Error::OffsetOverflow)?;
962 let physical_page_size_usize =
963 usize::try_from(physical_page_size).map_err(|_| Error::OffsetOverflow)?;
964 let max_batch_pages = u64::try_from((buffer_size.get() / physical_page_size_usize).max(1))
965 .map_err(|_| Error::OffsetOverflow)?;
966
967 let start_page = (proven / logical_page_size).min(self.current_page);
971 let mut valid_len = start_page
972 .checked_mul(logical_page_size)
973 .ok_or(Error::OffsetOverflow)?;
974 let mut page = start_page;
975 while page < total_pages {
976 let batch_pages = max_batch_pages.min(total_pages - page);
978 let batch_end = page.checked_add(batch_pages).ok_or(Error::OffsetOverflow)?;
979 let physical_offset = page
980 .checked_mul(physical_page_size)
981 .ok_or(Error::OffsetOverflow)?;
982 let physical_len = batch_pages
983 .checked_mul(physical_page_size)
984 .ok_or(Error::OffsetOverflow)?;
985 let physical_len = usize::try_from(physical_len).map_err(|_| Error::OffsetOverflow)?;
986
987 let physical = self
989 .blob
990 .read_at(physical_offset, physical_len, read_options)
991 .await?
992 .coalesce();
993
994 for physical_page in physical.as_ref().chunks_exact(physical_page_size_usize) {
996 let Some(checksum) = Checksum::validate_page(physical_page) else {
997 return Ok(valid_len);
998 };
999 let len = u64::from(checksum.len);
1000 valid_len = valid_len.checked_add(len).ok_or(Error::OffsetOverflow)?;
1001
1002 if len < logical_page_size {
1004 return Ok(valid_len);
1005 }
1006 }
1007 page = batch_end;
1008 }
1009 Ok(valid_len)
1010 }
1011
1012 async fn read_page(
1014 blob: &B,
1015 page: u64,
1016 page_size: u64,
1017 read_options: ReadOptions,
1018 ) -> Result<(IoBuf, u64, u64), Error> {
1019 let (logical, _) =
1020 super::get_page_with_checksum_from_blob(blob, page, page_size, read_options).await?;
1021 let start = page.checked_mul(page_size).ok_or(Error::OffsetOverflow)?;
1022 let len = u64::try_from(logical.len()).map_err(|_| Error::OffsetOverflow)?;
1023 let end = start.checked_add(len).ok_or(Error::OffsetOverflow)?;
1024 Ok((logical, start, end))
1025 }
1026
1027 pub async fn read_range(
1032 blob: &B,
1033 logical_page_size: NonZeroU16,
1034 offset: u64,
1035 len: usize,
1036 read_options: ReadOptions,
1037 ) -> Result<IoBufs, Error> {
1038 let len_u64 = u64::try_from(len).map_err(|_| Error::OffsetOverflow)?;
1040 let end = offset.checked_add(len_u64).ok_or(Error::OffsetOverflow)?;
1041 if len == 0 {
1042 return Ok(IoBufs::default());
1043 }
1044
1045 let logical_page_size: u64 = logical_page_size.widen();
1047 let first_page = offset / logical_page_size;
1048 let last_page = (end - 1) / logical_page_size;
1049 let mut out = IoBufs::default();
1050
1051 for page in first_page..=last_page {
1053 let (logical, page_start, page_end) =
1054 Self::read_page(blob, page, logical_page_size, read_options).await?;
1055 let overlap_start = offset.max(page_start);
1056 let overlap_end = end.min(page_end);
1057 if overlap_start >= overlap_end {
1058 return Err(Error::BlobInsufficientLength);
1059 }
1060 let start = (overlap_start - page_start) as usize;
1061 let end = (overlap_end - page_start) as usize;
1062 out.append(logical.slice(start..end));
1063 }
1064
1065 if out.len() != len {
1067 return Err(Error::BlobInsufficientLength);
1068 }
1069 Ok(out)
1070 }
1071
1072 pub async fn read_tail(
1078 blob: &B,
1079 physical_size: u64,
1080 logical_page_size: NonZeroU16,
1081 len: usize,
1082 read_options: ReadOptions,
1083 ) -> Result<(u64, IoBufs), Error> {
1084 if physical_size == 0 {
1085 return if len == 0 {
1086 Ok((0, IoBufs::default()))
1087 } else {
1088 Err(Error::BlobInsufficientLength)
1089 };
1090 }
1091
1092 let page_size: u64 = logical_page_size.widen();
1095 let physical_page_size = page_size
1096 .checked_add(CHECKSUM_SIZE)
1097 .ok_or(Error::OffsetOverflow)?;
1098 if !physical_size.is_multiple_of(physical_page_size) {
1099 return Err(Error::BlobInsufficientLength);
1100 }
1101
1102 let page = physical_size / physical_page_size - 1;
1104 let (tail, page_start, logical_size) =
1105 Self::read_page(blob, page, page_size, read_options).await?;
1106 let len_u64 = u64::try_from(len).map_err(|_| Error::OffsetOverflow)?;
1107 let offset = logical_size
1108 .checked_sub(len_u64)
1109 .ok_or(Error::BlobInsufficientLength)?;
1110
1111 let mut out = if offset < page_start {
1114 let prefix_len =
1115 usize::try_from(page_start - offset).map_err(|_| Error::OffsetOverflow)?;
1116 Self::read_range(blob, logical_page_size, offset, prefix_len, read_options).await?
1117 } else {
1118 IoBufs::default()
1119 };
1120 let tail_start = usize::try_from(offset.max(page_start) - page_start)
1121 .map_err(|_| Error::OffsetOverflow)?;
1122 out.append(tail.slice(tail_start..));
1123 assert_eq!(out.len(), len);
1124 Ok((logical_size, out))
1125 }
1126
1127 pub async fn wait_for_sync(&mut self) -> Result<(), Error> {
1129 self.sync_state.wait_for_pending().await
1130 }
1131
1132 pub async fn resize(&mut self, size: u64) -> Result<(), Error> {
1143 let current_size = self.buffer.size();
1144 if size == current_size {
1145 return Ok(());
1146 }
1147
1148 if size > current_size {
1150 let zeros_needed = (size - current_size) as usize;
1151 let mut zeros = self.cache_ref.pool().alloc(zeros_needed);
1152 zeros.put_bytes(0, zeros_needed);
1153 self.append_owned(zeros.freeze()).await?;
1154 return Ok(());
1155 }
1156
1157 self.shrink(size).await
1158 }
1159
1160 async fn shrink(&mut self, target_size: u64) -> Result<(), Error> {
1162 let page_size: u64 = self.cache_ref.page_size().widen();
1163 let physical_page_size = page_size
1164 .checked_add(CHECKSUM_SIZE)
1165 .ok_or(Error::OffsetOverflow)?;
1166
1167 self.sync().await?;
1169
1170 let full_pages = target_size / page_size;
1172 let partial_bytes = target_size % page_size;
1173 let physical_pages = full_pages
1174 .checked_add(u64::from(partial_bytes > 0))
1175 .ok_or(Error::OffsetOverflow)?;
1176 let new_physical_size = physical_pages
1177 .checked_mul(physical_page_size)
1178 .ok_or(Error::OffsetOverflow)?;
1179 let tail_offset = full_pages
1180 .checked_mul(page_size)
1181 .ok_or(Error::OffsetOverflow)?;
1182 let current_physical_size = if self.partial_page_state.is_some() {
1183 self.current_page
1184 .checked_add(1)
1185 .and_then(|pages| pages.checked_mul(physical_page_size))
1186 .ok_or(Error::OffsetOverflow)?
1187 } else {
1188 self.current_page
1189 .checked_mul(physical_page_size)
1190 .ok_or(Error::OffsetOverflow)?
1191 };
1192
1193 if new_physical_size != current_physical_size {
1196 self.sync_state
1197 .resize(&self.blob, new_physical_size)
1198 .await?;
1199 }
1200
1201 self.cache_ref.invalidate_from(self.id, full_pages);
1208
1209 if partial_bytes > 0 {
1210 return self
1211 .shrink_to_partial(full_pages, partial_bytes, page_size, tail_offset)
1212 .await;
1213 }
1214
1215 self.partial_page_state = None;
1217 self.durable_page_state = None;
1218 self.current_page = full_pages;
1219 self.buffer.offset = tail_offset;
1220 self.buffer.clear();
1221
1222 Ok(())
1223 }
1224
1225 async fn shrink_to_partial(
1227 &mut self,
1228 full_pages: u64,
1229 partial_bytes: u64,
1230 page_size: u64,
1231 tail_offset: u64,
1232 ) -> Result<(), Error> {
1233 self.current_page = full_pages;
1236 self.buffer.offset = tail_offset;
1237
1238 let (page_data, old_checksum) = super::get_page_with_checksum_from_blob(
1241 &self.blob,
1242 full_pages,
1243 page_size,
1244 ReadOptions::DONT_CACHE,
1245 )
1246 .await?;
1247
1248 if (page_data.len() as u64) < partial_bytes {
1250 return Err(Error::InvalidChecksum);
1251 }
1252
1253 self.buffer.clear();
1254 let new_data = &page_data.as_ref()[..partial_bytes as usize];
1255 let over_capacity = self.buffer.append(new_data);
1256 assert!(!over_capacity);
1257
1258 let final_record = self
1259 .sync_partial_page_shrink(
1260 full_pages,
1261 page_size,
1262 partial_bytes as u16,
1263 Crc32::checksum(new_data),
1264 &old_checksum,
1265 )
1266 .await?;
1267
1268 self.partial_page_state = Some(final_record);
1270 self.durable_page_state = Some(final_record);
1271
1272 Ok(())
1273 }
1274
1275 #[cfg(test)]
1277 pub(super) const fn cache_id(&self) -> u64 {
1278 self.id
1279 }
1280
1281 fn sealed_handle(&self, id: u64) -> super::Sealed<B> {
1283 let page_size: u64 = self.cache_ref.page_size().widen();
1284 let full_pages = self.current_page;
1285 assert_eq!(
1286 full_pages.checked_mul(page_size),
1287 Some(self.buffer.offset),
1288 "flushed page count is inconsistent with the buffer offset"
1289 );
1290 let partial_page = if self.buffer.is_empty() {
1291 None
1292 } else {
1293 Some(self.buffer.slice(..))
1294 };
1295 super::Sealed::new(
1296 self.blob.clone(),
1297 self.buffer.size(),
1298 partial_page,
1299 self.cache_ref.clone(),
1300 id,
1301 )
1302 }
1303
1304 pub async fn seal(mut self) -> Result<(super::Sealed<B>, Handle<()>), Error> {
1310 self.sync_state.wait_for_pending().await?;
1311 self.flush_internal(true, false).await?;
1312 let handle = self.sync_state.start_sync(&self.blob).await;
1313 Ok((self.sealed_handle(self.id), handle))
1314 }
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319 use super::*;
1320 use crate::{
1321 Buf, BufferPool, BufferPoolConfig, Handle, IoBufsMut, Runner as _, Spawner as _,
1322 Storage as _, Supervisor as _,
1323 buffer::{paged::CHECKSUM_SLOT_SIZE, tests::SyncTrackingBlob},
1324 deterministic,
1325 mocks::{DelayedSyncBlob, RecordingContext, next_pending_sync},
1326 telemetry::metrics::Registry,
1327 };
1328 use commonware_codec::ReadExt;
1329 use commonware_macros::test_traced;
1330 use commonware_utils::{NZU16, NZU32, NZUsize, channel::oneshot, sync::Mutex};
1331 use futures::FutureExt as _;
1332 use std::{
1333 num::NonZeroU16,
1334 sync::{
1335 Arc,
1336 atomic::{AtomicUsize, Ordering},
1337 },
1338 };
1339
1340 const PAGE_SIZE: NonZeroU16 = NZU16!(103); const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
1342
1343 #[test_traced("DEBUG")]
1344 fn test_writes_use_uncached_hint() {
1345 let executor = deterministic::Runner::default();
1346 executor.start(|context: deterministic::Context| async move {
1347 let blob = SyncTrackingBlob::new();
1348 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1349 let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
1350 .await
1351 .unwrap();
1352 writer.sync().await.unwrap();
1353
1354 writer.append(b"first").await.unwrap();
1355 writer.sync().await.unwrap();
1356 assert_eq!(blob.uncached_snapshot(), (0, 1));
1357
1358 writer.append(b"second").await.unwrap();
1359 let (_, sync) = writer.seal().await.unwrap();
1360 sync.await.unwrap();
1361 assert_eq!(blob.uncached_snapshot(), (1, 1));
1362 });
1363 }
1364
1365 #[test_traced("DEBUG")]
1370 fn test_unsynced_flushes_preserve_durable_checksum_slot() {
1371 let executor = deterministic::Runner::default();
1372 executor.start(|context: deterministic::Context| async move {
1373 let page_size = PAGE_SIZE.get() as usize;
1374 let physical_page = page_size + CHECKSUM_SIZE as usize;
1375 let (blob, blob_size) = context
1376 .open("test_partition", b"snapshot_torn")
1377 .await
1378 .unwrap();
1379 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1380 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref.clone())
1381 .await
1382 .unwrap();
1383
1384 let synced: Vec<u8> = (1u8..=24).collect();
1386 writer.append(&synced).await.unwrap();
1387 writer.sync().await.unwrap();
1388 let durable_image = blob
1389 .read_at(0, physical_page, ReadOptions::default())
1390 .await
1391 .unwrap()
1392 .coalesce();
1393
1394 writer.append(&[25u8; 8]).await.unwrap();
1396 drop(writer.snapshot().await.unwrap());
1397 writer.append(&[26u8; 8]).await.unwrap();
1398 drop(writer.snapshot().await.unwrap());
1399 let torn_image = blob
1400 .read_at(0, physical_page, ReadOptions::default())
1401 .await
1402 .unwrap()
1403 .coalesce();
1404
1405 let mut crash = durable_image.as_ref().to_vec();
1408 crash[page_size..].copy_from_slice(&torn_image.as_ref()[page_size..]);
1409 let (crashed, _) = context
1410 .open("test_partition", b"snapshot_crash")
1411 .await
1412 .unwrap();
1413 crashed
1414 .write_at(0, crash, WriteOptions::default())
1415 .await
1416 .unwrap();
1417 crashed.sync().await.unwrap();
1418
1419 let recovered = Writer::new(crashed, physical_page as u64, BUFFER_SIZE, cache_ref)
1421 .await
1422 .unwrap();
1423 assert_eq!(recovered.size(), synced.len() as u64);
1424 let read = recovered.read_at(0, synced.len()).await.unwrap().coalesce();
1425 assert_eq!(read.as_ref(), synced.as_slice());
1426 });
1427 }
1428
1429 #[test_traced("DEBUG")]
1431 fn test_recoverable_prefix_len_clean() {
1432 let executor = deterministic::Runner::default();
1433 executor.start(|context: deterministic::Context| async move {
1434 let (context, recordings) = RecordingContext::new(context);
1435 let (blob, blob_size) = context
1436 .open("test_partition", b"prefix_clean")
1437 .await
1438 .unwrap();
1439 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1440 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1441 .await
1442 .unwrap();
1443 assert_eq!(
1444 writer
1445 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1446 .await
1447 .unwrap(),
1448 0
1449 );
1450
1451 let total = PAGE_SIZE.get() as usize * 2 + 50;
1452 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1453 writer.append(&data).await.unwrap();
1454 writer.sync().await.unwrap();
1455
1456 recordings.clear();
1458 assert_eq!(
1459 writer
1460 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1461 .await
1462 .unwrap(),
1463 total as u64
1464 );
1465 let reads = recordings.snapshot().reads;
1466 assert!(!reads.is_empty());
1467 assert!(
1468 reads
1469 .iter()
1470 .all(|options| *options == ReadOptions::default())
1471 );
1472
1473 drop(writer);
1474 let (blob, blob_size) = context
1475 .open("test_partition", b"prefix_clean")
1476 .await
1477 .unwrap();
1478
1479 recordings.clear();
1481 let _recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1482 .await
1483 .unwrap();
1484 let reads = recordings.snapshot().reads;
1485 assert!(!reads.is_empty());
1486 assert!(
1487 reads
1488 .iter()
1489 .all(|options| *options == ReadOptions::default())
1490 );
1491 });
1492 }
1493
1494 #[test_traced("DEBUG")]
1497 fn test_recoverable_prefix_len_torn_interior_page() {
1498 let executor = deterministic::Runner::default();
1499 executor.start(|context: deterministic::Context| async move {
1500 let (blob, blob_size) = context
1501 .open("test_partition", b"prefix_torn")
1502 .await
1503 .unwrap();
1504 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1505 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1506 .await
1507 .unwrap();
1508 let total = PAGE_SIZE.get() as usize * 3 + 10;
1509 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1510 writer.append(&data).await.unwrap();
1511 writer.sync().await.unwrap();
1512
1513 let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
1515 let offset = physical_page_size + 7;
1516 let byte = blob
1517 .read_at(offset, 1, ReadOptions::default())
1518 .await
1519 .unwrap()
1520 .coalesce();
1521 blob.write_at(
1522 offset,
1523 vec![byte.as_ref()[0] ^ 0xFF],
1524 WriteOptions::default(),
1525 )
1526 .await
1527 .unwrap();
1528 blob.sync().await.unwrap();
1529
1530 assert_eq!(
1531 writer
1532 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1533 .await
1534 .unwrap(),
1535 PAGE_SIZE.get() as u64
1536 );
1537 });
1538 }
1539
1540 #[test_traced("DEBUG")]
1543 fn test_recoverable_prefix_len_proven_prefix() {
1544 let executor = deterministic::Runner::default();
1545 executor.start(|context: deterministic::Context| async move {
1546 let (blob, blob_size) = context
1547 .open("test_partition", b"prefix_proven")
1548 .await
1549 .unwrap();
1550 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1551 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1552 .await
1553 .unwrap();
1554 let total = PAGE_SIZE.get() as usize * 4;
1555 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1556 writer.append(&data).await.unwrap();
1557 writer.sync().await.unwrap();
1558
1559 let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
1561 let offset = physical_page_size + 7;
1562 let byte = blob
1563 .read_at(offset, 1, ReadOptions::default())
1564 .await
1565 .unwrap()
1566 .coalesce();
1567 blob.write_at(
1568 offset,
1569 vec![byte.as_ref()[0] ^ 0xFF],
1570 WriteOptions::default(),
1571 )
1572 .await
1573 .unwrap();
1574 blob.sync().await.unwrap();
1575
1576 let page = u64::from(PAGE_SIZE.get());
1579 for (proven, expected) in [
1580 (0, page),
1581 (page, page),
1582 (2 * page, 4 * page),
1583 (2 * page + 3, 4 * page),
1584 (total as u64, 4 * page),
1585 ] {
1586 assert_eq!(
1587 writer
1588 .recoverable_prefix_len(
1589 proven,
1590 NZUsize!(BUFFER_SIZE),
1591 ReadOptions::default()
1592 )
1593 .await
1594 .unwrap(),
1595 expected,
1596 "proven {proven}"
1597 );
1598 }
1599
1600 assert_eq!(
1603 writer
1604 .recoverable_prefix_len(
1605 100 * page,
1606 NZUsize!(BUFFER_SIZE),
1607 ReadOptions::default()
1608 )
1609 .await
1610 .unwrap(),
1611 4 * page
1612 );
1613
1614 writer.append(&data[..20]).await.unwrap();
1617 writer.sync().await.unwrap();
1618 for proven in [4 * page + 20, 5 * page, 100 * page] {
1619 assert_eq!(
1620 writer
1621 .recoverable_prefix_len(
1622 proven,
1623 NZUsize!(BUFFER_SIZE),
1624 ReadOptions::default()
1625 )
1626 .await
1627 .unwrap(),
1628 4 * page + 20,
1629 "proven {proven}"
1630 );
1631 }
1632 });
1633 }
1634
1635 #[test_traced("DEBUG")]
1638 fn test_recoverable_prefix_len_stale_short_interior_page() {
1639 let executor = deterministic::Runner::default();
1640 executor.start(|context: deterministic::Context| async move {
1641 let (blob, blob_size) = context
1642 .open("test_partition", b"prefix_stale")
1643 .await
1644 .unwrap();
1645 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1646 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1647 .await
1648 .unwrap();
1649
1650 let total = PAGE_SIZE.get() as usize * 2;
1652 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1653 writer.append(&data[..20]).await.unwrap();
1654 writer.sync().await.unwrap();
1655 let physical_page_size = (PAGE_SIZE.get() as u64 + CHECKSUM_SIZE) as usize;
1656 let stale = blob
1657 .read_at(0, physical_page_size, ReadOptions::default())
1658 .await
1659 .unwrap()
1660 .coalesce();
1661 let stale = stale.as_ref().to_vec();
1662
1663 writer.append(&data[20..]).await.unwrap();
1666 writer.sync().await.unwrap();
1667 blob.write_at(0, stale, WriteOptions::default())
1668 .await
1669 .unwrap();
1670 blob.sync().await.unwrap();
1671
1672 assert_eq!(
1673 writer
1674 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
1675 .await
1676 .unwrap(),
1677 20
1678 );
1679 });
1680 }
1681
1682 #[test_traced("DEBUG")]
1685 fn test_recoverable_prefix_len_batches_reads() {
1686 let executor = deterministic::Runner::default();
1687 executor.start(|context: deterministic::Context| async move {
1688 let (context, recordings) = RecordingContext::new(context);
1689 let (blob, blob_size) = context
1690 .open("test_partition", b"prefix_batches")
1691 .await
1692 .unwrap();
1693 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1694 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
1695 .await
1696 .unwrap();
1697
1698 let total = PAGE_SIZE.get() as usize * 8 + 10;
1700 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1701 writer.append(&data).await.unwrap();
1702 writer.sync().await.unwrap();
1703
1704 recordings.clear();
1706 assert_eq!(
1707 writer
1708 .recoverable_prefix_len(0, NZUsize!(1), ReadOptions::default())
1709 .await
1710 .unwrap(),
1711 total as u64
1712 );
1713 assert_eq!(recordings.snapshot().reads.len(), 9);
1714
1715 recordings.clear();
1717 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
1718 let scan_buffer = NonZeroUsize::new(physical_page_size * 3).unwrap();
1719 assert_eq!(
1720 writer
1721 .recoverable_prefix_len(0, scan_buffer, ReadOptions::default())
1722 .await
1723 .unwrap(),
1724 total as u64
1725 );
1726 assert_eq!(recordings.snapshot().reads.len(), 3);
1727 });
1728 }
1729
1730 #[test_traced("DEBUG")]
1731 fn test_read_many_into_empty() {
1732 let executor = deterministic::Runner::default();
1733 executor.start(|context: deterministic::Context| async move {
1734 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1735 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1736 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1737 .await
1738 .unwrap();
1739
1740 append.append(&[0u8; 8]).await.unwrap();
1741 assert_eq!(append.size(), 8);
1742
1743 let mut buf = [];
1745 append
1746 .read_many_into(&mut buf, &[], NZUsize!(4))
1747 .await
1748 .unwrap();
1749 });
1750 }
1751
1752 #[test_traced("DEBUG")]
1753 fn test_read_many_into_all_in_tip() {
1754 let executor = deterministic::Runner::default();
1756 executor.start(|context: deterministic::Context| async move {
1757 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1758 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1759 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1760 .await
1761 .unwrap();
1762
1763 let data: Vec<u8> = (0..20).collect();
1764 append.append(&data).await.unwrap();
1765 assert_eq!(append.size(), 20);
1766
1767 let offsets = [0u64, 4, 8, 12, 16];
1769 let mut buf = vec![0u8; 5 * 4];
1770 append
1771 .read_many_into(&mut buf, &offsets, NZUsize!(4))
1772 .await
1773 .unwrap();
1774
1775 for (i, &off) in offsets.iter().enumerate() {
1776 assert_eq!(
1777 &buf[i * 4..(i + 1) * 4],
1778 &data[off as usize..off as usize + 4],
1779 );
1780 }
1781 });
1782 }
1783
1784 #[test_traced("DEBUG")]
1785 fn test_try_read_sync_all_in_tip() {
1786 let executor = deterministic::Runner::default();
1787 executor.start(|context: deterministic::Context| async move {
1788 let (blob, blob_size) = context
1789 .open("test_partition", b"try_read_sync_tip")
1790 .await
1791 .unwrap();
1792 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1793 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1794 .await
1795 .unwrap();
1796
1797 let data: Vec<u8> = (0..20).collect();
1798 append.append(&data).await.unwrap();
1799
1800 let mut buf = vec![0u8; data.len()];
1801 assert!(append.try_read_sync_into(&mut buf, 0));
1802 assert_eq!(buf, data);
1803 });
1804 }
1805
1806 #[test_traced("DEBUG")]
1807 fn test_try_read_sync_cache_miss() {
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"try_read_sync_cache_miss")
1812 .await
1813 .unwrap();
1814 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
1816 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1817 .await
1818 .unwrap();
1819
1820 let page_size = PAGE_SIZE.get() as usize;
1821 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
1822 append.append(&data).await.unwrap();
1823 append.sync().await.unwrap();
1824
1825 let _ = append.read_at(0, page_size).await.unwrap();
1826
1827 let mut buf = vec![0xAA; 4];
1829 assert!(!append.try_read_sync_into(&mut buf, (page_size - 2) as u64));
1830 });
1831 }
1832
1833 #[test_traced("DEBUG")]
1834 fn test_read_many_into_all_from_cache() {
1835 let executor = deterministic::Runner::default();
1837 executor.start(|context: deterministic::Context| async move {
1838 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1839 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1840 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1841 .await
1842 .unwrap();
1843
1844 let data: Vec<u8> = (0..20).collect();
1845 append.append(&data).await.unwrap();
1846 append.sync().await.unwrap();
1847 assert_eq!(append.size(), 20);
1848
1849 let offsets = [0u64, 8, 16];
1850 let mut buf = vec![0u8; 3 * 4];
1851 append
1852 .read_many_into(&mut buf, &offsets, NZUsize!(4))
1853 .await
1854 .unwrap();
1855
1856 for (i, &off) in offsets.iter().enumerate() {
1857 assert_eq!(
1858 &buf[i * 4..(i + 1) * 4],
1859 &data[off as usize..off as usize + 4],
1860 );
1861 }
1862 });
1863 }
1864
1865 #[test_traced("DEBUG")]
1866 fn test_read_many_into_mixed_tip_and_cache() {
1867 let executor = deterministic::Runner::default();
1869 executor.start(|context: deterministic::Context| async move {
1870 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1871 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1872 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1873 .await
1874 .unwrap();
1875
1876 let first: Vec<u8> = (0..16).collect();
1877 append.append(&first).await.unwrap();
1878 append.sync().await.unwrap();
1879
1880 let second: Vec<u8> = (16..32).collect();
1881 append.append(&second).await.unwrap();
1882 assert_eq!(append.size(), 32);
1883
1884 let offsets = [0u64, 4, 16, 24];
1886 let mut buf = vec![0u8; 4 * 4];
1887 append
1888 .read_many_into(&mut buf, &offsets, NZUsize!(4))
1889 .await
1890 .unwrap();
1891
1892 let all: Vec<u8> = (0..32).collect();
1893 for (i, &off) in offsets.iter().enumerate() {
1894 assert_eq!(
1895 &buf[i * 4..(i + 1) * 4],
1896 &all[off as usize..off as usize + 4],
1897 );
1898 }
1899 });
1900 }
1901
1902 #[test_traced("DEBUG")]
1903 fn test_read_many_into_out_of_bounds() {
1904 let executor = deterministic::Runner::default();
1905 executor.start(|context: deterministic::Context| async move {
1906 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1907 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1908 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1909 .await
1910 .unwrap();
1911
1912 append.append(&[0u8; 8]).await.unwrap();
1913 assert_eq!(append.size(), 8);
1914
1915 let mut buf = vec![0u8; 4];
1917 let err = append
1918 .read_many_into(&mut buf, &[8], NZUsize!(4))
1919 .await
1920 .unwrap_err();
1921 assert!(matches!(err, Error::BlobInsufficientLength));
1922 });
1923 }
1924
1925 #[test_traced("DEBUG")]
1926 fn test_read_many_into_single_item() {
1927 let executor = deterministic::Runner::default();
1928 executor.start(|context: deterministic::Context| async move {
1929 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1930 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1931 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1932 .await
1933 .unwrap();
1934
1935 let data = vec![0xAA; 8];
1936 append.append(&data).await.unwrap();
1937 assert_eq!(append.size(), 8);
1938
1939 let mut buf = vec![0u8; 8];
1940 append
1941 .read_many_into(&mut buf, &[0], NZUsize!(8))
1942 .await
1943 .unwrap();
1944 assert_eq!(&buf, &data);
1945 });
1946 }
1947
1948 #[test_traced("DEBUG")]
1949 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
1950 fn test_read_many_into_rejects_invalid_buffer_len() {
1951 let executor = deterministic::Runner::default();
1952 executor.start(|context: deterministic::Context| async move {
1953 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1954 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1955 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1956 .await
1957 .unwrap();
1958
1959 let data: Vec<u8> = (0..16).collect();
1960 append.append(&data).await.unwrap();
1961
1962 let offsets = [0u64, 4];
1963 let mut buf = vec![0u8; 7];
1964 let _ = append.read_many_into(&mut buf, &offsets, NZUsize!(4)).await;
1965 });
1966 }
1967
1968 #[test_traced("DEBUG")]
1969 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
1970 fn test_read_many_into_rejects_unsorted_offsets() {
1971 let executor = deterministic::Runner::default();
1972 executor.start(|context: deterministic::Context| async move {
1973 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1974 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1975 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1976 .await
1977 .unwrap();
1978
1979 let data: Vec<u8> = (0..16).collect();
1980 append.append(&data).await.unwrap();
1981
1982 let mut buf = vec![0u8; 8];
1983 let _ = append.read_many_into(&mut buf, &[8, 4], NZUsize!(4)).await;
1984 });
1985 }
1986
1987 #[test_traced("DEBUG")]
1988 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
1989 fn test_read_many_into_rejects_overlapping_offsets() {
1990 let executor = deterministic::Runner::default();
1991 executor.start(|context: deterministic::Context| async move {
1992 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1993 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1994 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1995 .await
1996 .unwrap();
1997
1998 let data: Vec<u8> = (0..16).collect();
1999 append.append(&data).await.unwrap();
2000
2001 let mut buf = vec![0u8; 8];
2002 let _ = append.read_many_into(&mut buf, &[2, 4], NZUsize!(4)).await;
2003 });
2004 }
2005
2006 #[test_traced("DEBUG")]
2007 fn test_read_many_into_rejects_offset_overflow() {
2008 let executor = deterministic::Runner::default();
2009 executor.start(|context: deterministic::Context| async move {
2010 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2011 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2012 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2013 .await
2014 .unwrap();
2015
2016 let data: Vec<u8> = (0..16).collect();
2017 append.append(&data).await.unwrap();
2018
2019 let mut buf = vec![0u8; 8];
2020 let err = append
2021 .read_many_into(&mut buf, &[u64::MAX - 1, 4], NZUsize!(4))
2022 .await
2023 .unwrap_err();
2024 assert!(matches!(err, Error::OffsetOverflow));
2025 });
2026 }
2027
2028 #[test_traced("DEBUG")]
2029 fn test_read_many_into_matches_read_at() {
2030 let executor = deterministic::Runner::default();
2032 executor.start(|context: deterministic::Context| async move {
2033 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
2034 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2035 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2036 .await
2037 .unwrap();
2038
2039 let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
2041 append.append(&data).await.unwrap();
2042 append.sync().await.unwrap();
2043 let more: Vec<u8> = (0u8..50).collect();
2045 append.append(&more).await.unwrap();
2046 assert_eq!(append.size(), 350);
2047
2048 let item_size = 10;
2049 let offsets: Vec<u64> = (0..35).map(|i| i * item_size as u64).collect();
2050 let mut batch_buf = vec![0u8; offsets.len() * item_size];
2051 append
2052 .read_many_into(&mut batch_buf, &offsets, NZUsize!(item_size))
2053 .await
2054 .unwrap();
2055
2056 for (i, &off) in offsets.iter().enumerate() {
2058 let single = append.read_at(off, item_size).await.unwrap().coalesce();
2059 assert_eq!(
2060 &batch_buf[i * item_size..(i + 1) * item_size],
2061 single.as_ref(),
2062 "mismatch at offset {off}",
2063 );
2064 }
2065 });
2066 }
2067
2068 #[test_traced("DEBUG")]
2069 fn test_read_many_into_scattered_cache_misses() {
2070 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::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
2078 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2079 .await
2080 .unwrap();
2081
2082 let synced: Vec<u8> = (0u8..=255)
2084 .cycle()
2085 .take(PAGE_SIZE.get() as usize * 3)
2086 .collect();
2087 append.append(&synced).await.unwrap();
2088 append.sync().await.unwrap();
2089
2090 let item_size = 10;
2093 let tip_len = PAGE_SIZE.get() as usize / 2;
2094 let tip: Vec<u8> = (100u8..=255).cycle().take(tip_len).collect();
2095 append.append(&tip).await.unwrap();
2096
2097 let _ = append.read_at(0, item_size).await.unwrap();
2099 let _ = append
2100 .read_at(PAGE_SIZE.get() as u64 * 2, item_size)
2101 .await
2102 .unwrap();
2103
2104 let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
2107 let tip_off = synced.len() as u64 + item_size as u64;
2108 let offsets = [
2109 0u64, PAGE_SIZE.get() as u64, PAGE_SIZE.get() as u64 * 2, straddle_off, tip_off, ];
2115 let mut buf = vec![0u8; offsets.len() * item_size];
2116 append
2117 .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
2118 .await
2119 .unwrap();
2120
2121 let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
2122 for (i, &off) in offsets.iter().enumerate() {
2123 assert_eq!(
2124 &buf[i * item_size..(i + 1) * item_size],
2125 &read[off as usize..off as usize + item_size],
2126 );
2127 }
2128 });
2129 }
2130
2131 #[test_traced("DEBUG")]
2132 fn test_read_many_into_straddle_prefix_miss() {
2133 let executor = deterministic::Runner::default();
2137 executor.start(|context: deterministic::Context| async move {
2138 let (blob, blob_size) = context
2139 .open("test_partition", b"rmany_smiss")
2140 .await
2141 .unwrap();
2142 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
2144 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2145 .await
2146 .unwrap();
2147
2148 let synced: Vec<u8> = (0u8..=255)
2150 .cycle()
2151 .take(PAGE_SIZE.get() as usize * 3)
2152 .collect();
2153 append.append(&synced).await.unwrap();
2154 append.sync().await.unwrap();
2155 let item_size = 10;
2156 let tip: Vec<u8> = (100u8..=255)
2157 .cycle()
2158 .take(PAGE_SIZE.get() as usize / 2)
2159 .collect();
2160 append.append(&tip).await.unwrap();
2161
2162 let _ = append.read_at(0, item_size).await.unwrap();
2165
2166 let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
2167 let tip_off = synced.len() as u64 + item_size as u64;
2168 let offsets = [straddle_off, tip_off];
2169 let mut buf = vec![0u8; offsets.len() * item_size];
2170 let hits = append
2171 .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
2172 .await
2173 .unwrap();
2174
2175 assert_eq!(hits, 1);
2177 let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
2178 for (i, &off) in offsets.iter().enumerate() {
2179 assert_eq!(
2180 &buf[i * item_size..(i + 1) * item_size],
2181 &read[off as usize..off as usize + item_size],
2182 );
2183 }
2184 });
2185 }
2186
2187 #[test_traced("DEBUG")]
2188 fn test_append_crc_empty() {
2189 let executor = deterministic::Runner::default();
2190 executor.start(|context: deterministic::Context| async move {
2191 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2193 assert_eq!(blob_size, 0);
2194
2195 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2197
2198 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2200 .await
2201 .unwrap();
2202
2203 assert_eq!(append.size(), 0);
2205
2206 append.sync().await.unwrap();
2208 drop(append);
2209
2210 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2211 assert_eq!(blob_size, 0); let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2214 .await
2215 .unwrap();
2216
2217 assert_eq!(append.size(), 0);
2218 });
2219 }
2220
2221 #[test_traced("DEBUG")]
2222 fn test_append_crc_basic() {
2223 let executor = deterministic::Runner::default();
2224 executor.start(|context: deterministic::Context| async move {
2225 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2227 assert_eq!(blob_size, 0);
2228
2229 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2231
2232 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2234 .await
2235 .unwrap();
2236
2237 assert_eq!(append.size(), 0);
2239
2240 let data = vec![1, 2, 3, 4, 5];
2242 append.append(&data).await.unwrap();
2243
2244 assert_eq!(append.size(), 5);
2246
2247 let more_data = vec![6, 7, 8, 9, 10];
2249 append.append(&more_data).await.unwrap();
2250
2251 assert_eq!(append.size(), 10);
2253
2254 let read_buf = append.read_at(0, 5).await.unwrap().coalesce();
2256 assert_eq!(read_buf, &data[..]);
2257
2258 let read_buf = append.read_at(5, 5).await.unwrap().coalesce();
2260 assert_eq!(read_buf, &more_data[..]);
2261
2262 let read_buf = append.read_at(0, 10).await.unwrap().coalesce();
2264 assert_eq!(read_buf, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
2265
2266 append.sync().await.unwrap();
2269 drop(append);
2270
2271 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2272 assert_eq!(blob_size, 115);
2274 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2275 .await
2276 .unwrap();
2277 assert_eq!(append.size(), 10); let spanning_data: Vec<u8> = (11..=110).collect();
2283 append.append(&spanning_data).await.unwrap();
2284 assert_eq!(append.size(), 110);
2285
2286 let read_buf = append.read_at(10, 100).await.unwrap().coalesce();
2288 assert_eq!(read_buf, &spanning_data[..]);
2289
2290 let read_buf = append.read_at(0, 110).await.unwrap().coalesce();
2292 let expected: Vec<u8> = (1..=110).collect();
2293 assert_eq!(read_buf, &expected[..]);
2294
2295 append.sync().await.unwrap();
2297 drop(append);
2298
2299 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2300 assert_eq!(blob_size, 230);
2302 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2303 .await
2304 .unwrap();
2305 assert_eq!(append.size(), 110);
2306
2307 let boundary_data: Vec<u8> = (111..=206).collect();
2311 assert_eq!(boundary_data.len(), 96);
2312 append.append(&boundary_data).await.unwrap();
2313 assert_eq!(append.size(), 206);
2314
2315 let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
2317 let expected: Vec<u8> = (1..=206).collect();
2318 assert_eq!(read_buf, &expected[..]);
2319
2320 append.sync().await.unwrap();
2322 drop(append);
2323
2324 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
2325 assert_eq!(blob_size, 230);
2327 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2328 .await
2329 .unwrap();
2330 assert_eq!(append.size(), 206);
2331
2332 let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
2334 assert_eq!(read_buf, &expected[..]);
2335 });
2336 }
2337
2338 #[test_traced("DEBUG")]
2339 fn test_append_owned_bypass_from_empty_tip() {
2340 let executor = deterministic::Runner::default();
2343 executor.start(|context: deterministic::Context| async move {
2344 let (blob, blob_size) = context
2345 .open("test_partition", b"owned_empty")
2346 .await
2347 .unwrap();
2348 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2349 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2350 .await
2351 .unwrap();
2352
2353 let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2355 let src = IoBuf::from(data.clone());
2356 let src_start = src.as_ptr() as usize;
2357 let src_range = src_start..src_start + src.len();
2358 append.append_owned(src.clone()).await.unwrap();
2359 assert_eq!(append.size(), 500);
2360
2361 let tip_ptr = append.buffer.as_ref().as_ptr() as usize;
2363 assert!(!src_range.contains(&tip_ptr));
2364
2365 let mut probe = vec![0u8; PAGE_SIZE.get() as usize];
2368 assert_eq!(
2369 append.cache_ref.read_cached(append.id, &mut probe, 0),
2370 PAGE_SIZE.get() as usize
2371 );
2372 assert_eq!(probe, &data[..PAGE_SIZE.get() as usize]);
2373
2374 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2376 assert_eq!(read_buf, &data[..]);
2377
2378 append.sync().await.unwrap();
2380 drop(append);
2381
2382 let (blob, blob_size) = context
2383 .open("test_partition", b"owned_empty")
2384 .await
2385 .unwrap();
2386 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2387 .await
2388 .unwrap();
2389 assert_eq!(append.size(), 500);
2390 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2391 assert_eq!(read_buf, &data[..]);
2392 });
2393 }
2394
2395 #[test_traced("DEBUG")]
2396 fn test_append_owned_bypass_with_synced_partial_page() {
2397 let executor = deterministic::Runner::default();
2400 executor.start(|context: deterministic::Context| async move {
2401 let (blob, blob_size) = context
2402 .open("test_partition", b"owned_partial")
2403 .await
2404 .unwrap();
2405 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2406 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2407 .await
2408 .unwrap();
2409
2410 let all: Vec<u8> = (0..500).map(|i| (i % 247) as u8).collect();
2412 append.append(&all[..50]).await.unwrap();
2413 append.sync().await.unwrap();
2414
2415 append
2418 .append_owned(IoBuf::from(all[50..].to_vec()))
2419 .await
2420 .unwrap();
2421 assert_eq!(append.size(), 500);
2422 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2423 assert_eq!(read_buf, &all[..]);
2424
2425 drop(append);
2428 let (blob, blob_size) = context
2429 .open("test_partition", b"owned_partial")
2430 .await
2431 .unwrap();
2432 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2433 .await
2434 .unwrap();
2435 assert_eq!(append.size(), 50);
2436 let read_buf = append.read_at(0, 50).await.unwrap().coalesce();
2437 assert_eq!(read_buf, &all[..50]);
2438
2439 append
2442 .append_owned(IoBuf::from(all[50..].to_vec()))
2443 .await
2444 .unwrap();
2445 append.sync().await.unwrap();
2446 drop(append);
2447
2448 let (blob, blob_size) = context
2449 .open("test_partition", b"owned_partial")
2450 .await
2451 .unwrap();
2452 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2453 .await
2454 .unwrap();
2455 assert_eq!(append.size(), 500);
2456 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
2457 assert_eq!(read_buf, &all[..]);
2458 });
2459 }
2460
2461 #[test_traced("DEBUG")]
2462 fn test_append_owned_bypass_with_buffered_tip() {
2463 let executor = deterministic::Runner::default();
2466 executor.start(|context: deterministic::Context| async move {
2467 let (blob, blob_size) = context
2468 .open("test_partition", b"owned_buffered")
2469 .await
2470 .unwrap();
2471 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2472 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2473 .await
2474 .unwrap();
2475
2476 let all: Vec<u8> = (0..430).map(|i| (i % 239) as u8).collect();
2477 append.append(&all[..30]).await.unwrap();
2478 append
2479 .append_owned(IoBuf::from(all[30..].to_vec()))
2480 .await
2481 .unwrap();
2482 assert_eq!(append.size(), 430);
2483 let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
2484 assert_eq!(read_buf, &all[..]);
2485
2486 append.sync().await.unwrap();
2487 drop(append);
2488
2489 let (blob, blob_size) = context
2490 .open("test_partition", b"owned_buffered")
2491 .await
2492 .unwrap();
2493 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2494 .await
2495 .unwrap();
2496 assert_eq!(append.size(), 430);
2497 let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
2498 assert_eq!(read_buf, &all[..]);
2499 });
2500 }
2501
2502 #[test_traced("DEBUG")]
2503 fn test_append_owned_exact_page_multiple_and_small() {
2504 let executor = deterministic::Runner::default();
2507 executor.start(|context: deterministic::Context| async move {
2508 let (blob, blob_size) = context
2509 .open("test_partition", b"owned_exact")
2510 .await
2511 .unwrap();
2512 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2513 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2514 .await
2515 .unwrap();
2516
2517 let bulk: Vec<u8> = (0..412).map(|i| (i % 233) as u8).collect();
2519 append
2520 .append_owned(IoBuf::from(bulk.clone()))
2521 .await
2522 .unwrap();
2523 assert_eq!(append.size(), 412);
2524
2525 let small: Vec<u8> = (0..10).map(|i| (i % 229) as u8).collect();
2527 append
2528 .append_owned(IoBuf::from(small.clone()))
2529 .await
2530 .unwrap();
2531 assert_eq!(append.size(), 422);
2532
2533 let read_buf = append.read_at(0, 422).await.unwrap().coalesce();
2534 assert_eq!(&read_buf.as_ref()[..412], &bulk[..]);
2535 assert_eq!(&read_buf.as_ref()[412..], &small[..]);
2536
2537 append.sync().await.unwrap();
2538 drop(append);
2539
2540 let (blob, blob_size) = context
2541 .open("test_partition", b"owned_exact")
2542 .await
2543 .unwrap();
2544 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2545 .await
2546 .unwrap();
2547 assert_eq!(append.size(), 422);
2548 });
2549 }
2550
2551 #[test_traced("DEBUG")]
2552 fn test_append_owned_physical_bytes_match_buffered() {
2553 let executor = deterministic::Runner::default();
2556 executor.start(|context: deterministic::Context| async move {
2557 let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2558 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2559
2560 let (blob, size) = context
2561 .open("test_partition", b"phys_direct")
2562 .await
2563 .unwrap();
2564 let mut direct = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
2565 .await
2566 .unwrap();
2567 direct
2568 .append_owned(IoBuf::from(data.clone()))
2569 .await
2570 .unwrap();
2571 direct.sync().await.unwrap();
2572 drop(direct);
2573
2574 let (blob, size) = context
2576 .open("test_partition", b"phys_buffered")
2577 .await
2578 .unwrap();
2579 let mut buffered = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2580 .await
2581 .unwrap();
2582 for chunk in data.chunks(10) {
2583 buffered.append(chunk).await.unwrap();
2584 }
2585 buffered.sync().await.unwrap();
2586 drop(buffered);
2587
2588 let (blob_a, size_a) = context
2589 .open("test_partition", b"phys_direct")
2590 .await
2591 .unwrap();
2592 let (blob_b, size_b) = context
2593 .open("test_partition", b"phys_buffered")
2594 .await
2595 .unwrap();
2596 assert_eq!(size_a, size_b);
2597 let bytes_a = blob_a
2598 .read_at(0, size_a as usize, ReadOptions::default())
2599 .await
2600 .unwrap()
2601 .coalesce();
2602 let bytes_b = blob_b
2603 .read_at(0, size_b as usize, ReadOptions::default())
2604 .await
2605 .unwrap()
2606 .coalesce();
2607 assert_eq!(bytes_a.as_ref(), bytes_b.as_ref());
2608 });
2609 }
2610
2611 #[test_traced("DEBUG")]
2612 fn test_append_borrowed_large_takes_direct_path() {
2613 let executor = deterministic::Runner::default();
2616 executor.start(|context: deterministic::Context| async move {
2617 let (blob, blob_size) = context
2618 .open("test_partition", b"borrowed_large")
2619 .await
2620 .unwrap();
2621 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2622 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2623 .await
2624 .unwrap();
2625
2626 let all: Vec<u8> = (0..530).map(|i| (i % 241) as u8).collect();
2628 append.append(&all[..30]).await.unwrap();
2629
2630 append.append(&all[30..]).await.unwrap();
2632 assert_eq!(append.size(), 530);
2633
2634 assert_eq!(append.buffer.len(), 15);
2636
2637 let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2638 assert_eq!(read_buf, &all[..]);
2639
2640 append.sync().await.unwrap();
2641 drop(append);
2642
2643 let (blob, blob_size) = context
2644 .open("test_partition", b"borrowed_large")
2645 .await
2646 .unwrap();
2647 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2648 .await
2649 .unwrap();
2650 assert_eq!(append.size(), 530);
2651 let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2652 assert_eq!(read_buf, &all[..]);
2653 });
2654 }
2655
2656 #[test_traced("DEBUG")]
2657 fn test_sync_releases_tip_pool_slot_after_full_drain() {
2658 let executor = deterministic::Runner::default();
2659 executor.start(|context: deterministic::Context| async move {
2660 let mut registry = Registry::default();
2661 let pool = BufferPool::new(
2662 BufferPoolConfig::for_storage()
2663 .with_pool_min_size(PAGE_SIZE.get() as usize)
2664 .with_max_per_class(NZU32!(2)),
2665 &mut registry,
2666 );
2667 let cache_ref = CacheRef::new(pool.clone(), PAGE_SIZE, NZUsize!(1));
2668
2669 let (blob, blob_size) = context
2670 .open("test_partition", b"release_tip_backing")
2671 .await
2672 .unwrap();
2673 assert_eq!(blob_size, 0);
2674
2675 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2676 .await
2677 .unwrap();
2678
2679 append
2680 .append(&vec![7; PAGE_SIZE.get() as usize])
2681 .await
2682 .unwrap();
2683
2684 assert!(
2686 matches!(
2687 pool.try_alloc(BUFFER_SIZE),
2688 Err(crate::iobuf::PoolError::Exhausted)
2689 ),
2690 "full-page tip should occupy the remaining pooled slot before sync"
2691 );
2692
2693 append.sync().await.unwrap();
2694
2695 assert!(
2697 pool.try_alloc(BUFFER_SIZE).is_ok(),
2698 "sync should release pooled backing when no partial tail remains"
2699 );
2700 });
2701 }
2702
2703 #[test_traced("DEBUG")]
2704 fn test_sync_uses_range_sync_for_single_flush() {
2705 let executor = deterministic::Runner::default();
2706 executor.start(|context: deterministic::Context| async move {
2707 let blob = SyncTrackingBlob::new();
2708 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2709 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2710 .await
2711 .unwrap();
2712
2713 append.sync().await.unwrap();
2715 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2716 assert_eq!(writes, 0);
2717 assert_eq!(full_syncs, 1);
2718 assert_eq!(range_syncs, 0);
2719
2720 let data = b"hello world";
2722 append.append(data).await.unwrap();
2723 append.sync().await.unwrap();
2724
2725 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2726 assert_eq!(writes, 1);
2727 assert_eq!(full_syncs, 1);
2728 assert_eq!(range_syncs, 1);
2729
2730 append.sync().await.unwrap();
2732 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2733 assert_eq!(writes, 1);
2734 assert_eq!(full_syncs, 1);
2735 assert_eq!(range_syncs, 1);
2736
2737 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2738 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2739 .await
2740 .unwrap();
2741 let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
2742 assert_eq!(read.as_ref(), data);
2743 });
2744 }
2745
2746 #[test_traced("DEBUG")]
2747 fn test_start_sync_persists_and_marks_clean() {
2749 let executor = deterministic::Runner::default();
2750 executor.start(|context: deterministic::Context| async move {
2751 let blob = SyncTrackingBlob::new();
2752 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2753 let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2754 .await
2755 .unwrap();
2756
2757 let handle = writer.start_sync().await;
2759 handle.await.unwrap();
2761 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2762 assert_eq!(writes, 0);
2763 assert_eq!(full_syncs, 1);
2764 assert_eq!(range_syncs, 0);
2765
2766 let data = b"hello world";
2768 writer.append(data).await.unwrap();
2769 writer.sync().await.unwrap();
2770 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2771 assert_eq!(writes, 1);
2772 assert_eq!(full_syncs, 1);
2773 assert_eq!(range_syncs, 1);
2774
2775 let handle = writer.start_sync().await;
2777 handle.await.unwrap();
2778 let (_, _, full_syncs, range_syncs) = blob.snapshot();
2779 assert_eq!(full_syncs, 1);
2780 assert_eq!(range_syncs, 1);
2781
2782 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2784 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2785 .await
2786 .unwrap();
2787 let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
2788 assert_eq!(read.as_ref(), data);
2789 });
2790 }
2791
2792 #[test_traced("DEBUG")]
2793 fn test_sync_waits_for_outstanding_start_sync() {
2795 let executor = deterministic::Runner::default();
2796 executor.start(|context: deterministic::Context| async move {
2797 let inner = SyncTrackingBlob::new();
2798 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2799 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2800 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2801
2802 let handle = writer.start_sync().await;
2803 let deferred = next_pending_sync(&pending);
2804
2805 let mut sync = Box::pin(writer.sync());
2807 assert!(
2808 sync.as_mut().now_or_never().is_none(),
2809 "sync must wait for the outstanding start_sync handle"
2810 );
2811 drop(sync);
2812 let (_, _, full_syncs, range_syncs) = inner.snapshot();
2813 assert_eq!(full_syncs, 0);
2814 assert_eq!(range_syncs, 0);
2815
2816 deferred.release.send(Ok(())).unwrap();
2818 writer.sync().await.unwrap();
2819 handle.await.unwrap();
2820 let (_, _, full_syncs, range_syncs) = inner.snapshot();
2821 assert_eq!(full_syncs, 1);
2822 assert_eq!(range_syncs, 0);
2823 });
2824 }
2825
2826 #[test_traced("DEBUG")]
2827 fn test_sync_after_start_sync_and_small_write_waits_before_range_sync() {
2829 let executor = deterministic::Runner::default();
2830 executor.start(|context: deterministic::Context| async move {
2831 let inner = SyncTrackingBlob::new();
2832 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2833 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2834 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2835
2836 let handle = writer.start_sync().await;
2837 let deferred = next_pending_sync(&pending);
2838 writer.append(b"hello world").await.unwrap();
2839
2840 let mut sync = Box::pin(writer.sync());
2842 assert!(
2843 sync.as_mut().now_or_never().is_none(),
2844 "sync must join the outstanding barrier before flushing the small write"
2845 );
2846 drop(sync);
2847 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2848 assert_eq!(writes, 0);
2849 assert_eq!(full_syncs, 0);
2850 assert_eq!(range_syncs, 0);
2851
2852 deferred.release.send(Ok(())).unwrap();
2854 writer.sync().await.unwrap();
2855 handle.await.unwrap();
2856 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2857 assert_eq!(writes, 1);
2858 assert_eq!(full_syncs, 1);
2859 assert_eq!(range_syncs, 1);
2860 });
2861 }
2862
2863 #[test_traced("DEBUG")]
2864 fn test_write_flush_waits_for_outstanding_start_sync() {
2866 let executor = deterministic::Runner::default();
2867 executor.start(|context: deterministic::Context| async move {
2868 let inner = SyncTrackingBlob::new();
2869 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2870 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2871 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2872
2873 let handle = writer.start_sync().await;
2874 let deferred = next_pending_sync(&pending);
2875
2876 let data = vec![7; BUFFER_SIZE + PAGE_SIZE.get() as usize];
2877 let append = context.child("append").spawn(move |_| async move {
2878 writer.append(&data).await.unwrap();
2879 writer
2880 });
2881 deferred
2883 .blocked
2884 .await
2885 .expect("append never waited on start_sync");
2886 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2887 assert_eq!(writes, 0);
2888 assert_eq!(full_syncs, 0);
2889 assert_eq!(range_syncs, 0);
2890
2891 deferred.release.send(Ok(())).unwrap();
2893 let mut writer = append.await.unwrap();
2894 handle.await.unwrap();
2895 writer.sync().await.unwrap();
2896 let (_, writes, full_syncs, _) = inner.snapshot();
2897 assert!(writes > 0);
2898 assert!(full_syncs > 0);
2899 });
2900 }
2901
2902 #[test_traced("DEBUG")]
2903 fn test_seal_waits_for_outstanding_start_sync_before_flushing() {
2905 let executor = deterministic::Runner::default();
2906 executor.start(|context: deterministic::Context| async move {
2907 let inner = SyncTrackingBlob::new();
2908 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2909 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2910 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2911
2912 let prior = writer.start_sync().await;
2913 let deferred = next_pending_sync(&pending);
2914 writer.append(b"hello world").await.unwrap();
2915
2916 let seal = context
2917 .child("seal")
2918 .spawn(move |_| async move { writer.seal().await });
2919 deferred
2921 .blocked
2922 .await
2923 .expect("seal never waited on start_sync");
2924 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2925 assert_eq!(writes, 0);
2926 assert_eq!(full_syncs, 0);
2927 assert_eq!(range_syncs, 0);
2928
2929 deferred.release.send(Ok(())).unwrap();
2931 let (sealed, sync) = seal.await.unwrap().unwrap();
2932 prior.await.unwrap();
2933
2934 let deferred = next_pending_sync(&pending);
2936 deferred.release.send(Ok(())).unwrap();
2937 sync.await.unwrap();
2938 let read = sealed
2939 .read_at(0, b"hello world".len())
2940 .await
2941 .unwrap()
2942 .coalesce();
2943 assert_eq!(read.as_ref(), b"hello world");
2944 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2945 assert_eq!(writes, 1);
2946 assert_eq!(full_syncs, 2);
2947 assert_eq!(range_syncs, 0);
2948 });
2949 }
2950
2951 #[test_traced("DEBUG")]
2952 fn test_snapshot_waits_for_outstanding_start_sync_before_flushing() {
2954 let executor = deterministic::Runner::default();
2955 executor.start(|context: deterministic::Context| async move {
2956 let inner = SyncTrackingBlob::new();
2957 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2958 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2959 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2960
2961 let prior = writer.start_sync().await;
2963 let deferred = next_pending_sync(&pending);
2964 writer.append(b"hello world").await.unwrap();
2965
2966 let snapshot = context.child("snapshot").spawn(move |_| async move {
2967 let snapshot = writer.snapshot().await.unwrap();
2968 let read = snapshot
2969 .read_at(0, b"hello world".len())
2970 .await
2971 .unwrap()
2972 .coalesce();
2973 assert_eq!(read.as_ref(), b"hello world");
2974 writer
2975 });
2976
2977 deferred
2979 .blocked
2980 .await
2981 .expect("snapshot never waited on start_sync");
2982 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2983 assert_eq!(writes, 0);
2984 assert_eq!(full_syncs, 0);
2985 assert_eq!(range_syncs, 0);
2986
2987 deferred.release.send(Ok(())).unwrap();
2989 let _writer = snapshot.await.unwrap();
2990 prior.await.unwrap();
2991 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2992 assert_eq!(writes, 1);
2993 assert_eq!(full_syncs, 1);
2994 assert_eq!(range_syncs, 0);
2995 });
2996 }
2997
2998 #[test_traced("DEBUG")]
2999 fn test_replay_waits_for_outstanding_start_sync_before_flushing() {
3001 let executor = deterministic::Runner::default();
3002 executor.start(|context: deterministic::Context| async move {
3003 let inner = SyncTrackingBlob::new();
3004 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3005 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3006 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3007
3008 let prior = writer.start_sync().await;
3010 let deferred = next_pending_sync(&pending);
3011 writer.append(b"hello world").await.unwrap();
3012
3013 let replay = context.child("replay").spawn(move |_| async move {
3014 {
3015 let mut replay = writer
3016 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
3017 .await
3018 .unwrap();
3019 assert!(replay.ensure(1).await.unwrap());
3020 assert_eq!(replay.chunk()[0], b'h');
3021 }
3022 writer
3023 });
3024
3025 deferred
3027 .blocked
3028 .await
3029 .expect("replay never waited on start_sync");
3030 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3031 assert_eq!(writes, 0);
3032 assert_eq!(full_syncs, 0);
3033 assert_eq!(range_syncs, 0);
3034
3035 deferred.release.send(Ok(())).unwrap();
3037 let _writer = replay.await.unwrap();
3038 prior.await.unwrap();
3039 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3040 assert_eq!(writes, 1);
3041 assert_eq!(full_syncs, 1);
3042 assert_eq!(range_syncs, 0);
3043 });
3044 }
3045
3046 #[test_traced("DEBUG")]
3047 fn test_resize_grow_waits_for_outstanding_start_sync_before_writing() {
3049 let executor = deterministic::Runner::default();
3050 executor.start(|context: deterministic::Context| async move {
3051 let inner = SyncTrackingBlob::new();
3052 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3053 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3054 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3055
3056 let prior = writer.start_sync().await;
3058 let deferred = next_pending_sync(&pending);
3059
3060 let target_size = (BUFFER_SIZE + PAGE_SIZE.get() as usize) as u64;
3061 let resize = context.child("resize_grow").spawn(move |_| async move {
3062 writer.resize(target_size).await.unwrap();
3063 writer
3064 });
3065
3066 deferred
3068 .blocked
3069 .await
3070 .expect("resize grow never waited on start_sync");
3071 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
3072 assert_eq!(writes, 0);
3073 assert_eq!(full_syncs, 0);
3074 assert_eq!(range_syncs, 0);
3075
3076 deferred.release.send(Ok(())).unwrap();
3078 let mut writer = resize.await.unwrap();
3079 prior.await.unwrap();
3080 assert_eq!(writer.size(), target_size);
3081 writer.sync().await.unwrap();
3082 let (_, writes, full_syncs, _) = inner.snapshot();
3083 assert!(writes > 0);
3084 assert!(full_syncs > 0);
3085 });
3086 }
3087
3088 #[test_traced("DEBUG")]
3089 fn test_resize_shrink_waits_for_outstanding_start_sync_before_resizing() {
3091 let executor = deterministic::Runner::default();
3092 executor.start(|context: deterministic::Context| async move {
3093 let inner = SyncTrackingBlob::new();
3094 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
3095 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3096 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
3097
3098 let data = vec![3; PAGE_SIZE.get() as usize * 2];
3100 writer.append(&data).await.unwrap();
3101 writer.sync().await.unwrap();
3102 writer.append(b"x").await.unwrap();
3103 let prior = writer.start_sync().await;
3104 let deferred = next_pending_sync(&pending);
3105 let physical_size = inner.size();
3106
3107 let resize = context.child("resize_shrink").spawn(move |_| async move {
3108 writer.resize(PAGE_SIZE.get() as u64).await.unwrap();
3109 writer
3110 });
3111
3112 deferred
3114 .blocked
3115 .await
3116 .expect("resize shrink never waited on start_sync");
3117 assert_eq!(
3118 inner.size(),
3119 physical_size,
3120 "resize must not shrink the blob before the pending sync finishes"
3121 );
3122
3123 deferred.release.send(Ok(())).unwrap();
3125 let writer = resize.await.unwrap();
3126 prior.await.unwrap();
3127 assert_eq!(writer.size(), PAGE_SIZE.get() as u64);
3128 assert!(inner.size() < physical_size);
3129 });
3130 }
3131
3132 #[test_traced("DEBUG")]
3133 fn test_sync_failed_range_sync_does_not_mark_clean() {
3134 let executor = deterministic::Runner::default();
3135 executor.start(|context: deterministic::Context| async move {
3136 let name = b"failed_range_sync";
3137 let (blob, size) = context.open("test_partition", name).await.unwrap();
3138 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3139 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3140 .await
3141 .unwrap();
3142
3143 append.append(b"abc").await.unwrap();
3145
3146 context.remove("test_partition", Some(name)).await.unwrap();
3148 assert!(append.sync().await.is_err());
3149
3150 assert!(append.sync().await.is_err());
3153 });
3154 }
3155
3156 #[test_traced("DEBUG")]
3157 fn test_sync_uses_full_sync_after_prior_plain_flush() {
3158 let executor = deterministic::Runner::default();
3159 executor.start(|context: deterministic::Context| async move {
3160 let blob = SyncTrackingBlob::new();
3161 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3162 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3163 .await
3164 .unwrap();
3165
3166 let data = vec![7u8; BUFFER_SIZE + 1];
3169 append.append(&data).await.unwrap();
3170 append.sync().await.unwrap();
3171
3172 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3173 assert_eq!(writes, 2);
3174 assert_eq!(full_syncs, 1);
3175 assert_eq!(range_syncs, 0);
3176
3177 append.sync().await.unwrap();
3179 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3180 assert_eq!(writes, 2);
3181 assert_eq!(full_syncs, 1);
3182 assert_eq!(range_syncs, 0);
3183
3184 append.append(b"tip").await.unwrap();
3187 append.sync().await.unwrap();
3188
3189 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3190 assert_eq!(writes, 3);
3191 assert_eq!(full_syncs, 1);
3192 assert_eq!(range_syncs, 1);
3193
3194 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3195 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3196 .await
3197 .unwrap();
3198 let mut expected = data;
3199 expected.extend_from_slice(b"tip");
3200 let read = reopened
3201 .read_at(0, expected.len())
3202 .await
3203 .unwrap()
3204 .coalesce();
3205 assert_eq!(read.as_ref(), expected.as_slice());
3206 });
3207 }
3208
3209 #[test_traced("DEBUG")]
3210 fn test_sync_uses_full_sync_after_replay_plain_flush() {
3211 let executor = deterministic::Runner::default();
3212 executor.start(|context: deterministic::Context| async move {
3213 let blob = SyncTrackingBlob::new();
3214 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3215 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3216 .await
3217 .unwrap();
3218
3219 append.append(b"replayed").await.unwrap();
3221
3222 let mut replay = append
3224 .replay(NZUsize!(1024), ReadOptions::default())
3225 .await
3226 .unwrap();
3227 assert!(replay.ensure(b"replayed".len()).await.unwrap());
3228 assert_eq!(replay.remaining(), b"replayed".len());
3229 assert_eq!(replay.chunk(), b"replayed");
3230
3231 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3232 assert_eq!(writes, 1);
3233 assert_eq!(full_syncs, 0);
3234 assert_eq!(range_syncs, 0);
3235
3236 append.sync().await.unwrap();
3238 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3239 assert_eq!(writes, 1);
3240 assert_eq!(full_syncs, 1);
3241 assert_eq!(range_syncs, 0);
3242 });
3243 }
3244
3245 #[test_traced("DEBUG")]
3246 fn test_replay_uses_read_options_for_refills_and_seek() {
3247 let executor = deterministic::Runner::default();
3248 executor.start(|context: deterministic::Context| async move {
3249 let (blob, size) = context
3250 .open("test_partition", b"replay_read_options")
3251 .await
3252 .unwrap();
3253 let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
3254 let read_options = blob.read_options();
3255 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3256 let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
3257 .await
3258 .unwrap();
3259
3260 let page_size = PAGE_SIZE.get() as usize;
3261 let data = vec![3; page_size * 2];
3262 writer.append(&data).await.unwrap();
3263 writer.sync().await.unwrap();
3264 read_options.lock().clear();
3265
3266 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
3267 let mut replay = writer
3268 .replay(NZUsize!(physical_page_size), ReadOptions::DONT_CACHE)
3269 .await
3270 .unwrap();
3271
3272 assert!(replay.ensure(1).await.unwrap());
3275 replay.advance(page_size);
3276 assert!(replay.ensure(1).await.unwrap());
3277
3278 replay.seek_to(0).unwrap();
3280 assert!(replay.ensure(1).await.unwrap());
3281
3282 assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 3]);
3283 });
3284 }
3285
3286 #[test_traced("DEBUG")]
3287 fn test_recreated_sync_preserves_replay_plain_flush_barrier() {
3288 let executor = deterministic::Runner::default();
3289 executor.start(|context: deterministic::Context| async move {
3290 let blob = SyncTrackingBlob::new();
3291 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3292 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3293 .await
3294 .unwrap();
3295
3296 append.append(b"replayed").await.unwrap();
3297 let mut replay = append
3298 .replay(NZUsize!(1024), ReadOptions::default())
3299 .await
3300 .unwrap();
3301 assert!(replay.ensure(b"replayed".len()).await.unwrap());
3302 assert_eq!(replay.remaining(), b"replayed".len());
3303 assert_eq!(replay.chunk(), b"replayed");
3304 drop(replay);
3305 drop(append);
3306
3307 let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
3308 assert!(durable.is_empty());
3309 assert_eq!(writes, 1);
3310 assert_eq!(full_syncs, 0);
3311 assert_eq!(range_syncs, 0);
3312
3313 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3314 let mut reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3315 .await
3316 .unwrap();
3317 assert_eq!(reopened.size(), b"replayed".len() as u64);
3318 reopened.sync().await.unwrap();
3319
3320 let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
3321 assert_eq!(durable.len(), blob.size() as usize);
3322 assert_eq!(writes, 1);
3323 assert_eq!(full_syncs, 1);
3324 assert_eq!(range_syncs, 0);
3325 });
3326 }
3327
3328 #[test_traced("DEBUG")]
3329 fn test_recreated_sync_skips_barrier_after_invalid_truncation() {
3330 let executor = deterministic::Runner::default();
3331 executor.start(|context: deterministic::Context| async move {
3332 let blob = SyncTrackingBlob::new();
3333 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3334 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3335 .await
3336 .unwrap();
3337 append.sync().await.unwrap();
3338 append.append(b"valid").await.unwrap();
3339 append.sync().await.unwrap();
3340 drop(append);
3341
3342 blob.write_at(blob.size(), b"junk", WriteOptions::default())
3343 .await
3344 .unwrap();
3345
3346 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3347 let mut reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3348 .await
3349 .unwrap();
3350 assert_eq!(reopened.size(), b"valid".len() as u64);
3351
3352 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3353 assert_eq!(writes, 2);
3354 assert_eq!(full_syncs, 2);
3355 assert_eq!(range_syncs, 1);
3356
3357 reopened.sync().await.unwrap();
3358
3359 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3360 assert_eq!(writes, 2);
3361 assert_eq!(full_syncs, 2);
3362 assert_eq!(range_syncs, 1);
3363 });
3364 }
3365
3366 #[test_traced("DEBUG")]
3367 fn test_sync_fuses_partial_page_rewrite() {
3368 let executor = deterministic::Runner::default();
3369 executor.start(|context: deterministic::Context| async move {
3370 let blob = SyncTrackingBlob::new();
3371 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3372 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
3373 .await
3374 .unwrap();
3375 append.sync().await.unwrap();
3376
3377 append.append(b"abc").await.unwrap();
3379 append.sync().await.unwrap();
3380
3381 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3382 assert_eq!(writes, 1);
3383 assert_eq!(full_syncs, 1);
3384 assert_eq!(range_syncs, 1);
3385
3386 let slot0_offset = PAGE_SIZE.get() as u64;
3387 let slot1_offset = slot0_offset + CHECKSUM_SLOT_SIZE as u64;
3388 let slot0_before: Vec<u8> = blob
3389 .read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3390 .await
3391 .unwrap()
3392 .coalesce()
3393 .freeze()
3394 .into();
3395
3396 append.append(b"de").await.unwrap();
3400 append.sync().await.unwrap();
3401
3402 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3403 assert_eq!(writes, 2);
3404 assert_eq!(full_syncs, 1);
3405 assert_eq!(range_syncs, 2);
3406
3407 let slot0_after: Vec<u8> = blob
3408 .read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3409 .await
3410 .unwrap()
3411 .coalesce()
3412 .freeze()
3413 .into();
3414 assert_eq!(
3415 slot0_before, slot0_after,
3416 "protected slot must be resubmitted byte-identically"
3417 );
3418 let slot1_between: Vec<u8> = blob
3419 .read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3420 .await
3421 .unwrap()
3422 .coalesce()
3423 .freeze()
3424 .into();
3425
3426 append.append(b"fg").await.unwrap();
3428 append.sync().await.unwrap();
3429
3430 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
3431 assert_eq!(writes, 3);
3432 assert_eq!(full_syncs, 1);
3433 assert_eq!(range_syncs, 3);
3434
3435 let slot1_after: Vec<u8> = blob
3436 .read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
3437 .await
3438 .unwrap()
3439 .coalesce()
3440 .freeze()
3441 .into();
3442 assert_eq!(
3443 slot1_between, slot1_after,
3444 "protected slot must be resubmitted byte-identically"
3445 );
3446
3447 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3448 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
3449 .await
3450 .unwrap();
3451 let read = reopened.read_at(0, 7).await.unwrap().coalesce();
3452 assert_eq!(read.as_ref(), b"abcdefg");
3453 });
3454 }
3455
3456 #[test_traced("DEBUG")]
3457 fn test_read_up_to_zero_len_truncates_buffer() {
3458 let executor = deterministic::Runner::default();
3459 executor.start(|context: deterministic::Context| async move {
3460 let (blob, blob_size) = context
3462 .open("test_partition", b"read_up_to_zero_len")
3463 .await
3464 .unwrap();
3465 assert_eq!(blob_size, 0);
3466
3467 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3469
3470 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
3472 .await
3473 .unwrap();
3474 append.append(&[1, 2, 3, 4]).await.unwrap();
3475
3476 let stale = vec![9u8, 8, 7, 6];
3478 let (buf, read) = append.read_up_to(0, 0, stale).await.unwrap();
3479
3480 assert_eq!(read, 0);
3481 assert_eq!(buf.len(), 0, "read_up_to must truncate returned buffer");
3482 assert_eq!(buf.freeze().as_ref(), b"");
3483 });
3484 }
3485
3486 fn read_crc_record_from_page(page_bytes: &[u8]) -> Checksum {
3488 let crc_start = page_bytes.len() - CHECKSUM_SIZE as usize;
3489 Checksum::read(&mut &page_bytes[crc_start..]).unwrap()
3490 }
3491
3492 #[derive(Clone)]
3494 struct PartialWriteBlob<B: Blob> {
3495 inner: B,
3496 writes: Arc<AtomicUsize>,
3497 read_options: Arc<Mutex<Vec<ReadOptions>>>,
3498 failed_write_len: Arc<AtomicUsize>,
3499 fail_on: usize,
3500 partial_len: usize,
3501 }
3502
3503 impl<B: Blob> PartialWriteBlob<B> {
3504 fn new(inner: B, fail_on: usize, partial_len: usize) -> Self {
3505 Self {
3506 inner,
3507 writes: Arc::new(AtomicUsize::new(0)),
3508 read_options: Arc::new(Mutex::new(Vec::new())),
3509 failed_write_len: Arc::new(AtomicUsize::new(0)),
3510 fail_on,
3511 partial_len,
3512 }
3513 }
3514
3515 fn failed_write_len(&self) -> Arc<AtomicUsize> {
3516 self.failed_write_len.clone()
3517 }
3518
3519 fn write_count(&self) -> Arc<AtomicUsize> {
3520 self.writes.clone()
3521 }
3522
3523 fn read_options(&self) -> Arc<Mutex<Vec<ReadOptions>>> {
3524 self.read_options.clone()
3525 }
3526 }
3527
3528 impl<B: Blob> crate::Blob for PartialWriteBlob<B> {
3529 async fn read_at(
3530 &self,
3531 offset: u64,
3532 len: usize,
3533 options: ReadOptions,
3534 ) -> Result<IoBufsMut, Error> {
3535 self.read_options.lock().push(options);
3536 self.inner.read_at(offset, len, options).await
3537 }
3538
3539 async fn read_at_buf(
3540 &self,
3541 offset: u64,
3542 len: usize,
3543 bufs: impl Into<IoBufsMut> + Send,
3544 options: ReadOptions,
3545 ) -> Result<IoBufsMut, Error> {
3546 self.read_options.lock().push(options);
3547 self.inner.read_at_buf(offset, len, bufs, options).await
3548 }
3549
3550 async fn write_at(
3551 &self,
3552 offset: u64,
3553 bufs: impl Into<IoBufs> + Send,
3554 options: WriteOptions,
3555 ) -> Result<(), Error> {
3556 let bufs = bufs.into();
3557 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
3558 if write == self.fail_on {
3559 let bytes = bufs.coalesce();
3560 self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
3561 let partial_len = self.partial_len.min(bytes.len());
3562 self.inner
3563 .write_at(offset, bytes.slice(..partial_len), options)
3564 .await?;
3565 if !options.contains(WriteOptions::SYNC) {
3566 self.inner.sync().await?;
3567 }
3568 return Err(Error::Io(
3569 std::io::Error::other("injected partial write").into(),
3570 ));
3571 }
3572
3573 self.inner.write_at(offset, bufs, options).await
3574 }
3575
3576 async fn resize(&self, len: u64) -> Result<(), Error> {
3577 self.inner.resize(len).await
3578 }
3579
3580 async fn sync(&self) -> Result<(), Error> {
3581 self.inner.sync().await
3582 }
3583
3584 async fn start_sync(&self) -> Handle<()> {
3585 self.inner.start_sync().await
3586 }
3587 }
3588
3589 #[derive(Clone)]
3591 struct TornExtensionBlob<B: Blob> {
3592 inner: B,
3593 writes: Arc<AtomicUsize>,
3594 fail_on: usize,
3595 durable_payload_len: usize,
3596 }
3597
3598 impl<B: Blob> crate::Blob for TornExtensionBlob<B> {
3599 async fn read_at(
3600 &self,
3601 offset: u64,
3602 len: usize,
3603 options: ReadOptions,
3604 ) -> Result<IoBufsMut, Error> {
3605 self.inner.read_at(offset, len, options).await
3606 }
3607
3608 async fn read_at_buf(
3609 &self,
3610 offset: u64,
3611 len: usize,
3612 bufs: impl Into<IoBufsMut> + Send,
3613 options: ReadOptions,
3614 ) -> Result<IoBufsMut, Error> {
3615 self.inner.read_at_buf(offset, len, bufs, options).await
3616 }
3617
3618 async fn write_at(
3619 &self,
3620 offset: u64,
3621 bufs: impl Into<IoBufs> + Send,
3622 options: WriteOptions,
3623 ) -> Result<(), Error> {
3624 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
3625 if write != self.fail_on {
3626 return self.inner.write_at(offset, bufs, options).await;
3627 }
3628
3629 let bytes = bufs.into().coalesce();
3630 let footer_start = bytes
3631 .len()
3632 .checked_sub(CHECKSUM_SIZE as usize)
3633 .expect("physical page must contain a checksum footer");
3634 assert!(self.durable_payload_len <= footer_start);
3635 let footer_offset = offset
3636 .checked_add(footer_start as u64)
3637 .ok_or(Error::OffsetOverflow)?;
3638 let write_options = options.without(WriteOptions::SYNC);
3639
3640 self.inner
3641 .write_at(
3642 offset,
3643 bytes.slice(..self.durable_payload_len),
3644 write_options,
3645 )
3646 .await?;
3647 self.inner
3648 .write_at(footer_offset, bytes.slice(footer_start..), write_options)
3649 .await?;
3650 self.inner.sync().await?;
3651
3652 Err(Error::Io(
3653 std::io::Error::other("injected torn extension").into(),
3654 ))
3655 }
3656
3657 async fn resize(&self, len: u64) -> Result<(), Error> {
3658 self.inner.resize(len).await
3659 }
3660
3661 async fn sync(&self) -> Result<(), Error> {
3662 self.inner.sync().await
3663 }
3664
3665 async fn start_sync(&self) -> Handle<()> {
3666 self.inner.start_sync().await
3667 }
3668 }
3669
3670 #[test_traced("DEBUG")]
3671 fn test_torn_extension_with_new_footer_recovers_previous_prefix() {
3672 let executor = deterministic::Runner::default();
3673 executor.start(|context: deterministic::Context| async move {
3674 let (blob, blob_size) = context
3675 .open("test_partition", b"torn_extension_footer")
3676 .await
3677 .unwrap();
3678 let write_count = Arc::new(AtomicUsize::new(0));
3679 let faulty_blob = TornExtensionBlob {
3680 inner: blob.clone(),
3681 writes: write_count.clone(),
3682 fail_on: 2,
3683 durable_payload_len: 4,
3684 };
3685 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3686 let mut writer = Writer::new(faulty_blob, blob_size, BUFFER_SIZE, cache_ref)
3687 .await
3688 .unwrap();
3689
3690 writer.append(b"abc").await.unwrap();
3691 writer.sync().await.unwrap();
3692 assert_eq!(write_count.load(Ordering::SeqCst), 1);
3693
3694 writer.append(b"def").await.unwrap();
3695 assert!(writer.sync().await.is_err(), "extension write should fail");
3696 assert_eq!(write_count.load(Ordering::SeqCst), 2);
3697 drop(writer);
3698
3699 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3700 let page = blob
3701 .read_at(0, physical_page_size, ReadOptions::default())
3702 .await
3703 .unwrap()
3704 .coalesce();
3705 assert_eq!(&page.as_ref()[..6], b"abcd\0\0");
3706 let checksum = read_crc_record_from_page(page.as_ref());
3707 assert_eq!(checksum.len1, 3);
3708 assert_eq!(checksum.len2, 6);
3709
3710 let (blob, blob_size) = context
3711 .open("test_partition", b"torn_extension_footer")
3712 .await
3713 .unwrap();
3714 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3715 let recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
3716 .await
3717 .unwrap();
3718 assert_eq!(recovered.size(), 3);
3719
3720 assert_eq!(
3723 recovered
3724 .recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
3725 .await
3726 .unwrap(),
3727 3
3728 );
3729 let data = recovered.read_at(0, 3).await.unwrap().coalesce();
3730 assert_eq!(data.as_ref(), b"abc");
3731 });
3732 }
3733
3734 #[derive(Clone)]
3736 struct DelayedReadBlob<B: Blob> {
3737 inner: B,
3738 offset: u64,
3739 len: usize,
3740 reads: Arc<AtomicUsize>,
3741 started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
3742 release: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
3743 }
3744
3745 impl<B: Blob> DelayedReadBlob<B> {
3746 fn new(
3747 inner: B,
3748 offset: u64,
3749 len: usize,
3750 started: oneshot::Sender<()>,
3751 release: oneshot::Receiver<()>,
3752 ) -> Self {
3753 Self {
3754 inner,
3755 offset,
3756 len,
3757 reads: Arc::new(AtomicUsize::new(0)),
3758 started: Arc::new(Mutex::new(Some(started))),
3759 release: Arc::new(Mutex::new(Some(release))),
3760 }
3761 }
3762 }
3763
3764 impl<B: Blob> crate::Blob for DelayedReadBlob<B> {
3765 async fn read_at(
3766 &self,
3767 offset: u64,
3768 len: usize,
3769 options: ReadOptions,
3770 ) -> Result<IoBufsMut, Error> {
3771 if offset == self.offset
3772 && len == self.len
3773 && self.reads.fetch_add(1, Ordering::SeqCst) == 0
3774 {
3775 let bytes = self.inner.read_at(offset, len, options).await?;
3776
3777 let sender = self
3778 .started
3779 .lock()
3780 .take()
3781 .expect("delayed read start signal consumed more than once");
3782 let _ = sender.send(());
3783
3784 let release = self
3785 .release
3786 .lock()
3787 .take()
3788 .expect("delayed read release receiver consumed more than once");
3789 release.await.expect("release signal dropped");
3790
3791 return Ok(bytes);
3792 }
3793
3794 self.inner.read_at(offset, len, options).await
3795 }
3796
3797 async fn read_at_buf(
3798 &self,
3799 offset: u64,
3800 len: usize,
3801 bufs: impl Into<IoBufsMut> + Send,
3802 options: ReadOptions,
3803 ) -> Result<IoBufsMut, Error> {
3804 if offset == self.offset
3805 && len == self.len
3806 && self.reads.fetch_add(1, Ordering::SeqCst) == 0
3807 {
3808 let bytes = self.inner.read_at_buf(offset, len, bufs, options).await?;
3809
3810 let sender = self
3811 .started
3812 .lock()
3813 .take()
3814 .expect("delayed read start signal consumed more than once");
3815 let _ = sender.send(());
3816
3817 let release = self
3818 .release
3819 .lock()
3820 .take()
3821 .expect("delayed read release receiver consumed more than once");
3822 release.await.expect("release signal dropped");
3823
3824 return Ok(bytes);
3825 }
3826
3827 self.inner.read_at_buf(offset, len, bufs, options).await
3828 }
3829
3830 async fn write_at(
3831 &self,
3832 offset: u64,
3833 bufs: impl Into<IoBufs> + Send,
3834 options: WriteOptions,
3835 ) -> Result<(), Error> {
3836 self.inner.write_at(offset, bufs, options).await
3837 }
3838
3839 async fn resize(&self, len: u64) -> Result<(), Error> {
3840 self.inner.resize(len).await
3841 }
3842
3843 async fn sync(&self) -> Result<(), Error> {
3844 self.inner.sync().await
3845 }
3846
3847 async fn start_sync(&self) -> Handle<()> {
3848 self.inner.start_sync().await
3849 }
3850 }
3851
3852 const DUMMY_MARKER: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
3855
3856 #[test_traced("DEBUG")]
3859 fn test_to_physical_pages_zero_copy_full_pages_and_materialized_partial() {
3860 let executor = deterministic::Runner::default();
3867 executor.start(|context: deterministic::Context| async move {
3868 let (blob, blob_size) = context
3870 .open("test_partition", b"to_physical_pages_zero_copy")
3871 .await
3872 .unwrap();
3873 assert_eq!(blob_size, 0);
3874
3875 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3877
3878 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
3880 .await
3881 .unwrap();
3882
3883 let page_size = PAGE_SIZE.get() as usize;
3886 let partial_len = 17usize;
3887 let data: Vec<u8> = (0..(page_size * 2 + partial_len))
3888 .map(|i| (i % 251) as u8)
3889 .collect();
3890
3891 let mut buffer = Buffer::new(0, data.len(), cache_ref.pool().clone());
3893 let over_capacity = buffer.append(&data);
3894 assert!(!over_capacity);
3895
3896 let (physical_pages, partial_page_state) =
3898 append.to_physical_pages(&buffer, true, None, None);
3899
3900 assert_eq!(physical_pages.chunk_count(), 5);
3903
3904 let checksum = partial_page_state.expect("partial page state must be returned");
3906 assert_eq!(checksum.len as usize, partial_len);
3907
3908 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
3911 let coalesced = physical_pages.coalesce();
3912 assert_eq!(coalesced.len(), physical_page_size * 3);
3913
3914 assert_eq!(&coalesced.as_ref()[..page_size], &data[..page_size]);
3916 assert_eq!(
3917 &coalesced.as_ref()[physical_page_size..physical_page_size + page_size],
3918 &data[page_size..page_size * 2],
3919 );
3920
3921 let partial_start = physical_page_size * 2;
3924 assert_eq!(
3925 &coalesced.as_ref()[partial_start..partial_start + partial_len],
3926 &data[page_size * 2..],
3927 );
3928 assert!(
3929 coalesced.as_ref()[partial_start + partial_len..partial_start + page_size]
3930 .iter()
3931 .all(|byte| *byte == 0)
3932 );
3933
3934 assert!(Checksum::validate_page(&coalesced.as_ref()[..physical_page_size]).is_some());
3936 assert!(
3937 Checksum::validate_page(
3938 &coalesced.as_ref()[physical_page_size..physical_page_size * 2]
3939 )
3940 .is_some()
3941 );
3942 assert!(
3943 Checksum::validate_page(
3944 &coalesced.as_ref()[physical_page_size * 2..physical_page_size * 3]
3945 )
3946 .is_some()
3947 );
3948 });
3949 }
3950
3951 #[test_traced("DEBUG")]
3957 fn test_crc_slot1_protected() {
3958 let executor = deterministic::Runner::default();
3959 executor.start(|context: deterministic::Context| async move {
3960 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3961 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3962 let slot0_offset = PAGE_SIZE.get() as u64;
3963 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3964
3965 let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
3967 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3968 .await
3969 .unwrap();
3970 append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3971 append.sync().await.unwrap();
3972 drop(append);
3973
3974 let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
3976 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3977 .await
3978 .unwrap();
3979 append
3980 .append(&(11..=30).collect::<Vec<u8>>())
3981 .await
3982 .unwrap();
3983 append.sync().await.unwrap();
3984 drop(append);
3985
3986 let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
3988 let page = blob
3989 .read_at(0, physical_page_size, ReadOptions::default())
3990 .await
3991 .unwrap()
3992 .coalesce();
3993 let crc = read_crc_record_from_page(page.as_ref());
3994 assert!(
3995 crc.len2 > crc.len1,
3996 "Slot 1 should be authoritative (len2={} > len1={})",
3997 crc.len2,
3998 crc.len1
3999 );
4000
4001 let slot1_before: Vec<u8> = blob
4003 .read_at(slot1_offset, 6, ReadOptions::default())
4004 .await
4005 .unwrap()
4006 .coalesce()
4007 .freeze()
4008 .into();
4009
4010 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4012 .await
4013 .unwrap();
4014 blob.sync().await.unwrap();
4015
4016 let slot0_mangled: Vec<u8> = blob
4018 .read_at(slot0_offset, 6, ReadOptions::default())
4019 .await
4020 .unwrap()
4021 .coalesce()
4022 .freeze()
4023 .into();
4024 assert_eq!(slot0_mangled, DUMMY_MARKER, "Mangle failed");
4025
4026 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4028 .await
4029 .unwrap();
4030 append
4031 .append(&(31..=50).collect::<Vec<u8>>())
4032 .await
4033 .unwrap();
4034 append.sync().await.unwrap();
4035 drop(append);
4036
4037 let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
4039
4040 let slot0_after: Vec<u8> = blob
4042 .read_at(slot0_offset, 6, ReadOptions::default())
4043 .await
4044 .unwrap()
4045 .coalesce()
4046 .freeze()
4047 .into();
4048 assert_ne!(
4049 slot0_after, DUMMY_MARKER,
4050 "Slot 0 should have been overwritten with new CRC"
4051 );
4052
4053 let slot1_after: Vec<u8> = blob
4055 .read_at(slot1_offset, 6, ReadOptions::default())
4056 .await
4057 .unwrap()
4058 .coalesce()
4059 .freeze()
4060 .into();
4061 assert_eq!(
4062 slot1_before, slot1_after,
4063 "Slot 1 was modified! Protected region violated."
4064 );
4065
4066 let page = blob
4068 .read_at(0, physical_page_size, ReadOptions::default())
4069 .await
4070 .unwrap()
4071 .coalesce();
4072 let crc = read_crc_record_from_page(page.as_ref());
4073 assert_eq!(crc.len1, 50, "Slot 0 should have len=50");
4074 });
4075 }
4076
4077 #[test_traced("DEBUG")]
4083 fn test_crc_slot0_protected() {
4084 let executor = deterministic::Runner::default();
4085 executor.start(|context: deterministic::Context| async move {
4086 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4087 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4088 let slot0_offset = PAGE_SIZE.get() as u64;
4089 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4090
4091 let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
4093 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4094 .await
4095 .unwrap();
4096 append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
4097 append.sync().await.unwrap();
4098 drop(append);
4099
4100 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4102 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4103 .await
4104 .unwrap();
4105 append
4106 .append(&(11..=30).collect::<Vec<u8>>())
4107 .await
4108 .unwrap();
4109 append.sync().await.unwrap();
4110 drop(append);
4111
4112 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4114 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4115 .await
4116 .unwrap();
4117 append
4118 .append(&(31..=50).collect::<Vec<u8>>())
4119 .await
4120 .unwrap();
4121 append.sync().await.unwrap();
4122 drop(append);
4123
4124 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
4126 let page = blob
4127 .read_at(0, physical_page_size, ReadOptions::default())
4128 .await
4129 .unwrap()
4130 .coalesce();
4131 let crc = read_crc_record_from_page(page.as_ref());
4132 assert!(
4133 crc.len1 > crc.len2,
4134 "Slot 0 should be authoritative (len1={} > len2={})",
4135 crc.len1,
4136 crc.len2
4137 );
4138
4139 let slot0_before: Vec<u8> = blob
4141 .read_at(slot0_offset, 6, ReadOptions::default())
4142 .await
4143 .unwrap()
4144 .coalesce()
4145 .freeze()
4146 .into();
4147
4148 blob.write_at(slot1_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4150 .await
4151 .unwrap();
4152 blob.sync().await.unwrap();
4153
4154 let slot1_mangled: Vec<u8> = blob
4156 .read_at(slot1_offset, 6, ReadOptions::default())
4157 .await
4158 .unwrap()
4159 .coalesce()
4160 .freeze()
4161 .into();
4162 assert_eq!(slot1_mangled, DUMMY_MARKER, "Mangle failed");
4163
4164 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4166 .await
4167 .unwrap();
4168 append
4169 .append(&(51..=70).collect::<Vec<u8>>())
4170 .await
4171 .unwrap();
4172 append.sync().await.unwrap();
4173 drop(append);
4174
4175 let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
4177
4178 let slot1_after: Vec<u8> = blob
4180 .read_at(slot1_offset, 6, ReadOptions::default())
4181 .await
4182 .unwrap()
4183 .coalesce()
4184 .freeze()
4185 .into();
4186 assert_ne!(
4187 slot1_after, DUMMY_MARKER,
4188 "Slot 1 should have been overwritten with new CRC"
4189 );
4190
4191 let slot0_after: Vec<u8> = blob
4193 .read_at(slot0_offset, 6, ReadOptions::default())
4194 .await
4195 .unwrap()
4196 .coalesce()
4197 .freeze()
4198 .into();
4199 assert_eq!(
4200 slot0_before, slot0_after,
4201 "Slot 0 was modified! Protected region violated."
4202 );
4203
4204 let page = blob
4206 .read_at(0, physical_page_size, ReadOptions::default())
4207 .await
4208 .unwrap()
4209 .coalesce();
4210 let crc = read_crc_record_from_page(page.as_ref());
4211 assert_eq!(crc.len2, 70, "Slot 1 should have len=70");
4212 });
4213 }
4214
4215 #[test_traced("DEBUG")]
4222 fn test_data_prefix_preserved_when_extending() {
4223 let executor = deterministic::Runner::default();
4224 executor.start(|context: deterministic::Context| async move {
4225 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4226 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4227
4228 let (blob, _) = context
4230 .open("test_partition", b"prefix_test")
4231 .await
4232 .unwrap();
4233 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4234 .await
4235 .unwrap();
4236 let data1: Vec<u8> = (1..=20).collect();
4237 append.append(&data1).await.unwrap();
4238 append.sync().await.unwrap();
4239 drop(append);
4240
4241 let (blob, size) = context
4243 .open("test_partition", b"prefix_test")
4244 .await
4245 .unwrap();
4246 assert_eq!(size, physical_page_size as u64);
4247
4248 let prefix_before: Vec<u8> = blob
4249 .read_at(0, 20, ReadOptions::default())
4250 .await
4251 .unwrap()
4252 .coalesce()
4253 .freeze()
4254 .into();
4255
4256 blob.write_at(25, DUMMY_MARKER.to_vec(), WriteOptions::default())
4258 .await
4259 .unwrap();
4260 blob.sync().await.unwrap();
4261
4262 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4264 .await
4265 .unwrap();
4266 append
4267 .append(&(21..=40).collect::<Vec<u8>>())
4268 .await
4269 .unwrap();
4270 append.sync().await.unwrap();
4271 drop(append);
4272
4273 let (blob, _) = context
4275 .open("test_partition", b"prefix_test")
4276 .await
4277 .unwrap();
4278
4279 let prefix_after: Vec<u8> = blob
4281 .read_at(0, 20, ReadOptions::default())
4282 .await
4283 .unwrap()
4284 .coalesce()
4285 .freeze()
4286 .into();
4287 assert_eq!(prefix_before, prefix_after, "Data prefix was modified!");
4288
4289 let overwritten: Vec<u8> = blob
4291 .read_at(25, 6, ReadOptions::default())
4292 .await
4293 .unwrap()
4294 .coalesce()
4295 .freeze()
4296 .into();
4297 assert_eq!(
4298 overwritten,
4299 vec![26, 27, 28, 29, 30, 31],
4300 "New data should overwrite padding area"
4301 );
4302 });
4303 }
4304
4305 #[test_traced("DEBUG")]
4311 fn test_crc_slot_protection_across_page_boundary() {
4312 let executor = deterministic::Runner::default();
4313 executor.start(|context: deterministic::Context| async move {
4314 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4315 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4316 let slot0_offset = PAGE_SIZE.get() as u64;
4317 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
4318
4319 let (blob, _) = context.open("test_partition", b"boundary").await.unwrap();
4321 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4322 .await
4323 .unwrap();
4324 append.append(&(1..=50).collect::<Vec<u8>>()).await.unwrap();
4325 append.sync().await.unwrap();
4326 drop(append);
4327
4328 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4330 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4331 .await
4332 .unwrap();
4333 append
4334 .append(&(51..=80).collect::<Vec<u8>>())
4335 .await
4336 .unwrap();
4337 append.sync().await.unwrap();
4338 drop(append);
4339
4340 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4342 let page = blob
4343 .read_at(0, physical_page_size, ReadOptions::default())
4344 .await
4345 .unwrap()
4346 .coalesce();
4347 let crc = read_crc_record_from_page(page.as_ref());
4348 assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
4349
4350 let slot1_before: Vec<u8> = blob
4352 .read_at(slot1_offset, 6, ReadOptions::default())
4353 .await
4354 .unwrap()
4355 .coalesce()
4356 .freeze()
4357 .into();
4358
4359 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
4361 .await
4362 .unwrap();
4363 blob.sync().await.unwrap();
4364
4365 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4367 .await
4368 .unwrap();
4369 append
4370 .append(&(81..=120).collect::<Vec<u8>>())
4371 .await
4372 .unwrap();
4373 append.sync().await.unwrap();
4374 drop(append);
4375
4376 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
4378 assert_eq!(size, (physical_page_size * 2) as u64, "Should have 2 pages");
4379
4380 let slot0_after: Vec<u8> = blob
4382 .read_at(slot0_offset, 6, ReadOptions::default())
4383 .await
4384 .unwrap()
4385 .coalesce()
4386 .freeze()
4387 .into();
4388 assert_ne!(
4389 slot0_after, DUMMY_MARKER,
4390 "Slot 0 should have full-page CRC"
4391 );
4392
4393 let slot1_after: Vec<u8> = blob
4395 .read_at(slot1_offset, 6, ReadOptions::default())
4396 .await
4397 .unwrap()
4398 .coalesce()
4399 .freeze()
4400 .into();
4401 assert_eq!(
4402 slot1_before, slot1_after,
4403 "Slot 1 was modified during page boundary crossing!"
4404 );
4405
4406 let page0 = blob
4408 .read_at(0, physical_page_size, ReadOptions::default())
4409 .await
4410 .unwrap()
4411 .coalesce();
4412 let crc0 = read_crc_record_from_page(page0.as_ref());
4413 assert_eq!(
4414 crc0.len1,
4415 PAGE_SIZE.get(),
4416 "Slot 0 should have full page length"
4417 );
4418
4419 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4421 .await
4422 .unwrap();
4423 assert_eq!(append.size(), 120);
4424 let all_data: Vec<u8> = append.read_at(0, 120).await.unwrap().coalesce().into();
4425 let expected: Vec<u8> = (1..=120).collect();
4426 assert_eq!(all_data, expected);
4427 });
4428 }
4429
4430 #[test_traced("DEBUG")]
4439 fn test_crc_fallback_on_corrupted_primary() {
4440 let executor = deterministic::Runner::default();
4441 executor.start(|context: deterministic::Context| async move {
4442 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4443 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4444 let crc2_offset = PAGE_SIZE.get() as u64 + 8;
4446
4447 let (blob, _) = context
4449 .open("test_partition", b"crc_fallback")
4450 .await
4451 .unwrap();
4452 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4453 .await
4454 .unwrap();
4455 let data1: Vec<u8> = (1..=10).collect();
4456 append.append(&data1).await.unwrap();
4457 append.sync().await.unwrap();
4458 drop(append);
4459
4460 let (blob, size) = context
4462 .open("test_partition", b"crc_fallback")
4463 .await
4464 .unwrap();
4465 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4466 .await
4467 .unwrap();
4468 append
4469 .append(&(11..=30).collect::<Vec<u8>>())
4470 .await
4471 .unwrap();
4472 append.sync().await.unwrap();
4473 drop(append);
4474
4475 let (blob, size) = context
4477 .open("test_partition", b"crc_fallback")
4478 .await
4479 .unwrap();
4480 assert_eq!(size, physical_page_size as u64);
4481
4482 let page = blob
4483 .read_at(0, physical_page_size, ReadOptions::default())
4484 .await
4485 .unwrap()
4486 .coalesce();
4487 let crc = read_crc_record_from_page(page.as_ref());
4488 assert!(
4489 crc.len2 > crc.len1,
4490 "Slot 1 should be authoritative (len2={} > len1={})",
4491 crc.len2,
4492 crc.len1
4493 );
4494 assert_eq!(crc.len2, 30, "Slot 1 should have len=30");
4495 assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
4496
4497 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
4499 .await
4500 .unwrap();
4501 assert_eq!(append.size(), 30);
4502 let all_data: Vec<u8> = append.read_at(0, 30).await.unwrap().coalesce().into();
4503 let expected: Vec<u8> = (1..=30).collect();
4504 assert_eq!(all_data, expected);
4505 drop(append);
4506
4507 blob.write_at(
4510 crc2_offset,
4511 vec![0xDE, 0xAD, 0xBE, 0xEF],
4512 WriteOptions::default(),
4513 )
4514 .await
4515 .unwrap();
4516 blob.sync().await.unwrap();
4517
4518 let page = blob
4520 .read_at(0, physical_page_size, ReadOptions::default())
4521 .await
4522 .unwrap()
4523 .coalesce();
4524 let crc = read_crc_record_from_page(page.as_ref());
4525 assert_eq!(crc.len2, 30, "len2 should still be 30 after corruption");
4526 assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be our corrupted value");
4527
4528 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4530 .await
4531 .unwrap();
4532
4533 assert_eq!(
4535 append.size(),
4536 10,
4537 "Should fall back to slot 0's 10 bytes after primary CRC corruption"
4538 );
4539
4540 let fallback_data: Vec<u8> = append.read_at(0, 10).await.unwrap().coalesce().into();
4542 assert_eq!(
4543 fallback_data, data1,
4544 "Fallback data should match original 10 bytes"
4545 );
4546
4547 let result = append.read_at(0, 11).await;
4549 assert!(result.is_err(), "Reading beyond fallback size should fail");
4550 });
4551 }
4552
4553 #[test_traced("DEBUG")]
4565 fn test_non_last_page_rejects_partial_fallback() {
4566 let executor = deterministic::Runner::default();
4567 executor.start(|context: deterministic::Context| async move {
4568 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4569 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4570 let page0_crc2_offset = PAGE_SIZE.get() as u64 + 8;
4572
4573 let (blob, _) = context
4575 .open("test_partition", b"non_last_page")
4576 .await
4577 .unwrap();
4578 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
4579 .await
4580 .unwrap();
4581 append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
4582 append.sync().await.unwrap();
4583 drop(append);
4584
4585 let (blob, size) = context
4587 .open("test_partition", b"non_last_page")
4588 .await
4589 .unwrap();
4590 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4591 .await
4592 .unwrap();
4593 append
4595 .append(&(11..=PAGE_SIZE.get() as u8).collect::<Vec<u8>>())
4596 .await
4597 .unwrap();
4598 append.sync().await.unwrap();
4599 drop(append);
4600
4601 let (blob, size) = context
4603 .open("test_partition", b"non_last_page")
4604 .await
4605 .unwrap();
4606 let page = blob
4607 .read_at(0, physical_page_size, ReadOptions::default())
4608 .await
4609 .unwrap()
4610 .coalesce();
4611 let crc = read_crc_record_from_page(page.as_ref());
4612 assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
4613 assert_eq!(
4614 crc.len2,
4615 PAGE_SIZE.get(),
4616 "Slot 1 should have len=103 (full page)"
4617 );
4618 assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
4619
4620 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4622 .await
4623 .unwrap();
4624 append
4626 .append(&(104..=113).collect::<Vec<u8>>())
4627 .await
4628 .unwrap();
4629 append.sync().await.unwrap();
4630 drop(append);
4631
4632 let (blob, size) = context
4634 .open("test_partition", b"non_last_page")
4635 .await
4636 .unwrap();
4637 assert_eq!(
4638 size,
4639 (physical_page_size * 2) as u64,
4640 "Should have 2 physical pages"
4641 );
4642
4643 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
4645 .await
4646 .unwrap();
4647 assert_eq!(append.size(), 113);
4648 let all_data: Vec<u8> = append.read_at(0, 113).await.unwrap().coalesce().into();
4649 let expected: Vec<u8> = (1..=113).collect();
4650 assert_eq!(all_data, expected);
4651 drop(append);
4652
4653 blob.write_at(
4655 page0_crc2_offset,
4656 vec![0xDE, 0xAD, 0xBE, 0xEF],
4657 WriteOptions::default(),
4658 )
4659 .await
4660 .unwrap();
4661 blob.sync().await.unwrap();
4662
4663 let page = blob
4665 .read_at(0, physical_page_size, ReadOptions::default())
4666 .await
4667 .unwrap()
4668 .coalesce();
4669 let crc = read_crc_record_from_page(page.as_ref());
4670 assert_eq!(crc.len2, PAGE_SIZE.get(), "len2 should still be 103");
4671 assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be corrupted");
4672 assert_eq!(crc.len1, 10, "Fallback slot 0 has partial length");
4674
4675 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4681 .await
4682 .unwrap();
4683
4684 assert_eq!(append.size(), 113);
4687
4688 let result = append.read_at(0, 10).await;
4691 assert!(
4692 result.is_err(),
4693 "Reading from corrupted non-last page via Append should fail, but got: {:?}",
4694 result
4695 );
4696 drop(append);
4697
4698 let (blob, size) = context
4700 .open("test_partition", b"non_last_page")
4701 .await
4702 .unwrap();
4703 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4704 .await
4705 .unwrap();
4706 let mut replay = append
4707 .replay(NZUsize!(1024), ReadOptions::default())
4708 .await
4709 .unwrap();
4710
4711 let result = replay.ensure(1).await;
4713 assert!(
4714 result.is_err(),
4715 "Reading from corrupted non-last page via Replay should fail, but got: {:?}",
4716 result
4717 );
4718 });
4719 }
4720
4721 #[test]
4722 fn test_resize_partial_shrink_uses_uncached_read_hint() {
4723 let executor = deterministic::Runner::default();
4724 executor.start(|context| async move {
4725 let (blob, size) = context
4726 .open("test_partition", b"resize_read_options")
4727 .await
4728 .unwrap();
4729 let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
4730 let read_options = blob.read_options();
4731 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4732 let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4733 .await
4734 .unwrap();
4735
4736 let data = vec![7; PAGE_SIZE.get() as usize];
4737 writer.append(&data).await.unwrap();
4738 writer.sync().await.unwrap();
4739
4740 writer.resize(50).await.unwrap();
4743
4744 assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE]);
4745 });
4746 }
4747
4748 #[test]
4749 fn test_resize_shrink_validates_crc() {
4750 let executor = deterministic::Runner::default();
4753
4754 executor.start(|context| async move {
4755 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4756 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4757
4758 let (blob, size) = context
4759 .open("test_partition", b"resize_crc_test")
4760 .await
4761 .unwrap();
4762
4763 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4764 .await
4765 .unwrap();
4766
4767 let data: Vec<u8> = (0..=249).collect();
4770 append.append(&data).await.unwrap();
4771 append.sync().await.unwrap();
4772 assert_eq!(append.size(), 250);
4773 drop(append);
4774
4775 let (blob, size) = context
4777 .open("test_partition", b"resize_crc_test")
4778 .await
4779 .unwrap();
4780 assert_eq!(size as usize, physical_page_size * 3);
4781
4782 let page1_crc_offset = (physical_page_size * 2 - CHECKSUM_SIZE as usize) as u64;
4784 blob.write_at(
4785 page1_crc_offset,
4786 vec![0xFF; CHECKSUM_SIZE as usize],
4787 WriteOptions::default(),
4788 )
4789 .await
4790 .unwrap();
4791 blob.sync().await.unwrap();
4792
4793 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4796 .await
4797 .unwrap();
4798 assert_eq!(append.size(), 250);
4799
4800 let result = append.resize(150).await;
4804 assert!(
4805 matches!(result, Err(crate::Error::InvalidChecksum)),
4806 "Expected InvalidChecksum when shrinking to corrupted page, got: {:?}",
4807 result
4808 );
4809 });
4810 }
4811
4812 #[test]
4813 fn test_resize_invalidates_cache() {
4814 let executor = deterministic::Runner::default();
4819 executor.start(|context: deterministic::Context| async move {
4820 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4821 let (blob, blob_size) = context
4822 .open("test_partition", b"resize_invalidates_cache")
4823 .await
4824 .unwrap();
4825 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4826 .await
4827 .unwrap();
4828
4829 let page_size = PAGE_SIZE.get() as usize;
4832 let old_bytes = vec![0xAAu8; page_size];
4833 append.append(&old_bytes).await.unwrap();
4834 append.sync().await.unwrap();
4835
4836 let mut probe = vec![0u8; 16];
4838 assert!(append.try_read_sync_into(&mut probe, 0));
4839 assert_eq!(probe, vec![0xAAu8; 16]);
4840
4841 append.resize(0).await.unwrap();
4843 let new_bytes = vec![0xBBu8; 16];
4844 append.append(&new_bytes).await.unwrap();
4845
4846 let mut probe = vec![0u8; 16];
4849 let hit = append.try_read_sync_into(&mut probe, 0);
4850 assert!(
4851 !hit || probe == new_bytes,
4852 "try_read_sync_into served stale pre-resize bytes: {probe:?}"
4853 );
4854 });
4855 }
4856
4857 #[test]
4858 fn test_snapshot_fetch_cannot_repopulate_live_cache_after_resize() {
4859 let executor = deterministic::Runner::default();
4860 executor.start(|context: deterministic::Context| async move {
4861 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4862 let page_size = PAGE_SIZE.get() as usize;
4863 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
4864 let (inner, blob_size) = context
4865 .open("test_partition", b"snapshot_resize_cache")
4866 .await
4867 .unwrap();
4868 let (started_tx, started_rx) = oneshot::channel();
4869 let (release_tx, release_rx) = oneshot::channel();
4870 let blob = DelayedReadBlob::new(
4871 inner,
4872 physical_page_size as u64,
4873 physical_page_size,
4874 started_tx,
4875 release_rx,
4876 );
4877 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4878 .await
4879 .unwrap();
4880
4881 let old_page0 = vec![0x11u8; page_size];
4882 let old_page1 = vec![0x22u8; page_size];
4883 writer.append(&old_page0).await.unwrap();
4884 writer.append(&old_page1).await.unwrap();
4885 writer.sync().await.unwrap();
4886
4887 writer.cache_ref.invalidate_from(writer.id, 1);
4888
4889 let snapshot = writer.snapshot().await.unwrap();
4890 let snapshot_task = context
4891 .child("snapshot")
4892 .spawn(move |_| async move { snapshot.read_at(page_size as u64, page_size).await });
4893 started_rx.await.expect("snapshot read never started");
4894
4895 writer.resize(page_size as u64).await.unwrap();
4896 let new_page1 = vec![0x33u8; page_size];
4897 writer.append(&new_page1).await.unwrap();
4898 writer.sync().await.unwrap();
4899
4900 let _ = release_tx.send(());
4901 let stale = snapshot_task
4902 .await
4903 .expect("snapshot task failed")
4904 .expect("snapshot read failed")
4905 .coalesce();
4906 assert_eq!(stale.as_ref(), old_page1.as_slice());
4907
4908 let mut probe = vec![0u8; page_size];
4909 assert!(writer.try_read_sync_into(&mut probe, page_size as u64));
4910 assert_eq!(probe, new_page1);
4911 });
4912 }
4913
4914 #[test]
4915 fn test_resize_shrink_allowed_while_snapshot_alive() {
4916 let executor = deterministic::Runner::default();
4917 executor.start(|context: deterministic::Context| async move {
4918 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4919 let (blob, blob_size) = context
4920 .open("test_partition", b"snapshot_blocks_shrink")
4921 .await
4922 .unwrap();
4923 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4924 .await
4925 .unwrap();
4926
4927 let page_size = PAGE_SIZE.get() as usize;
4928 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2 + 7).collect();
4929 append.append(&data).await.unwrap();
4930 append.sync().await.unwrap();
4931
4932 let snapshot = append.snapshot().await.unwrap();
4933 let snapshot_clone = snapshot.clone();
4934 let snapshot_size = snapshot.size();
4935
4936 let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
4937 assert_eq!(read.as_ref(), data.as_slice());
4938
4939 append.resize(snapshot_size + 3).await.unwrap();
4941 assert_eq!(append.size(), snapshot_size + 3);
4942
4943 append.resize(snapshot_size - 1).await.unwrap();
4946 assert_eq!(append.size(), snapshot_size - 1);
4947 assert_eq!(snapshot_clone.size(), snapshot_size);
4948 });
4949 }
4950
4951 #[test]
4952 fn test_snapshot_read_many_into_matches_read_at() {
4953 let executor = deterministic::Runner::default();
4954 executor.start(|context: deterministic::Context| async move {
4955 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4956 let (blob, blob_size) = context
4957 .open("test_partition", b"snapshot_read_many")
4958 .await
4959 .unwrap();
4960 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4961 .await
4962 .unwrap();
4963
4964 let item_size = 4;
4965 let page_size = PAGE_SIZE.get() as usize;
4966 let total = page_size * 3 + 13;
4967 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
4968 append.append(&data).await.unwrap();
4969 append.sync().await.unwrap();
4970
4971 let snapshot = append.snapshot().await.unwrap();
4972 let offsets = [
4973 0,
4974 (page_size - item_size) as u64,
4975 (page_size + 5) as u64,
4976 (page_size * 3 + 2) as u64,
4977 ];
4978 let mut batch = vec![0u8; offsets.len() * item_size];
4980 snapshot
4981 .read_many_into(&mut batch, &offsets, NZUsize!(item_size))
4982 .await
4983 .unwrap();
4984
4985 for (item, offset) in batch.chunks_exact(item_size).zip(offsets) {
4986 let single = snapshot
4987 .read_at(offset, item_size)
4988 .await
4989 .unwrap()
4990 .coalesce();
4991 assert_eq!(item, single.as_ref());
4992 assert_eq!(item, &data[offset as usize..offset as usize + item_size]);
4993 }
4994 });
4995 }
4996
4997 #[test]
4998 fn test_snapshot_try_read_sync_prefers_snapshot_tail() {
4999 let executor = deterministic::Runner::default();
5000 executor.start(|context: deterministic::Context| async move {
5001 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5002 let (blob, blob_size) = context
5003 .open("test_partition", b"snapshot_try_read_tail")
5004 .await
5005 .unwrap();
5006 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5007 .await
5008 .unwrap();
5009
5010 let page_size = PAGE_SIZE.get() as usize;
5011 append.append(&vec![0xAA; page_size]).await.unwrap();
5012 append.sync().await.unwrap();
5013
5014 let tail = b"oldtail";
5015 append.append(tail).await.unwrap();
5016 let snapshot = append.snapshot().await.unwrap();
5017
5018 let poison = vec![0xBB; page_size];
5019 assert_eq!(
5021 append.cache_ref.cache(append.id, &poison, page_size as u64),
5022 0
5023 );
5024
5025 let mut read = vec![0; tail.len()];
5026 assert!(snapshot.try_read_sync_into(&mut read, page_size as u64));
5027 assert_eq!(read.as_slice(), tail);
5028 });
5029 }
5030
5031 #[test]
5032 fn test_resize_same_size_is_noop() {
5033 let executor = deterministic::Runner::default();
5034 executor.start(|context: deterministic::Context| async move {
5035 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5036 let (blob, blob_size) = context
5037 .open("test_partition", b"resize_same_size")
5038 .await
5039 .unwrap();
5040 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5041 .await
5042 .unwrap();
5043
5044 append.append(b"hello world").await.unwrap();
5045 assert_eq!(append.size(), 11);
5046
5047 append.resize(11).await.unwrap();
5049 assert_eq!(append.size(), 11);
5050
5051 let read = append.read_at(0, 11).await.unwrap().coalesce();
5053 assert_eq!(read.as_ref(), b"hello world");
5054 });
5055 }
5056
5057 #[test]
5058 fn test_resize_same_page_shrink_reopens_at_shorter_size() {
5059 let executor = deterministic::Runner::default();
5060
5061 executor.start(|context| async move {
5062 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5063 let data: Vec<u8> = (0..50).collect();
5064
5065 let (blob, size) = context
5066 .open("test_partition", b"same_page_shrink")
5067 .await
5068 .unwrap();
5069 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5070 .await
5071 .unwrap();
5072
5073 append.append(&data).await.unwrap();
5076 append.sync().await.unwrap();
5077
5078 append.resize(45).await.unwrap();
5079 drop(append);
5080
5081 let (blob, size) = context
5082 .open("test_partition", b"same_page_shrink")
5083 .await
5084 .unwrap();
5085 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5086 .await
5087 .unwrap();
5088 assert_eq!(append.size(), 45);
5089 let read = append.read_at(0, 45).await.unwrap().coalesce();
5090 assert_eq!(read.as_ref(), &data[..45]);
5091 });
5092 }
5093
5094 #[test_traced("DEBUG")]
5098 fn test_shrink_then_torn_rewrite_does_not_resurrect() {
5099 let executor = deterministic::Runner::default();
5100 executor.start(|context: deterministic::Context| async move {
5101 let (blob, blob_size) = context
5102 .open("test_partition", b"shrink_torn")
5103 .await
5104 .unwrap();
5105 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5106 let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
5107 .await
5108 .unwrap();
5109
5110 let data: Vec<u8> = (1u8..=80).collect();
5112 writer.append(&data).await.unwrap();
5113 writer.sync().await.unwrap();
5114 writer.resize(50).await.unwrap();
5115 drop(writer);
5116
5117 let page_size = u64::from(PAGE_SIZE.get());
5120 blob.write_at(
5121 page_size,
5122 80u16.to_be_bytes().to_vec(),
5123 WriteOptions::default(),
5124 )
5125 .await
5126 .unwrap();
5127 blob.sync().await.unwrap();
5128
5129 let (blob, blob_size) = context
5130 .open("test_partition", b"shrink_torn")
5131 .await
5132 .unwrap();
5133 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5134 let recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5135 .await
5136 .unwrap();
5137 assert_eq!(recovered.size(), 50);
5138 let read = recovered.read_at(0, 50).await.unwrap().coalesce();
5139 assert_eq!(read.as_ref(), &data[..50]);
5140 });
5141 }
5142
5143 #[test]
5144 fn test_resize_same_page_shrink_survives_interrupted_crc_stage() {
5145 let executor = deterministic::Runner::default();
5146
5147 executor.start(|context| async move {
5148 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5149 let data: Vec<u8> = (0..50).collect();
5150
5151 let (blob, size) = context
5152 .open("test_partition", b"same_page_shrink_interrupted")
5153 .await
5154 .unwrap();
5155 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5156 .await
5157 .unwrap();
5158 append.append(&data[..40]).await.unwrap();
5159 append.sync().await.unwrap();
5160 append.append(&data[40..]).await.unwrap();
5161 append.sync().await.unwrap();
5162 drop(append);
5163
5164 let (blob, size) = context
5165 .open("test_partition", b"same_page_shrink_interrupted")
5166 .await
5167 .unwrap();
5168 let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
5169 let write_count = faulty_blob.write_count();
5170 let failed_write_len = faulty_blob.failed_write_len();
5171 let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5172 .await
5173 .unwrap();
5174
5175 assert!(
5176 append.resize(45).await.is_err(),
5177 "phase-1 partial write should fail"
5178 );
5179 assert_eq!(write_count.load(Ordering::SeqCst), 1);
5180 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
5181 drop(append);
5182
5183 let (blob, size) = context
5184 .open("test_partition", b"same_page_shrink_interrupted")
5185 .await
5186 .unwrap();
5187 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5188 .await
5189 .unwrap();
5190 assert_eq!(append.size(), 50);
5191 let read = append.read_at(0, 50).await.unwrap().coalesce();
5192 assert_eq!(read.as_ref(), &data);
5193 });
5194 }
5195
5196 #[test]
5197 fn test_resize_same_page_shrink_survives_interrupted_len_stage() {
5198 let executor = deterministic::Runner::default();
5199
5200 executor.start(|context| async move {
5201 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
5202 const LARGE_BUFFER_SIZE: usize = 1_200;
5203
5204 let cache_ref =
5205 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
5206 let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
5207
5208 let (blob, size) = context
5209 .open("test_partition", b"same_page_shrink_len_stage")
5210 .await
5211 .unwrap();
5212 let mut append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5213 .await
5214 .unwrap();
5215 append.append(&data[..255]).await.unwrap();
5216 append.sync().await.unwrap();
5217 append.append(&data[255..]).await.unwrap();
5218 append.sync().await.unwrap();
5219 drop(append);
5220
5221 let (blob, size) = context
5222 .open("test_partition", b"same_page_shrink_len_stage")
5223 .await
5224 .unwrap();
5225 let faulty_blob = PartialWriteBlob::new(blob, 2, 1);
5226 let write_count = faulty_blob.write_count();
5227 let failed_write_len = faulty_blob.failed_write_len();
5228 let mut append = Writer::new(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5229 .await
5230 .unwrap();
5231
5232 assert!(
5233 append.resize(257).await.is_err(),
5234 "length-stage partial write should fail"
5235 );
5236 assert_eq!(write_count.load(Ordering::SeqCst), 2);
5237 assert_eq!(failed_write_len.load(Ordering::SeqCst), 2);
5238 drop(append);
5239
5240 let (blob, size) = context
5241 .open("test_partition", b"same_page_shrink_len_stage")
5242 .await
5243 .unwrap();
5244 let append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref)
5245 .await
5246 .unwrap();
5247 assert_eq!(append.size(), 300);
5248 let read = append.read_at(0, 300).await.unwrap().coalesce();
5249 assert_eq!(read.as_ref(), &data);
5250 });
5251 }
5252
5253 #[test]
5254 fn test_resize_same_page_shrink_preserves_validated_fallback_slot() {
5255 let executor = deterministic::Runner::default();
5256
5257 executor.start(|context| async move {
5258 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5259 let data: Vec<u8> = (0..52).collect();
5260
5261 let (blob, size) = context
5262 .open("test_partition", b"same_page_shrink_fallback_slot")
5263 .await
5264 .unwrap();
5265 let faulty_blob = PartialWriteBlob::new(blob.clone(), 4, 3);
5266 let write_count = faulty_blob.write_count();
5267 let failed_write_len = faulty_blob.failed_write_len();
5268 let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5269 .await
5270 .unwrap();
5271 append.append(&data[..48]).await.unwrap();
5272 append.sync().await.unwrap();
5273 assert_eq!(write_count.load(Ordering::SeqCst), 1);
5274
5275 append.append(&data[48..50]).await.unwrap();
5276 append.sync().await.unwrap();
5277 assert_eq!(write_count.load(Ordering::SeqCst), 2);
5278
5279 append.append(&data[50..]).await.unwrap();
5280 append.sync().await.unwrap();
5281 assert_eq!(write_count.load(Ordering::SeqCst), 3);
5282
5283 let slot0_offset = PAGE_SIZE.get() as u64;
5288 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
5289 .await
5290 .unwrap();
5291 blob.sync().await.unwrap();
5292
5293 assert!(
5294 append.resize(45).await.is_err(),
5295 "phase-1 partial write should fail"
5296 );
5297 assert_eq!(write_count.load(Ordering::SeqCst), 4);
5298 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
5299 drop(append);
5300
5301 let (blob, size) = context
5302 .open("test_partition", b"same_page_shrink_fallback_slot")
5303 .await
5304 .unwrap();
5305 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5306 .await
5307 .unwrap();
5308 assert_eq!(append.size(), 50);
5309 let read = append.read_at(0, 50).await.unwrap().coalesce();
5310 assert_eq!(read.as_ref(), &data[..50]);
5311 });
5312 }
5313
5314 #[test]
5315 fn test_resize_full_page_to_partial_reopens_at_shorter_size() {
5316 let executor = deterministic::Runner::default();
5317
5318 executor.start(|context| async move {
5319 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5320 let page_size = PAGE_SIZE.get() as u64;
5321 let target = page_size + 45;
5322 let data: Vec<u8> = (0..page_size * 2).map(|i| (i % 251) as u8).collect();
5323
5324 let (blob, size) = context
5325 .open("test_partition", b"full_page_to_partial")
5326 .await
5327 .unwrap();
5328 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5329 .await
5330 .unwrap();
5331 append.append(&data).await.unwrap();
5332 append.sync().await.unwrap();
5333
5334 append.resize(target).await.unwrap();
5335 drop(append);
5336
5337 let (blob, size) = context
5338 .open("test_partition", b"full_page_to_partial")
5339 .await
5340 .unwrap();
5341 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5342 .await
5343 .unwrap();
5344 assert_eq!(append.size(), target);
5345 let read = append.read_at(0, target as usize).await.unwrap().coalesce();
5346 assert_eq!(read.as_ref(), &data[..target as usize]);
5347 });
5348 }
5349
5350 #[test]
5351 fn test_resize_full_page_to_partial_survives_interrupted_crc_stage() {
5352 let executor = deterministic::Runner::default();
5353
5354 executor.start(|context| async move {
5355 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5356 let page_size = PAGE_SIZE.get() as u64;
5357 let target = page_size + 45;
5358 let data: Vec<u8> = (0..page_size * 3).map(|i| (i % 251) as u8).collect();
5359
5360 let (blob, size) = context
5361 .open("test_partition", b"full_page_to_partial_interrupted")
5362 .await
5363 .unwrap();
5364 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5365 .await
5366 .unwrap();
5367 append.append(&data).await.unwrap();
5368 append.sync().await.unwrap();
5369 drop(append);
5370
5371 let (blob, size) = context
5372 .open("test_partition", b"full_page_to_partial_interrupted")
5373 .await
5374 .unwrap();
5375 let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
5376 let write_count = faulty_blob.write_count();
5377 let failed_write_len = faulty_blob.failed_write_len();
5378 let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
5379 .await
5380 .unwrap();
5381
5382 assert!(
5383 append.resize(target).await.is_err(),
5384 "phase-1 partial write should fail"
5385 );
5386 assert_eq!(write_count.load(Ordering::SeqCst), 1);
5387 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
5388 drop(append);
5389
5390 let (blob, size) = context
5391 .open("test_partition", b"full_page_to_partial_interrupted")
5392 .await
5393 .unwrap();
5394 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
5395 .await
5396 .unwrap();
5397 assert_eq!(append.size(), page_size * 2);
5398 let read = append
5399 .read_at(0, (page_size * 2) as usize)
5400 .await
5401 .unwrap()
5402 .coalesce();
5403 assert_eq!(read.as_ref(), &data[..(page_size * 2) as usize]);
5404 });
5405 }
5406
5407 #[test]
5408 fn test_resize_same_page_shrink_survives_interrupted_length_invalidation() {
5409 let executor = deterministic::Runner::default();
5410
5411 executor.start(|context| async move {
5412 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
5413 const LARGE_BUFFER_SIZE: usize = 1_200;
5414
5415 let cache_ref =
5416 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
5417 let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
5418
5419 let (blob, size) = context
5420 .open(
5421 "test_partition",
5422 b"same_page_shrink_interrupted_len_invalidation",
5423 )
5424 .await
5425 .unwrap();
5426 let mut append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5427 .await
5428 .unwrap();
5429 append.append(&data[..255]).await.unwrap();
5432 append.sync().await.unwrap();
5433 append.append(&data[255..]).await.unwrap();
5434 append.sync().await.unwrap();
5435 drop(append);
5436
5437 let (blob, size) = context
5438 .open(
5439 "test_partition",
5440 b"same_page_shrink_interrupted_len_invalidation",
5441 )
5442 .await
5443 .unwrap();
5444 let faulty_blob = PartialWriteBlob::new(blob, 3, 1);
5445 let write_count = faulty_blob.write_count();
5446 let failed_write_len = faulty_blob.failed_write_len();
5447 let mut append = Writer::new(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
5448 .await
5449 .unwrap();
5450
5451 assert!(
5452 append.resize(40).await.is_err(),
5453 "old-slot length invalidation should fail"
5454 );
5455 assert_eq!(write_count.load(Ordering::SeqCst), 3);
5456 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
5457 drop(append);
5458
5459 let (blob, size) = context
5460 .open(
5461 "test_partition",
5462 b"same_page_shrink_interrupted_len_invalidation",
5463 )
5464 .await
5465 .unwrap();
5466 let append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref)
5467 .await
5468 .unwrap();
5469 assert_eq!(append.size(), 40);
5470 let read = append.read_at(0, 40).await.unwrap().coalesce();
5471 assert_eq!(read.as_ref(), &data[..40]);
5472 });
5473 }
5474
5475 #[test_traced("DEBUG")]
5476 fn test_resize_partial_shrink_without_physical_resize_uses_range_sync() {
5477 let executor = deterministic::Runner::default();
5478 executor.start(|context: deterministic::Context| async move {
5479 let blob = SyncTrackingBlob::new();
5480 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5481 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
5482 .await
5483 .unwrap();
5484 append.sync().await.unwrap();
5485
5486 let data = vec![5u8; PAGE_SIZE.get() as usize];
5487 append.append(&data).await.unwrap();
5488 append.sync().await.unwrap();
5489
5490 append.resize(50).await.unwrap();
5492 append.sync().await.unwrap();
5493
5494 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
5495 assert_eq!(writes, 4);
5496 assert_eq!(full_syncs, 1);
5497 assert_eq!(range_syncs, 4);
5498 });
5499 }
5500
5501 #[test_traced("DEBUG")]
5502 fn test_resize_partial_shrink_with_physical_resize_clears_full_sync_requirement() {
5503 let executor = deterministic::Runner::default();
5504 executor.start(|context: deterministic::Context| async move {
5505 let blob = SyncTrackingBlob::new();
5506 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5507 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
5508 .await
5509 .unwrap();
5510 append.sync().await.unwrap();
5511
5512 let data = vec![9u8; PAGE_SIZE.get() as usize * 2];
5513 append.append(&data).await.unwrap();
5514 append.sync().await.unwrap();
5515
5516 append.resize(50).await.unwrap();
5519 append.sync().await.unwrap();
5520
5521 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
5522 assert_eq!(writes, 4);
5523 assert_eq!(full_syncs, 2);
5524 assert_eq!(range_syncs, 3);
5525
5526 append.append(b"x").await.unwrap();
5528 append.sync().await.unwrap();
5529
5530 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
5531 assert_eq!(writes, 5);
5532 assert_eq!(full_syncs, 2);
5533 assert_eq!(range_syncs, 4);
5534
5535 let mut expected = data[..50].to_vec();
5536 expected.push(b'x');
5537 let read = append.read_at(0, expected.len()).await.unwrap().coalesce();
5538 assert_eq!(read.as_ref(), expected.as_slice());
5539 });
5540 }
5541
5542 #[test_traced("DEBUG")]
5543 fn test_resize_page_boundary_shrink_uses_full_sync() {
5544 let executor = deterministic::Runner::default();
5545 executor.start(|context: deterministic::Context| async move {
5546 let blob = SyncTrackingBlob::new();
5547 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5548 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
5549 .await
5550 .unwrap();
5551 append.sync().await.unwrap();
5552
5553 let page_size = PAGE_SIZE.get() as usize;
5556 let data = vec![11u8; page_size * 2];
5557 append.append(&data).await.unwrap();
5558 append.sync().await.unwrap();
5559
5560 append.resize(PAGE_SIZE.get() as u64).await.unwrap();
5562 append.sync().await.unwrap();
5563
5564 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
5566 assert_eq!(writes, 1);
5567 assert_eq!(full_syncs, 2);
5568 assert_eq!(range_syncs, 1);
5569
5570 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5571 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
5572 .await
5573 .unwrap();
5574 assert_eq!(reopened.size(), PAGE_SIZE.get() as u64);
5575 let read = reopened.read_at(0, page_size).await.unwrap().coalesce();
5576 assert_eq!(read.as_ref(), &data[..page_size]);
5577 });
5578 }
5579
5580 #[test]
5581 fn test_reopen_partial_tail_append_and_resize() {
5582 let executor = deterministic::Runner::default();
5583
5584 executor.start(|context| async move {
5585 const PAGE_SIZE: NonZeroU16 = NZU16!(64);
5586 const BUFFER_SIZE: usize = 256;
5587
5588 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(4));
5589
5590 let (blob, size) = context
5591 .open("test_partition", b"partial_tail_test")
5592 .await
5593 .unwrap();
5594
5595 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5596 .await
5597 .unwrap();
5598
5599 append.append(&[1, 2, 3, 4, 5]).await.unwrap();
5601 append.sync().await.unwrap();
5602 assert_eq!(append.size(), 5);
5603 drop(append);
5604
5605 let (blob, size) = context
5606 .open("test_partition", b"partial_tail_test")
5607 .await
5608 .unwrap();
5609
5610 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5611 .await
5612 .unwrap();
5613 assert_eq!(append.size(), 5);
5614
5615 append.append(&[6, 7, 8]).await.unwrap();
5616 append.resize(6).await.unwrap();
5617 append.sync().await.unwrap();
5618
5619 let data: Vec<u8> = append.read_at(0, 6).await.unwrap().coalesce().into();
5620 assert_eq!(data, vec![1, 2, 3, 4, 5, 6]);
5621 });
5622 }
5623
5624 #[test]
5625 fn test_corrupted_crc_len_too_large() {
5626 let executor = deterministic::Runner::default();
5627
5628 executor.start(|context| async move {
5629 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5630 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
5631
5632 let (blob, size) = context
5634 .open("test_partition", b"crc_len_test")
5635 .await
5636 .unwrap();
5637
5638 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5639 .await
5640 .unwrap();
5641
5642 append.append(&[0x42; 50]).await.unwrap();
5643 append.sync().await.unwrap();
5644 drop(append);
5645
5646 let (blob, size) = context
5648 .open("test_partition", b"crc_len_test")
5649 .await
5650 .unwrap();
5651 assert_eq!(size as usize, physical_page_size);
5652
5653 let crc_offset = PAGE_SIZE.get() as u64;
5655
5656 let bad_crc_record: [u8; 12] = [
5659 0xFF, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
5664 blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
5665 .await
5666 .unwrap();
5667 blob.sync().await.unwrap();
5668
5669 let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
5671
5672 match result {
5675 Ok(append) => {
5676 let recovered_size = append.size();
5678 assert_eq!(
5679 recovered_size, 0,
5680 "Corrupted page should be truncated, size should be 0"
5681 );
5682 }
5683 Err(e) => {
5684 assert!(
5686 matches!(e, crate::Error::InvalidChecksum),
5687 "Expected InvalidChecksum error, got: {:?}",
5688 e
5689 );
5690 }
5691 }
5692 });
5693 }
5694
5695 #[test]
5696 fn test_corrupted_crc_both_slots_len_too_large() {
5697 let executor = deterministic::Runner::default();
5698
5699 executor.start(|context| async move {
5700 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5701
5702 let (blob, size) = context
5704 .open("test_partition", b"crc_both_bad")
5705 .await
5706 .unwrap();
5707
5708 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
5709 .await
5710 .unwrap();
5711
5712 append.append(&[0x42; 50]).await.unwrap();
5713 append.sync().await.unwrap();
5714 drop(append);
5715
5716 let (blob, size) = context
5718 .open("test_partition", b"crc_both_bad")
5719 .await
5720 .unwrap();
5721
5722 let crc_offset = PAGE_SIZE.get() as u64;
5723
5724 let bad_crc_record: [u8; 12] = [
5726 0x01, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x02, 0x00, 0xCA, 0xFE, 0xBA, 0xBE, ];
5731 blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
5732 .await
5733 .unwrap();
5734 blob.sync().await.unwrap();
5735
5736 let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
5738
5739 match result {
5740 Ok(append) => {
5741 assert_eq!(append.size(), 0);
5743 }
5744 Err(e) => {
5745 assert!(
5746 matches!(e, crate::Error::InvalidChecksum),
5747 "Expected InvalidChecksum, got: {:?}",
5748 e
5749 );
5750 }
5751 }
5752 });
5753 }
5754
5755 #[test_traced("DEBUG")]
5757 fn test_reader_sees_buffered_bytes() {
5758 let executor = deterministic::Runner::default();
5759 executor.start(|context: deterministic::Context| async move {
5760 let (blob, blob_size) = context.open("test_partition", b"rdr_buf").await.unwrap();
5761 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5762 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5763 .await
5764 .unwrap();
5765
5766 let data: Vec<u8> = (0u8..50).collect();
5767 writer.append(&data).await.unwrap();
5768
5769 assert_eq!(writer.size(), 50);
5771 let read = writer.read_at(0, 50).await.unwrap().coalesce();
5772 assert_eq!(read.as_ref(), data.as_slice());
5773
5774 let mut buf = vec![0u8; 10];
5775 assert!(writer.try_read_sync_into(&mut buf, 20));
5776 assert_eq!(buf, data[20..30]);
5777 });
5778 }
5779
5780 #[test_traced("DEBUG")]
5783 fn test_reader_read_past_resize_errors_cleanly() {
5784 let executor = deterministic::Runner::default();
5785 executor.start(|context: deterministic::Context| async move {
5786 let (blob, blob_size) = context.open("test_partition", b"rdr_rsz").await.unwrap();
5787 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
5788 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
5789 .await
5790 .unwrap();
5791
5792 let page_size = PAGE_SIZE.get() as usize;
5793 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
5794 writer.append(&data).await.unwrap();
5795 writer.sync().await.unwrap();
5796 assert_eq!(writer.size(), (page_size * 2) as u64);
5797
5798 let new_size = (page_size / 2) as u64;
5800 writer.resize(new_size).await.unwrap();
5801
5802 let err = writer
5804 .read_at(new_size, page_size)
5805 .await
5806 .expect_err("read past resized end must fail");
5807 assert!(matches!(err, crate::Error::BlobInsufficientLength));
5808
5809 let read = writer
5811 .read_at(0, new_size as usize)
5812 .await
5813 .unwrap()
5814 .coalesce();
5815 assert_eq!(read.as_ref(), &data[..new_size as usize]);
5816 });
5817 }
5818}