1use super::{
39 read::{PageReader, Replay},
40 view::View,
41};
42use crate::{
43 buffer::{
44 paged::{CacheRef, Checksum, Slot, CHECKSUM_SIZE, CHECKSUM_SLOT_SIZE},
45 tip::Buffer,
46 SyncState,
47 },
48 Blob, Error, Handle, IoBuf, IoBufMut, IoBufs,
49};
50use bytes::BufMut;
51use commonware_cryptography::Crc32;
52use std::num::{NonZeroU16, NonZeroUsize};
53use tracing::warn;
54
55fn adjusted_capacity(capacity: usize, page_size: u64) -> usize {
63 let page_size = page_size as usize;
64 let rounded = capacity.next_multiple_of(page_size);
65 let floor = page_size * 2;
66 if rounded < floor {
67 warn!(
68 floor,
69 "requested buffer capacity is too low, increasing it to floor"
70 );
71 }
72 rounded.max(floor)
73}
74
75const fn too_big_for_buffer(
83 buffer_len: usize,
84 buffer_capacity: usize,
85 append_len: usize,
86 logical_page_size: usize,
87) -> bool {
88 let fill = buffer_len.next_multiple_of(logical_page_size) - buffer_len;
89 let overflows_capacity = buffer_len + append_len > buffer_capacity;
90 let has_full_page_after_fill = append_len >= fill + logical_page_size;
91
92 overflows_capacity && has_full_page_after_fill
93}
94
95pub struct Writer<B: Blob> {
97 blob: B,
99
100 current_page: u64,
102
103 partial_page_state: Option<Checksum>,
106
107 sync_state: SyncState,
109
110 id: u64,
112
113 cache_ref: CacheRef,
115
116 buffer: Buffer,
119}
120
121impl<B: Blob> Writer<B> {
122 async fn write_at(&mut self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
124 self.sync_state.write_at(&self.blob, offset, bufs).await
125 }
126
127 async fn write_at_sync(
132 &mut self,
133 offset: u64,
134 bufs: impl Into<IoBufs> + Send,
135 ) -> Result<(), Error> {
136 self.sync_state
137 .write_at_sync(&self.blob, offset, bufs)
138 .await
139 }
140
141 async fn write_at_maybe_sync(
143 &mut self,
144 offset: u64,
145 bufs: impl Into<IoBufs> + Send,
146 sync: bool,
147 ) -> Result<(), Error> {
148 if sync {
149 self.write_at_sync(offset, bufs).await
150 } else {
151 self.write_at(offset, bufs).await
152 }
153 }
154
155 pub async fn new(
159 blob: B,
160 original_blob_size: u64,
161 capacity: usize,
162 cache_ref: CacheRef,
163 ) -> Result<Self, Error> {
164 let (partial_page_state, pages, invalid_data_found) =
165 Self::read_last_valid_page(&blob, original_blob_size, cache_ref.page_size()).await?;
166 if invalid_data_found {
167 let new_blob_size = pages * (cache_ref.page_size() + CHECKSUM_SIZE);
169 warn!(
170 original_blob_size,
171 new_blob_size, "truncating blob to remove invalid data"
172 );
173 blob.resize(new_blob_size).await?;
174 blob.sync().await?;
175 }
176
177 let capacity = adjusted_capacity(capacity, cache_ref.page_size());
178 let needs_sync = !invalid_data_found; let (current_page, partial_page_state, partial_data) = match partial_page_state {
181 Some((partial_page, crc_record)) => (pages - 1, Some(crc_record), Some(partial_page)),
182 None => (pages, None, None),
183 };
184
185 let buffer = Buffer::from(
186 current_page * cache_ref.page_size(),
187 partial_data.unwrap_or_default(),
188 capacity,
189 cache_ref.pool().clone(),
190 );
191
192 Ok(Self {
193 blob,
194 current_page,
195 partial_page_state,
196 sync_state: if needs_sync {
197 SyncState::Dirty
198 } else {
199 SyncState::Clean
200 },
201 id: cache_ref.next_id(),
202 cache_ref,
203 buffer,
204 })
205 }
206
207 async fn read_last_valid_page(
225 blob: &B,
226 blob_size: u64,
227 page_size: u64,
228 ) -> Result<(Option<(IoBuf, Checksum)>, u64, bool), Error> {
229 let physical_page_size = page_size + CHECKSUM_SIZE;
230 let partial_bytes = blob_size % physical_page_size;
231 let mut last_page_end = blob_size - partial_bytes;
232
233 let mut invalid_data_found = partial_bytes != 0;
236
237 while last_page_end != 0 {
238 let page_start = last_page_end - physical_page_size;
240 let buf = blob
241 .read_at(page_start, physical_page_size as usize)
242 .await?
243 .coalesce()
244 .freeze();
245
246 match Checksum::validate_page(buf.as_ref()) {
247 Some(crc_record) => {
248 let (len, _) = crc_record.get_crc();
250 let len = len as u64;
251 if len != page_size {
252 let logical_bytes = buf.slice(..len as usize);
254 return Ok((
255 Some((logical_bytes, crc_record)),
256 last_page_end / physical_page_size,
257 invalid_data_found,
258 ));
259 }
260 return Ok((None, last_page_end / physical_page_size, invalid_data_found));
262 }
263 None => {
264 last_page_end = page_start;
266 invalid_data_found = true;
267 }
268 }
269 }
270
271 Ok((None, 0, invalid_data_found))
273 }
274
275 pub async fn append(&mut self, buf: &[u8]) -> Result<u64, Error> {
278 let logical_page_size = self.cache_ref.page_size() as usize;
279
280 if too_big_for_buffer(
282 self.buffer.len(),
283 self.buffer.capacity,
284 buf.len(),
285 logical_page_size,
286 ) {
287 return self.append_owned(IoBuf::copy_from_slice(buf)).await;
288 }
289
290 let offset = self.buffer.size();
291 if self.buffer.append(buf) {
292 self.flush_internal(false, false).await?;
293 }
294 Ok(offset)
295 }
296
297 pub async fn append_owned(&mut self, buf: IoBuf) -> Result<u64, Error> {
303 let logical_page_size = self.cache_ref.page_size() as usize;
304 let offset = self.buffer.size();
305
306 if !too_big_for_buffer(
308 self.buffer.len(),
309 self.buffer.capacity,
310 buf.len(),
311 logical_page_size,
312 ) {
313 if self.buffer.append(buf.as_ref()) {
314 self.flush_internal(false, false).await?;
315 }
316 return Ok(offset);
317 }
318
319 let fill = self.buffer.len().next_multiple_of(logical_page_size) - self.buffer.len();
321
322 if fill > 0 {
325 self.buffer.append(&buf.as_ref()[..fill]);
326 }
327 let boundary = self.buffer.size();
328 if !self.buffer.is_empty() {
329 self.flush_internal(false, false).await?;
330 assert!(
331 self.buffer.size() == boundary && self.buffer.is_empty(),
332 "flush left unexpected buffered bytes before a direct-path append"
333 );
334 }
335
336 let bulk_len = (buf.len() - fill) / logical_page_size * logical_page_size;
339 let bulk = buf.slice(fill..fill + bulk_len);
340 let mut physical_pages = IoBufs::default();
341 self.append_full_pages(&bulk, None, &mut physical_pages);
342
343 assert!(
344 self.partial_page_state.is_none(),
345 "an empty tip implies no partial page state"
346 );
347
348 self.sync_state.wait_for_pending().await?;
350
351 let chunk_len = self.buffer.capacity;
356 let mut cache_offset = boundary;
357 for chunk in bulk.as_ref().chunks(chunk_len) {
358 let remaining = self.cache_ref.cache(self.id, chunk, cache_offset);
359 assert_eq!(remaining, 0, "cached bulk pages must be page-aligned");
360 cache_offset += chunk.len() as u64;
361 }
362
363 self.current_page += (bulk_len / logical_page_size) as u64;
368 let suffix = buf.slice(fill + bulk_len..);
369 let suffix = if suffix.is_empty() {
370 suffix
371 } else {
372 let mut copied = self.cache_ref.pool().alloc(suffix.len());
373 copied.put_slice(suffix.as_ref());
374 copied.freeze()
375 };
376 self.buffer.replace(boundary + bulk_len as u64, suffix);
377
378 assert_eq!(
380 self.current_page * self.cache_ref.page_size(),
381 self.buffer.offset
382 );
383
384 let physical_page_size = logical_page_size as u64 + CHECKSUM_SIZE;
385 let write_at_offset = boundary / logical_page_size as u64 * physical_page_size;
386 self.write_at(write_at_offset, physical_pages).await?;
387
388 Ok(offset)
389 }
390
391 async fn flush_internal(
404 &mut self,
405 write_partial_page: bool,
406 sync: bool,
407 ) -> Result<bool, Error> {
408 let (mut physical_pages, partial_page_state) = self.to_physical_pages(
411 &self.buffer,
412 write_partial_page,
413 self.partial_page_state.as_ref(),
414 );
415
416 if physical_pages.is_empty() {
418 return Ok(false);
419 }
420
421 self.sync_state.wait_for_pending().await?;
423
424 let logical_page_size = self.cache_ref.page_size() as usize;
427 let pages_to_cache = self.buffer.len() / logical_page_size;
428 let bytes_to_drain = pages_to_cache * logical_page_size;
429
430 let cache_pages = if pages_to_cache > 0 {
432 Some((self.buffer.offset, self.buffer.slice(..bytes_to_drain)))
433 } else {
434 None
435 };
436
437 if bytes_to_drain == self.buffer.len() && bytes_to_drain != 0 {
440 let _ = self
441 .buffer
442 .take()
443 .expect("take must succeed when flush drains all buffered bytes");
444 } else if bytes_to_drain != 0 {
445 self.buffer.drop_prefix(bytes_to_drain);
446 self.buffer.offset += bytes_to_drain as u64;
447 }
448 let new_offset = self.buffer.offset;
449
450 if let Some((cache_offset, pages)) = cache_pages {
453 let remaining = self.cache_ref.cache(self.id, pages.as_ref(), cache_offset);
454 assert_eq!(remaining, 0, "cached full-page prefix must be page-aligned");
455 }
456
457 let physical_page_size = logical_page_size + CHECKSUM_SIZE as usize;
458 let write_at_offset = self.current_page * physical_page_size as u64;
459
460 let protected_regions = Self::identify_protected_regions(self.partial_page_state.as_ref());
462
463 self.current_page += pages_to_cache as u64;
467 self.partial_page_state = partial_page_state;
468
469 assert_eq!(self.current_page * self.cache_ref.page_size(), new_offset);
471
472 match protected_regions {
475 Some((prefix_len, Slot::First)) => {
476 let has_first_write = prefix_len < logical_page_size;
483 if has_first_write {
484 let _ = physical_pages.split_to(prefix_len);
485 let first_payload = physical_pages.split_to(logical_page_size - prefix_len);
486 let has_second_write = physical_pages.len() > CHECKSUM_SLOT_SIZE;
487 self.write_at_maybe_sync(
488 write_at_offset + prefix_len as u64,
489 first_payload,
490 sync && !has_second_write,
491 )
492 .await?;
493 if !has_second_write {
494 return Ok(sync);
495 }
496 } else {
497 let _ = physical_pages.split_to(logical_page_size);
499 }
500
501 if physical_pages.len() > CHECKSUM_SLOT_SIZE {
503 let _ = physical_pages.split_to(CHECKSUM_SLOT_SIZE);
504 self.write_at_maybe_sync(
505 write_at_offset + (logical_page_size + CHECKSUM_SLOT_SIZE) as u64,
506 physical_pages,
507 sync && !has_first_write,
508 )
509 .await?;
510 if !has_first_write {
511 return Ok(sync);
512 }
513 }
514
515 Ok(false)
516 }
517 Some((prefix_len, Slot::Second)) => {
518 let first_crc_end = logical_page_size + CHECKSUM_SLOT_SIZE;
525 let skip = physical_page_size - first_crc_end;
526 let has_first_write = prefix_len < first_crc_end;
527 if has_first_write {
528 let _ = physical_pages.split_to(prefix_len);
529 let first_payload = physical_pages.split_to(first_crc_end - prefix_len);
530 let has_second_write = physical_pages.len() > skip;
531 self.write_at_maybe_sync(
532 write_at_offset + prefix_len as u64,
533 first_payload,
534 sync && !has_second_write,
535 )
536 .await?;
537 if !has_second_write {
538 return Ok(sync);
539 }
540 } else {
541 let _ = physical_pages.split_to(first_crc_end);
543 }
544
545 if physical_pages.len() > skip {
547 let _ = physical_pages.split_to(skip);
548 self.write_at_maybe_sync(
549 write_at_offset + physical_page_size as u64,
550 physical_pages,
551 sync && !has_first_write,
552 )
553 .await?;
554 if !has_first_write {
555 return Ok(sync);
556 }
557 }
558
559 Ok(false)
560 }
561 None => {
562 self.write_at_maybe_sync(write_at_offset, physical_pages, sync)
564 .await?;
565 Ok(sync)
566 }
567 }
568 }
569
570 pub const fn size(&self) -> u64 {
572 self.buffer.size()
573 }
574
575 fn view(&self) -> View<'_, B> {
577 View {
578 blob: &self.blob,
579 cache_ref: &self.cache_ref,
580 id: self.id,
581 size: self.buffer.size(),
582 tail_offset: self.buffer.offset,
583 tail: self.buffer.as_ref(),
584 }
585 }
586
587 pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
591 self.view().try_read_sync_into(buf, offset)
592 }
593
594 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
596 self.view().read_at(offset, len).await
597 }
598
599 pub async fn read_up_to(
604 &self,
605 offset: u64,
606 len: usize,
607 bufs: impl Into<IoBufMut> + Send,
608 ) -> Result<(IoBufMut, usize), Error> {
609 self.view().read_up_to(offset, len, bufs).await
610 }
611
612 pub async fn read_many_into(
620 &self,
621 buf: &mut [u8],
622 offsets: &[u64],
623 item_size: NonZeroUsize,
624 ) -> Result<usize, Error> {
625 self.view().read_many_into(buf, offsets, item_size).await
626 }
627
628 pub fn try_read_many_sync_into(
631 &self,
632 buf: &mut [u8],
633 offsets: &[u64],
634 item_size: NonZeroUsize,
635 ) -> Vec<usize> {
636 self.view().try_read_many_sync_into(buf, offsets, item_size)
637 }
638
639 pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
642 self.view().try_read_ranges_sync_into(buf, ranges)
643 }
644
645 pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
647 self.view().read_into(buf, offset).await
648 }
649
650 fn identify_protected_regions(partial_page_state: Option<&Checksum>) -> Option<(usize, Slot)> {
661 let crc_record = partial_page_state?;
662 let (old_len, _) = crc_record.get_crc();
663 Some((old_len as usize, crc_record.authoritative()))
665 }
666
667 fn to_physical_pages(
680 &self,
681 buffer: &Buffer,
682 include_partial_page: bool,
683 old_crc_record: Option<&Checksum>,
684 ) -> (IoBufs, Option<Checksum>) {
685 let logical_page_size = self.cache_ref.page_size() as usize;
686 let physical_page_size = logical_page_size + CHECKSUM_SIZE as usize;
687 let pages_to_write = buffer.len() / logical_page_size;
688 let mut write_buffer = IoBufs::default();
689 let buffer_data = buffer.as_ref();
690
691 if pages_to_write > 0 {
692 self.append_full_pages(
693 &buffer.slice(..pages_to_write * logical_page_size),
694 old_crc_record,
695 &mut write_buffer,
696 );
697 }
698
699 if !include_partial_page {
700 return (write_buffer, None);
701 }
702
703 let partial_page = &buffer_data[pages_to_write * logical_page_size..];
704 if partial_page.is_empty() {
705 return (write_buffer, None);
707 }
708
709 if pages_to_write == 0 {
712 if let Some(old_crc) = old_crc_record {
713 let (old_len, _) = old_crc.get_crc();
714 if partial_page.len() == old_len as usize {
715 return (write_buffer, None);
716 }
717 }
718 }
719 let partial_len = partial_page.len();
720 let crc = Crc32::checksum(partial_page);
721
722 let crc_record = if let (0, Some(old_crc)) = (pages_to_write, old_crc_record) {
725 Self::build_crc_record_preserving_old(partial_len as u16, crc, old_crc)
726 } else {
727 Checksum::new(partial_len as u16, crc)
728 };
729
730 let mut padded = self.cache_ref.pool().alloc(physical_page_size);
733 padded.put_slice(partial_page);
734 let zero_count = logical_page_size - partial_len;
735 if zero_count > 0 {
736 padded.put_bytes(0, zero_count);
737 }
738 padded.put_slice(&crc_record.to_bytes());
739 write_buffer.append(padded.freeze());
740
741 (write_buffer, Some(crc_record))
744 }
745
746 fn append_full_pages(
752 &self,
753 data: &IoBuf,
754 old_crc_record: Option<&Checksum>,
755 write_buffer: &mut IoBufs,
756 ) {
757 let logical_page_size = self.cache_ref.page_size() as usize;
758 let pages = data.len() / logical_page_size;
759 debug_assert!(pages > 0);
760 debug_assert_eq!(data.len() % logical_page_size, 0);
761 let logical_page_size_u16 =
762 u16::try_from(logical_page_size).expect("page size must fit in u16 for CRC record");
763
764 let mut crcs = self.cache_ref.pool().alloc(CHECKSUM_SIZE as usize * pages);
767 let data_bytes = data.as_ref();
768 for page in 0..pages {
769 let start_read_idx = page * logical_page_size;
770 let end_read_idx = start_read_idx + logical_page_size;
771 let logical_page = &data_bytes[start_read_idx..end_read_idx];
772 let crc = Crc32::checksum(logical_page);
773
774 let crc_record = if let (0, Some(old_crc)) = (page, old_crc_record) {
777 Self::build_crc_record_preserving_old(logical_page_size_u16, crc, old_crc)
778 } else {
779 Checksum::new(logical_page_size_u16, crc)
780 };
781 crcs.put_slice(&crc_record.to_bytes());
782 }
783 let crc_blob = crcs.freeze();
784
785 for page in 0..pages {
787 let start_read_idx = page * logical_page_size;
788 let end_read_idx = start_read_idx + logical_page_size;
789 write_buffer.append(data.slice(start_read_idx..end_read_idx));
790
791 let crc_start = page * CHECKSUM_SIZE as usize;
792 write_buffer.append(crc_blob.slice(crc_start..crc_start + CHECKSUM_SIZE as usize));
793 }
794 }
795
796 const fn build_crc_record_preserving_old(
802 new_len: u16,
803 new_crc: u32,
804 old_crc: &Checksum,
805 ) -> Checksum {
806 let (old_len, old_crc_val) = old_crc.get_crc();
807 match old_crc.authoritative() {
809 Slot::First => Checksum {
810 len1: old_len,
811 crc1: old_crc_val,
812 len2: new_len,
813 crc2: new_crc,
814 },
815 Slot::Second => Checksum {
816 len1: new_len,
817 crc1: new_crc,
818 len2: old_len,
819 crc2: old_crc_val,
820 },
821 }
822 }
823
824 async fn sync_partial_page_shrink(
826 &mut self,
827 page: u64,
828 logical_page_size: u64,
829 new_len: u16,
830 new_crc: u32,
831 old_crc: &Checksum,
832 ) -> Result<Checksum, Error> {
833 let physical_page_size = logical_page_size
838 .checked_add(CHECKSUM_SIZE)
839 .ok_or(Error::OffsetOverflow)?;
840 let crc_start = page
841 .checked_mul(physical_page_size)
842 .and_then(|start| start.checked_add(logical_page_size))
843 .ok_or(Error::OffsetOverflow)?;
844 let old_slot = old_crc.authoritative();
845 let new_slot = old_slot.other();
846
847 let new_slot_offset = crc_start
850 .checked_add(new_slot.offset() as u64)
851 .ok_or(Error::OffsetOverflow)?;
852 let staged_slot = Checksum::slot_bytes(0, new_crc);
853 self.write_at_sync(new_slot_offset, staged_slot.to_vec())
854 .await?;
855
856 let published_len = Checksum::slot_len_bytes(new_len);
859 self.write_at_sync(new_slot_offset, published_len.to_vec())
860 .await?;
861
862 let old_slot_offset = crc_start
866 .checked_add(old_slot.offset() as u64)
867 .ok_or(Error::OffsetOverflow)?;
868 self.write_at_sync(old_slot_offset, Checksum::slot_len_bytes(0).to_vec())
869 .await?;
870
871 Ok(Checksum::in_slot(new_slot, new_len, new_crc))
872 }
873
874 pub async fn replay(&mut self, buffer_size: NonZeroUsize) -> Result<Replay<B>, Error> {
882 let logical_page_size = self.cache_ref.page_size();
883 let logical_page_size_nz =
884 NonZeroU16::new(logical_page_size as u16).expect("page_size is non-zero");
885
886 self.flush_internal(true, false).await?;
888
889 let physical_page_size = logical_page_size + CHECKSUM_SIZE;
891 let prefetch_pages = buffer_size.get() / physical_page_size as usize;
892 let prefetch_pages = prefetch_pages.max(1); let (physical_blob_size, logical_blob_size) = self.partial_page_state.as_ref().map_or_else(
896 || {
897 let physical = physical_page_size * self.current_page;
899 let logical = logical_page_size * self.current_page;
900 (physical, logical)
901 },
902 |crc_record| {
903 let (partial_len, _) = crc_record.get_crc();
905 let partial_len = partial_len as u64;
906 let physical = physical_page_size * (self.current_page + 1);
908 let logical = logical_page_size * self.current_page + partial_len;
910 (physical, logical)
911 },
912 );
913
914 let reader = PageReader::new(
915 self.blob.clone(),
916 physical_blob_size,
917 logical_blob_size,
918 prefetch_pages,
919 logical_page_size_nz,
920 );
921 Ok(Replay::new(reader))
922 }
923
924 pub async fn snapshot(&mut self) -> Result<super::Sealed<B>, Error> {
934 self.flush_internal(true, false).await?;
935 Ok(self.sealed_handle(self.cache_ref.next_id()))
936 }
937
938 pub async fn sync(&mut self) -> Result<(), Error> {
944 if self.flush_internal(true, true).await? {
947 return Ok(());
948 }
949
950 self.sync_state.sync(&self.blob).await
953 }
954
955 pub async fn start_sync(&mut self) -> Handle<()> {
962 if let Err(err) = self.flush_internal(true, false).await {
963 return Handle::ready(Err(err));
964 }
965 self.sync_state.start_sync(&self.blob).await
966 }
967
968 pub async fn wait_for_sync(&mut self) -> Result<(), Error> {
970 self.sync_state.wait_for_pending().await
971 }
972
973 pub async fn resize(&mut self, size: u64) -> Result<(), Error> {
984 let current_size = self.buffer.size();
985 if size == current_size {
986 return Ok(());
987 }
988
989 if size > current_size {
991 let zeros_needed = (size - current_size) as usize;
992 let mut zeros = self.cache_ref.pool().alloc(zeros_needed);
993 zeros.put_bytes(0, zeros_needed);
994 self.append_owned(zeros.freeze()).await?;
995 return Ok(());
996 }
997
998 self.shrink(size).await
999 }
1000
1001 async fn shrink(&mut self, target_size: u64) -> Result<(), Error> {
1003 let logical_page_size = self.cache_ref.page_size();
1004 let physical_page_size = logical_page_size
1005 .checked_add(CHECKSUM_SIZE)
1006 .ok_or(Error::OffsetOverflow)?;
1007
1008 self.sync().await?;
1010
1011 let full_pages = target_size / logical_page_size;
1013 let partial_bytes = target_size % logical_page_size;
1014 let physical_pages = full_pages
1015 .checked_add(u64::from(partial_bytes > 0))
1016 .ok_or(Error::OffsetOverflow)?;
1017 let new_physical_size = physical_pages
1018 .checked_mul(physical_page_size)
1019 .ok_or(Error::OffsetOverflow)?;
1020 let tail_offset = full_pages
1021 .checked_mul(logical_page_size)
1022 .ok_or(Error::OffsetOverflow)?;
1023 let current_physical_size = if self.partial_page_state.is_some() {
1024 self.current_page
1025 .checked_add(1)
1026 .and_then(|pages| pages.checked_mul(physical_page_size))
1027 .ok_or(Error::OffsetOverflow)?
1028 } else {
1029 self.current_page
1030 .checked_mul(physical_page_size)
1031 .ok_or(Error::OffsetOverflow)?
1032 };
1033
1034 if new_physical_size != current_physical_size {
1037 self.sync_state
1038 .resize(&self.blob, new_physical_size)
1039 .await?;
1040 }
1041
1042 self.cache_ref.invalidate_from(self.id, full_pages);
1049
1050 if partial_bytes > 0 {
1051 return self
1052 .shrink_to_partial(full_pages, partial_bytes, logical_page_size, tail_offset)
1053 .await;
1054 }
1055
1056 self.partial_page_state = None;
1058 self.current_page = full_pages;
1059 self.buffer.offset = tail_offset;
1060 self.buffer.clear();
1061
1062 Ok(())
1063 }
1064
1065 async fn shrink_to_partial(
1067 &mut self,
1068 full_pages: u64,
1069 partial_bytes: u64,
1070 logical_page_size: u64,
1071 tail_offset: u64,
1072 ) -> Result<(), Error> {
1073 self.current_page = full_pages;
1076 self.buffer.offset = tail_offset;
1077
1078 let (page_data, old_crc) =
1079 super::get_page_with_checksum_from_blob(&self.blob, full_pages, logical_page_size)
1080 .await?;
1081
1082 if (page_data.len() as u64) < partial_bytes {
1084 return Err(Error::InvalidChecksum);
1085 }
1086
1087 self.buffer.clear();
1088 let new_data = &page_data.as_ref()[..partial_bytes as usize];
1089 let over_capacity = self.buffer.append(new_data);
1090 assert!(!over_capacity);
1091
1092 let final_record = self
1093 .sync_partial_page_shrink(
1094 full_pages,
1095 logical_page_size,
1096 partial_bytes as u16,
1097 Crc32::checksum(new_data),
1098 &old_crc,
1099 )
1100 .await?;
1101 self.partial_page_state = Some(final_record);
1102
1103 Ok(())
1104 }
1105
1106 #[cfg(test)]
1108 pub(super) const fn cache_id(&self) -> u64 {
1109 self.id
1110 }
1111
1112 fn sealed_handle(&self, id: u64) -> super::Sealed<B> {
1114 let logical_page_size = self.cache_ref.page_size();
1115 let full_pages = self.current_page;
1116 assert_eq!(
1117 full_pages.checked_mul(logical_page_size),
1118 Some(self.buffer.offset),
1119 "flushed page count is inconsistent with the buffer offset"
1120 );
1121 let partial_page = if self.buffer.is_empty() {
1122 None
1123 } else {
1124 Some(self.buffer.slice(..))
1125 };
1126 super::Sealed::new(
1127 self.blob.clone(),
1128 self.buffer.size(),
1129 partial_page,
1130 self.cache_ref.clone(),
1131 id,
1132 )
1133 }
1134
1135 pub async fn seal(mut self) -> Result<super::Sealed<B>, Error> {
1142 self.sync_state.wait_for_pending().await?;
1143 self.flush_internal(true, false).await?;
1144 Ok(self.sealed_handle(self.id))
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::*;
1151 use crate::{
1152 buffer::{paged::CHECKSUM_SLOT_LEN_SIZE, tests::SyncTrackingBlob},
1153 deterministic,
1154 mocks::{next_pending_sync, DelayedSyncBlob},
1155 telemetry::metrics::Registry,
1156 Buf, BufferPool, BufferPoolConfig, Handle, IoBufsMut, Runner as _, Spawner as _,
1157 Storage as _, Supervisor as _,
1158 };
1159 use commonware_codec::ReadExt;
1160 use commonware_macros::test_traced;
1161 use commonware_utils::{channel::oneshot, sync::Mutex, NZUsize, NZU16, NZU32};
1162 use futures::FutureExt as _;
1163 use std::{
1164 num::NonZeroU16,
1165 sync::{
1166 atomic::{AtomicUsize, Ordering},
1167 Arc,
1168 },
1169 };
1170
1171 const PAGE_SIZE: NonZeroU16 = NZU16!(103); const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
1173
1174 #[test_traced("DEBUG")]
1175 fn test_read_many_into_empty() {
1176 let executor = deterministic::Runner::default();
1177 executor.start(|context: deterministic::Context| async move {
1178 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1179 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1180 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1181 .await
1182 .unwrap();
1183
1184 append.append(&[0u8; 8]).await.unwrap();
1185 assert_eq!(append.size(), 8);
1186
1187 let mut buf = [];
1189 append
1190 .read_many_into(&mut buf, &[], NZUsize!(4))
1191 .await
1192 .unwrap();
1193 });
1194 }
1195
1196 #[test_traced("DEBUG")]
1197 fn test_read_many_into_all_in_tip() {
1198 let executor = deterministic::Runner::default();
1200 executor.start(|context: deterministic::Context| async move {
1201 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1202 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1203 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1204 .await
1205 .unwrap();
1206
1207 let data: Vec<u8> = (0..20).collect();
1208 append.append(&data).await.unwrap();
1209 assert_eq!(append.size(), 20);
1210
1211 let offsets = [0u64, 4, 8, 12, 16];
1213 let mut buf = vec![0u8; 5 * 4];
1214 append
1215 .read_many_into(&mut buf, &offsets, NZUsize!(4))
1216 .await
1217 .unwrap();
1218
1219 for (i, &off) in offsets.iter().enumerate() {
1220 assert_eq!(
1221 &buf[i * 4..(i + 1) * 4],
1222 &data[off as usize..off as usize + 4],
1223 );
1224 }
1225 });
1226 }
1227
1228 #[test_traced("DEBUG")]
1229 fn test_try_read_sync_all_in_tip() {
1230 let executor = deterministic::Runner::default();
1231 executor.start(|context: deterministic::Context| async move {
1232 let (blob, blob_size) = context
1233 .open("test_partition", b"try_read_sync_tip")
1234 .await
1235 .unwrap();
1236 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1237 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1238 .await
1239 .unwrap();
1240
1241 let data: Vec<u8> = (0..20).collect();
1242 append.append(&data).await.unwrap();
1243
1244 let mut buf = vec![0u8; data.len()];
1245 assert!(append.try_read_sync_into(&mut buf, 0));
1246 assert_eq!(buf, data);
1247 });
1248 }
1249
1250 #[test_traced("DEBUG")]
1251 fn test_try_read_sync_cache_miss() {
1252 let executor = deterministic::Runner::default();
1253 executor.start(|context: deterministic::Context| async move {
1254 let (blob, blob_size) = context
1255 .open("test_partition", b"try_read_sync_cache_miss")
1256 .await
1257 .unwrap();
1258 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
1260 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1261 .await
1262 .unwrap();
1263
1264 let page_size = PAGE_SIZE.get() as usize;
1265 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
1266 append.append(&data).await.unwrap();
1267 append.sync().await.unwrap();
1268
1269 let _ = append.read_at(0, page_size).await.unwrap();
1270
1271 let mut buf = vec![0xAA; 4];
1273 assert!(!append.try_read_sync_into(&mut buf, (page_size - 2) as u64));
1274 });
1275 }
1276
1277 #[test_traced("DEBUG")]
1278 fn test_read_many_into_all_from_cache() {
1279 let executor = deterministic::Runner::default();
1281 executor.start(|context: deterministic::Context| async move {
1282 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1283 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1284 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1285 .await
1286 .unwrap();
1287
1288 let data: Vec<u8> = (0..20).collect();
1289 append.append(&data).await.unwrap();
1290 append.sync().await.unwrap();
1291 assert_eq!(append.size(), 20);
1292
1293 let offsets = [0u64, 8, 16];
1294 let mut buf = vec![0u8; 3 * 4];
1295 append
1296 .read_many_into(&mut buf, &offsets, NZUsize!(4))
1297 .await
1298 .unwrap();
1299
1300 for (i, &off) in offsets.iter().enumerate() {
1301 assert_eq!(
1302 &buf[i * 4..(i + 1) * 4],
1303 &data[off as usize..off as usize + 4],
1304 );
1305 }
1306 });
1307 }
1308
1309 #[test_traced("DEBUG")]
1310 fn test_read_many_into_mixed_tip_and_cache() {
1311 let executor = deterministic::Runner::default();
1313 executor.start(|context: deterministic::Context| async move {
1314 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1315 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1316 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1317 .await
1318 .unwrap();
1319
1320 let first: Vec<u8> = (0..16).collect();
1321 append.append(&first).await.unwrap();
1322 append.sync().await.unwrap();
1323
1324 let second: Vec<u8> = (16..32).collect();
1325 append.append(&second).await.unwrap();
1326 assert_eq!(append.size(), 32);
1327
1328 let offsets = [0u64, 4, 16, 24];
1330 let mut buf = vec![0u8; 4 * 4];
1331 append
1332 .read_many_into(&mut buf, &offsets, NZUsize!(4))
1333 .await
1334 .unwrap();
1335
1336 let all: Vec<u8> = (0..32).collect();
1337 for (i, &off) in offsets.iter().enumerate() {
1338 assert_eq!(
1339 &buf[i * 4..(i + 1) * 4],
1340 &all[off as usize..off as usize + 4],
1341 );
1342 }
1343 });
1344 }
1345
1346 #[test_traced("DEBUG")]
1347 fn test_read_many_into_out_of_bounds() {
1348 let executor = deterministic::Runner::default();
1349 executor.start(|context: deterministic::Context| async move {
1350 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1351 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1352 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1353 .await
1354 .unwrap();
1355
1356 append.append(&[0u8; 8]).await.unwrap();
1357 assert_eq!(append.size(), 8);
1358
1359 let mut buf = vec![0u8; 4];
1361 let err = append
1362 .read_many_into(&mut buf, &[8], NZUsize!(4))
1363 .await
1364 .unwrap_err();
1365 assert!(matches!(err, Error::BlobInsufficientLength));
1366 });
1367 }
1368
1369 #[test_traced("DEBUG")]
1370 fn test_read_many_into_single_item() {
1371 let executor = deterministic::Runner::default();
1372 executor.start(|context: deterministic::Context| async move {
1373 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1374 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1375 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1376 .await
1377 .unwrap();
1378
1379 let data = vec![0xAA; 8];
1380 append.append(&data).await.unwrap();
1381 assert_eq!(append.size(), 8);
1382
1383 let mut buf = vec![0u8; 8];
1384 append
1385 .read_many_into(&mut buf, &[0], NZUsize!(8))
1386 .await
1387 .unwrap();
1388 assert_eq!(&buf, &data);
1389 });
1390 }
1391
1392 #[test_traced("DEBUG")]
1393 #[should_panic(expected = "buf must hold one slot per range totaling its length")]
1394 fn test_read_many_into_rejects_invalid_buffer_len() {
1395 let executor = deterministic::Runner::default();
1396 executor.start(|context: deterministic::Context| async move {
1397 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1398 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1399 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1400 .await
1401 .unwrap();
1402
1403 let data: Vec<u8> = (0..16).collect();
1404 append.append(&data).await.unwrap();
1405
1406 let offsets = [0u64, 4];
1407 let mut buf = vec![0u8; 7];
1408 let _ = append.read_many_into(&mut buf, &offsets, NZUsize!(4)).await;
1409 });
1410 }
1411
1412 #[test_traced("DEBUG")]
1413 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
1414 fn test_read_many_into_rejects_unsorted_offsets() {
1415 let executor = deterministic::Runner::default();
1416 executor.start(|context: deterministic::Context| async move {
1417 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1418 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1419 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1420 .await
1421 .unwrap();
1422
1423 let data: Vec<u8> = (0..16).collect();
1424 append.append(&data).await.unwrap();
1425
1426 let mut buf = vec![0u8; 8];
1427 let _ = append.read_many_into(&mut buf, &[8, 4], NZUsize!(4)).await;
1428 });
1429 }
1430
1431 #[test_traced("DEBUG")]
1432 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
1433 fn test_read_many_into_rejects_overlapping_offsets() {
1434 let executor = deterministic::Runner::default();
1435 executor.start(|context: deterministic::Context| async move {
1436 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1437 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1438 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1439 .await
1440 .unwrap();
1441
1442 let data: Vec<u8> = (0..16).collect();
1443 append.append(&data).await.unwrap();
1444
1445 let mut buf = vec![0u8; 8];
1446 let _ = append.read_many_into(&mut buf, &[2, 4], NZUsize!(4)).await;
1447 });
1448 }
1449
1450 #[test_traced("DEBUG")]
1451 fn test_read_many_into_rejects_offset_overflow() {
1452 let executor = deterministic::Runner::default();
1453 executor.start(|context: deterministic::Context| async move {
1454 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1455 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1456 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1457 .await
1458 .unwrap();
1459
1460 let data: Vec<u8> = (0..16).collect();
1461 append.append(&data).await.unwrap();
1462
1463 let mut buf = vec![0u8; 8];
1464 let err = append
1465 .read_many_into(&mut buf, &[u64::MAX - 1, 4], NZUsize!(4))
1466 .await
1467 .unwrap_err();
1468 assert!(matches!(err, Error::OffsetOverflow));
1469 });
1470 }
1471
1472 #[test_traced("DEBUG")]
1473 fn test_read_many_into_matches_read_at() {
1474 let executor = deterministic::Runner::default();
1476 executor.start(|context: deterministic::Context| async move {
1477 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1478 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1479 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1480 .await
1481 .unwrap();
1482
1483 let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
1485 append.append(&data).await.unwrap();
1486 append.sync().await.unwrap();
1487 let more: Vec<u8> = (0u8..50).collect();
1489 append.append(&more).await.unwrap();
1490 assert_eq!(append.size(), 350);
1491
1492 let item_size = 10;
1493 let offsets: Vec<u64> = (0..35).map(|i| i * item_size as u64).collect();
1494 let mut batch_buf = vec![0u8; offsets.len() * item_size];
1495 append
1496 .read_many_into(&mut batch_buf, &offsets, NZUsize!(item_size))
1497 .await
1498 .unwrap();
1499
1500 for (i, &off) in offsets.iter().enumerate() {
1502 let single = append.read_at(off, item_size).await.unwrap().coalesce();
1503 assert_eq!(
1504 &batch_buf[i * item_size..(i + 1) * item_size],
1505 single.as_ref(),
1506 "mismatch at offset {off}",
1507 );
1508 }
1509 });
1510 }
1511
1512 #[test_traced("DEBUG")]
1513 fn test_read_many_into_scattered_cache_misses() {
1514 let executor = deterministic::Runner::default();
1518 executor.start(|context: deterministic::Context| async move {
1519 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1520 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
1522 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1523 .await
1524 .unwrap();
1525
1526 let synced: Vec<u8> = (0u8..=255)
1528 .cycle()
1529 .take(PAGE_SIZE.get() as usize * 3)
1530 .collect();
1531 append.append(&synced).await.unwrap();
1532 append.sync().await.unwrap();
1533
1534 let item_size = 10;
1537 let tip_len = PAGE_SIZE.get() as usize / 2;
1538 let tip: Vec<u8> = (100u8..=255).cycle().take(tip_len).collect();
1539 append.append(&tip).await.unwrap();
1540
1541 let _ = append.read_at(0, item_size).await.unwrap();
1543 let _ = append
1544 .read_at(PAGE_SIZE.get() as u64 * 2, item_size)
1545 .await
1546 .unwrap();
1547
1548 let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
1551 let tip_off = synced.len() as u64 + item_size as u64;
1552 let offsets = [
1553 0u64, PAGE_SIZE.get() as u64, PAGE_SIZE.get() as u64 * 2, straddle_off, tip_off, ];
1559 let mut buf = vec![0u8; offsets.len() * item_size];
1560 append
1561 .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
1562 .await
1563 .unwrap();
1564
1565 let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
1566 for (i, &off) in offsets.iter().enumerate() {
1567 assert_eq!(
1568 &buf[i * item_size..(i + 1) * item_size],
1569 &read[off as usize..off as usize + item_size],
1570 );
1571 }
1572 });
1573 }
1574
1575 #[test_traced("DEBUG")]
1576 fn test_read_many_into_straddle_prefix_miss() {
1577 let executor = deterministic::Runner::default();
1581 executor.start(|context: deterministic::Context| async move {
1582 let (blob, blob_size) = context
1583 .open("test_partition", b"rmany_smiss")
1584 .await
1585 .unwrap();
1586 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
1588 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1589 .await
1590 .unwrap();
1591
1592 let synced: Vec<u8> = (0u8..=255)
1594 .cycle()
1595 .take(PAGE_SIZE.get() as usize * 3)
1596 .collect();
1597 append.append(&synced).await.unwrap();
1598 append.sync().await.unwrap();
1599 let item_size = 10;
1600 let tip: Vec<u8> = (100u8..=255)
1601 .cycle()
1602 .take(PAGE_SIZE.get() as usize / 2)
1603 .collect();
1604 append.append(&tip).await.unwrap();
1605
1606 let _ = append.read_at(0, item_size).await.unwrap();
1609
1610 let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
1611 let tip_off = synced.len() as u64 + item_size as u64;
1612 let offsets = [straddle_off, tip_off];
1613 let mut buf = vec![0u8; offsets.len() * item_size];
1614 let hits = append
1615 .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
1616 .await
1617 .unwrap();
1618
1619 assert_eq!(hits, 1);
1621 let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
1622 for (i, &off) in offsets.iter().enumerate() {
1623 assert_eq!(
1624 &buf[i * item_size..(i + 1) * item_size],
1625 &read[off as usize..off as usize + item_size],
1626 );
1627 }
1628 });
1629 }
1630
1631 #[test_traced("DEBUG")]
1632 fn test_append_crc_empty() {
1633 let executor = deterministic::Runner::default();
1634 executor.start(|context: deterministic::Context| async move {
1635 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1637 assert_eq!(blob_size, 0);
1638
1639 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1641
1642 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1644 .await
1645 .unwrap();
1646
1647 assert_eq!(append.size(), 0);
1649
1650 append.sync().await.unwrap();
1652 drop(append);
1653
1654 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1655 assert_eq!(blob_size, 0); let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1658 .await
1659 .unwrap();
1660
1661 assert_eq!(append.size(), 0);
1662 });
1663 }
1664
1665 #[test_traced("DEBUG")]
1666 fn test_append_crc_basic() {
1667 let executor = deterministic::Runner::default();
1668 executor.start(|context: deterministic::Context| async move {
1669 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1671 assert_eq!(blob_size, 0);
1672
1673 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1675
1676 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1678 .await
1679 .unwrap();
1680
1681 assert_eq!(append.size(), 0);
1683
1684 let data = vec![1, 2, 3, 4, 5];
1686 append.append(&data).await.unwrap();
1687
1688 assert_eq!(append.size(), 5);
1690
1691 let more_data = vec![6, 7, 8, 9, 10];
1693 append.append(&more_data).await.unwrap();
1694
1695 assert_eq!(append.size(), 10);
1697
1698 let read_buf = append.read_at(0, 5).await.unwrap().coalesce();
1700 assert_eq!(read_buf, &data[..]);
1701
1702 let read_buf = append.read_at(5, 5).await.unwrap().coalesce();
1704 assert_eq!(read_buf, &more_data[..]);
1705
1706 let read_buf = append.read_at(0, 10).await.unwrap().coalesce();
1708 assert_eq!(read_buf, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
1709
1710 append.sync().await.unwrap();
1713 drop(append);
1714
1715 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1716 assert_eq!(blob_size, 115);
1718 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1719 .await
1720 .unwrap();
1721 assert_eq!(append.size(), 10); let spanning_data: Vec<u8> = (11..=110).collect();
1727 append.append(&spanning_data).await.unwrap();
1728 assert_eq!(append.size(), 110);
1729
1730 let read_buf = append.read_at(10, 100).await.unwrap().coalesce();
1732 assert_eq!(read_buf, &spanning_data[..]);
1733
1734 let read_buf = append.read_at(0, 110).await.unwrap().coalesce();
1736 let expected: Vec<u8> = (1..=110).collect();
1737 assert_eq!(read_buf, &expected[..]);
1738
1739 append.sync().await.unwrap();
1741 drop(append);
1742
1743 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1744 assert_eq!(blob_size, 230);
1746 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1747 .await
1748 .unwrap();
1749 assert_eq!(append.size(), 110);
1750
1751 let boundary_data: Vec<u8> = (111..=206).collect();
1755 assert_eq!(boundary_data.len(), 96);
1756 append.append(&boundary_data).await.unwrap();
1757 assert_eq!(append.size(), 206);
1758
1759 let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
1761 let expected: Vec<u8> = (1..=206).collect();
1762 assert_eq!(read_buf, &expected[..]);
1763
1764 append.sync().await.unwrap();
1766 drop(append);
1767
1768 let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1769 assert_eq!(blob_size, 230);
1771 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1772 .await
1773 .unwrap();
1774 assert_eq!(append.size(), 206);
1775
1776 let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
1778 assert_eq!(read_buf, &expected[..]);
1779 });
1780 }
1781
1782 #[test_traced("DEBUG")]
1783 fn test_append_owned_bypass_from_empty_tip() {
1784 let executor = deterministic::Runner::default();
1787 executor.start(|context: deterministic::Context| async move {
1788 let (blob, blob_size) = context
1789 .open("test_partition", b"owned_empty")
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.clone())
1794 .await
1795 .unwrap();
1796
1797 let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
1799 let src = IoBuf::from(data.clone());
1800 let src_start = src.as_ptr() as usize;
1801 let src_range = src_start..src_start + src.len();
1802 append.append_owned(src.clone()).await.unwrap();
1803 assert_eq!(append.size(), 500);
1804
1805 let tip_ptr = append.buffer.as_ref().as_ptr() as usize;
1807 assert!(!src_range.contains(&tip_ptr));
1808
1809 let mut probe = vec![0u8; PAGE_SIZE.get() as usize];
1812 assert_eq!(
1813 append.cache_ref.read_cached(append.id, &mut probe, 0),
1814 PAGE_SIZE.get() as usize
1815 );
1816 assert_eq!(probe, &data[..PAGE_SIZE.get() as usize]);
1817
1818 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1820 assert_eq!(read_buf, &data[..]);
1821
1822 append.sync().await.unwrap();
1824 drop(append);
1825
1826 let (blob, blob_size) = context
1827 .open("test_partition", b"owned_empty")
1828 .await
1829 .unwrap();
1830 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1831 .await
1832 .unwrap();
1833 assert_eq!(append.size(), 500);
1834 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1835 assert_eq!(read_buf, &data[..]);
1836 });
1837 }
1838
1839 #[test_traced("DEBUG")]
1840 fn test_append_owned_bypass_with_synced_partial_page() {
1841 let executor = deterministic::Runner::default();
1844 executor.start(|context: deterministic::Context| async move {
1845 let (blob, blob_size) = context
1846 .open("test_partition", b"owned_partial")
1847 .await
1848 .unwrap();
1849 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1850 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1851 .await
1852 .unwrap();
1853
1854 let all: Vec<u8> = (0..500).map(|i| (i % 247) as u8).collect();
1856 append.append(&all[..50]).await.unwrap();
1857 append.sync().await.unwrap();
1858
1859 append
1862 .append_owned(IoBuf::from(all[50..].to_vec()))
1863 .await
1864 .unwrap();
1865 assert_eq!(append.size(), 500);
1866 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1867 assert_eq!(read_buf, &all[..]);
1868
1869 drop(append);
1872 let (blob, blob_size) = context
1873 .open("test_partition", b"owned_partial")
1874 .await
1875 .unwrap();
1876 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1877 .await
1878 .unwrap();
1879 assert_eq!(append.size(), 50);
1880 let read_buf = append.read_at(0, 50).await.unwrap().coalesce();
1881 assert_eq!(read_buf, &all[..50]);
1882
1883 append
1886 .append_owned(IoBuf::from(all[50..].to_vec()))
1887 .await
1888 .unwrap();
1889 append.sync().await.unwrap();
1890 drop(append);
1891
1892 let (blob, blob_size) = context
1893 .open("test_partition", b"owned_partial")
1894 .await
1895 .unwrap();
1896 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1897 .await
1898 .unwrap();
1899 assert_eq!(append.size(), 500);
1900 let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1901 assert_eq!(read_buf, &all[..]);
1902 });
1903 }
1904
1905 #[test_traced("DEBUG")]
1906 fn test_append_owned_bypass_with_buffered_tip() {
1907 let executor = deterministic::Runner::default();
1910 executor.start(|context: deterministic::Context| async move {
1911 let (blob, blob_size) = context
1912 .open("test_partition", b"owned_buffered")
1913 .await
1914 .unwrap();
1915 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1916 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1917 .await
1918 .unwrap();
1919
1920 let all: Vec<u8> = (0..430).map(|i| (i % 239) as u8).collect();
1921 append.append(&all[..30]).await.unwrap();
1922 append
1923 .append_owned(IoBuf::from(all[30..].to_vec()))
1924 .await
1925 .unwrap();
1926 assert_eq!(append.size(), 430);
1927 let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
1928 assert_eq!(read_buf, &all[..]);
1929
1930 append.sync().await.unwrap();
1931 drop(append);
1932
1933 let (blob, blob_size) = context
1934 .open("test_partition", b"owned_buffered")
1935 .await
1936 .unwrap();
1937 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1938 .await
1939 .unwrap();
1940 assert_eq!(append.size(), 430);
1941 let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
1942 assert_eq!(read_buf, &all[..]);
1943 });
1944 }
1945
1946 #[test_traced("DEBUG")]
1947 fn test_append_owned_exact_page_multiple_and_small() {
1948 let executor = deterministic::Runner::default();
1951 executor.start(|context: deterministic::Context| async move {
1952 let (blob, blob_size) = context
1953 .open("test_partition", b"owned_exact")
1954 .await
1955 .unwrap();
1956 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1957 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1958 .await
1959 .unwrap();
1960
1961 let bulk: Vec<u8> = (0..412).map(|i| (i % 233) as u8).collect();
1963 append
1964 .append_owned(IoBuf::from(bulk.clone()))
1965 .await
1966 .unwrap();
1967 assert_eq!(append.size(), 412);
1968
1969 let small: Vec<u8> = (0..10).map(|i| (i % 229) as u8).collect();
1971 append
1972 .append_owned(IoBuf::from(small.clone()))
1973 .await
1974 .unwrap();
1975 assert_eq!(append.size(), 422);
1976
1977 let read_buf = append.read_at(0, 422).await.unwrap().coalesce();
1978 assert_eq!(&read_buf.as_ref()[..412], &bulk[..]);
1979 assert_eq!(&read_buf.as_ref()[412..], &small[..]);
1980
1981 append.sync().await.unwrap();
1982 drop(append);
1983
1984 let (blob, blob_size) = context
1985 .open("test_partition", b"owned_exact")
1986 .await
1987 .unwrap();
1988 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1989 .await
1990 .unwrap();
1991 assert_eq!(append.size(), 422);
1992 });
1993 }
1994
1995 #[test_traced("DEBUG")]
1996 fn test_append_owned_physical_bytes_match_buffered() {
1997 let executor = deterministic::Runner::default();
2000 executor.start(|context: deterministic::Context| async move {
2001 let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2002 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2003
2004 let (blob, size) = context
2005 .open("test_partition", b"phys_direct")
2006 .await
2007 .unwrap();
2008 let mut direct = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
2009 .await
2010 .unwrap();
2011 direct
2012 .append_owned(IoBuf::from(data.clone()))
2013 .await
2014 .unwrap();
2015 direct.sync().await.unwrap();
2016 drop(direct);
2017
2018 let (blob, size) = context
2020 .open("test_partition", b"phys_buffered")
2021 .await
2022 .unwrap();
2023 let mut buffered = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2024 .await
2025 .unwrap();
2026 for chunk in data.chunks(10) {
2027 buffered.append(chunk).await.unwrap();
2028 }
2029 buffered.sync().await.unwrap();
2030 drop(buffered);
2031
2032 let (blob_a, size_a) = context
2033 .open("test_partition", b"phys_direct")
2034 .await
2035 .unwrap();
2036 let (blob_b, size_b) = context
2037 .open("test_partition", b"phys_buffered")
2038 .await
2039 .unwrap();
2040 assert_eq!(size_a, size_b);
2041 let bytes_a = blob_a.read_at(0, size_a as usize).await.unwrap().coalesce();
2042 let bytes_b = blob_b.read_at(0, size_b as usize).await.unwrap().coalesce();
2043 assert_eq!(bytes_a.as_ref(), bytes_b.as_ref());
2044 });
2045 }
2046
2047 #[test_traced("DEBUG")]
2048 fn test_append_borrowed_large_takes_direct_path() {
2049 let executor = deterministic::Runner::default();
2052 executor.start(|context: deterministic::Context| async move {
2053 let (blob, blob_size) = context
2054 .open("test_partition", b"borrowed_large")
2055 .await
2056 .unwrap();
2057 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2058 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2059 .await
2060 .unwrap();
2061
2062 let all: Vec<u8> = (0..530).map(|i| (i % 241) as u8).collect();
2064 append.append(&all[..30]).await.unwrap();
2065
2066 append.append(&all[30..]).await.unwrap();
2068 assert_eq!(append.size(), 530);
2069
2070 assert_eq!(append.buffer.len(), 15);
2072
2073 let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2074 assert_eq!(read_buf, &all[..]);
2075
2076 append.sync().await.unwrap();
2077 drop(append);
2078
2079 let (blob, blob_size) = context
2080 .open("test_partition", b"borrowed_large")
2081 .await
2082 .unwrap();
2083 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2084 .await
2085 .unwrap();
2086 assert_eq!(append.size(), 530);
2087 let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2088 assert_eq!(read_buf, &all[..]);
2089 });
2090 }
2091
2092 #[test_traced("DEBUG")]
2093 fn test_sync_releases_tip_pool_slot_after_full_drain() {
2094 let executor = deterministic::Runner::default();
2095 executor.start(|context: deterministic::Context| async move {
2096 let mut registry = Registry::default();
2097 let pool = BufferPool::new(
2098 BufferPoolConfig::for_storage()
2099 .with_pool_min_size(PAGE_SIZE.get() as usize)
2100 .with_max_per_class(NZU32!(2)),
2101 &mut registry,
2102 );
2103 let cache_ref = CacheRef::new(pool.clone(), PAGE_SIZE, NZUsize!(1));
2104
2105 let (blob, blob_size) = context
2106 .open("test_partition", b"release_tip_backing")
2107 .await
2108 .unwrap();
2109 assert_eq!(blob_size, 0);
2110
2111 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2112 .await
2113 .unwrap();
2114
2115 append
2116 .append(&vec![7; PAGE_SIZE.get() as usize])
2117 .await
2118 .unwrap();
2119
2120 assert!(
2122 matches!(
2123 pool.try_alloc(BUFFER_SIZE),
2124 Err(crate::iobuf::PoolError::Exhausted)
2125 ),
2126 "full-page tip should occupy the remaining pooled slot before sync"
2127 );
2128
2129 append.sync().await.unwrap();
2130
2131 assert!(
2133 pool.try_alloc(BUFFER_SIZE).is_ok(),
2134 "sync should release pooled backing when no partial tail remains"
2135 );
2136 });
2137 }
2138
2139 #[test_traced("DEBUG")]
2140 fn test_sync_uses_range_sync_for_single_flush() {
2141 let executor = deterministic::Runner::default();
2142 executor.start(|context: deterministic::Context| async move {
2143 let blob = SyncTrackingBlob::new();
2144 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2145 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2146 .await
2147 .unwrap();
2148
2149 append.sync().await.unwrap();
2151 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2152 assert_eq!(writes, 0);
2153 assert_eq!(full_syncs, 1);
2154 assert_eq!(range_syncs, 0);
2155
2156 let data = b"hello world";
2158 append.append(data).await.unwrap();
2159 append.sync().await.unwrap();
2160
2161 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2162 assert_eq!(writes, 1);
2163 assert_eq!(full_syncs, 1);
2164 assert_eq!(range_syncs, 1);
2165
2166 append.sync().await.unwrap();
2168 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2169 assert_eq!(writes, 1);
2170 assert_eq!(full_syncs, 1);
2171 assert_eq!(range_syncs, 1);
2172
2173 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2174 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2175 .await
2176 .unwrap();
2177 let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
2178 assert_eq!(read.as_ref(), data);
2179 });
2180 }
2181
2182 #[test_traced("DEBUG")]
2183 fn test_start_sync_persists_and_marks_clean() {
2185 let executor = deterministic::Runner::default();
2186 executor.start(|context: deterministic::Context| async move {
2187 let blob = SyncTrackingBlob::new();
2188 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2189 let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2190 .await
2191 .unwrap();
2192
2193 let handle = writer.start_sync().await;
2195 handle.await.unwrap();
2197 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2198 assert_eq!(writes, 0);
2199 assert_eq!(full_syncs, 1);
2200 assert_eq!(range_syncs, 0);
2201
2202 let data = b"hello world";
2204 writer.append(data).await.unwrap();
2205 writer.sync().await.unwrap();
2206 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2207 assert_eq!(writes, 1);
2208 assert_eq!(full_syncs, 1);
2209 assert_eq!(range_syncs, 1);
2210
2211 let handle = writer.start_sync().await;
2213 handle.await.unwrap();
2214 let (_, _, full_syncs, range_syncs) = blob.snapshot();
2215 assert_eq!(full_syncs, 1);
2216 assert_eq!(range_syncs, 1);
2217
2218 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2220 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2221 .await
2222 .unwrap();
2223 let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
2224 assert_eq!(read.as_ref(), data);
2225 });
2226 }
2227
2228 #[test_traced("DEBUG")]
2229 fn test_sync_waits_for_outstanding_start_sync() {
2231 let executor = deterministic::Runner::default();
2232 executor.start(|context: deterministic::Context| async move {
2233 let inner = SyncTrackingBlob::new();
2234 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2235 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2236 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2237
2238 let handle = writer.start_sync().await;
2239 let deferred = next_pending_sync(&pending);
2240
2241 let mut sync = Box::pin(writer.sync());
2243 assert!(
2244 sync.as_mut().now_or_never().is_none(),
2245 "sync must wait for the outstanding start_sync handle"
2246 );
2247 drop(sync);
2248 let (_, _, full_syncs, range_syncs) = inner.snapshot();
2249 assert_eq!(full_syncs, 0);
2250 assert_eq!(range_syncs, 0);
2251
2252 deferred.release.send(Ok(())).unwrap();
2254 writer.sync().await.unwrap();
2255 handle.await.unwrap();
2256 let (_, _, full_syncs, range_syncs) = inner.snapshot();
2257 assert_eq!(full_syncs, 1);
2258 assert_eq!(range_syncs, 0);
2259 });
2260 }
2261
2262 #[test_traced("DEBUG")]
2263 fn test_sync_after_start_sync_and_small_write_waits_before_range_sync() {
2265 let executor = deterministic::Runner::default();
2266 executor.start(|context: deterministic::Context| async move {
2267 let inner = SyncTrackingBlob::new();
2268 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2269 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2270 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2271
2272 let handle = writer.start_sync().await;
2273 let deferred = next_pending_sync(&pending);
2274 writer.append(b"hello world").await.unwrap();
2275
2276 let mut sync = Box::pin(writer.sync());
2278 assert!(
2279 sync.as_mut().now_or_never().is_none(),
2280 "sync must join the outstanding barrier before flushing the small write"
2281 );
2282 drop(sync);
2283 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2284 assert_eq!(writes, 0);
2285 assert_eq!(full_syncs, 0);
2286 assert_eq!(range_syncs, 0);
2287
2288 deferred.release.send(Ok(())).unwrap();
2290 writer.sync().await.unwrap();
2291 handle.await.unwrap();
2292 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2293 assert_eq!(writes, 1);
2294 assert_eq!(full_syncs, 1);
2295 assert_eq!(range_syncs, 1);
2296 });
2297 }
2298
2299 #[test_traced("DEBUG")]
2300 fn test_write_flush_waits_for_outstanding_start_sync() {
2302 let executor = deterministic::Runner::default();
2303 executor.start(|context: deterministic::Context| async move {
2304 let inner = SyncTrackingBlob::new();
2305 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2306 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2307 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2308
2309 let handle = writer.start_sync().await;
2310 let deferred = next_pending_sync(&pending);
2311
2312 let data = vec![7; BUFFER_SIZE + PAGE_SIZE.get() as usize];
2313 let append = context.child("append").spawn(move |_| async move {
2314 writer.append(&data).await.unwrap();
2315 writer
2316 });
2317 deferred
2319 .blocked
2320 .await
2321 .expect("append never waited on start_sync");
2322 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2323 assert_eq!(writes, 0);
2324 assert_eq!(full_syncs, 0);
2325 assert_eq!(range_syncs, 0);
2326
2327 deferred.release.send(Ok(())).unwrap();
2329 let mut writer = append.await.unwrap();
2330 handle.await.unwrap();
2331 writer.sync().await.unwrap();
2332 let (_, writes, full_syncs, _) = inner.snapshot();
2333 assert!(writes > 0);
2334 assert!(full_syncs > 0);
2335 });
2336 }
2337
2338 #[test_traced("DEBUG")]
2339 fn test_seal_waits_for_outstanding_start_sync_before_flushing() {
2341 let executor = deterministic::Runner::default();
2342 executor.start(|context: deterministic::Context| async move {
2343 let inner = SyncTrackingBlob::new();
2344 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2345 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2346 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2347
2348 let prior = writer.start_sync().await;
2349 let deferred = next_pending_sync(&pending);
2350 writer.append(b"hello world").await.unwrap();
2351
2352 let seal = context
2353 .child("seal")
2354 .spawn(move |_| async move { writer.seal().await });
2355 deferred
2357 .blocked
2358 .await
2359 .expect("seal never waited on start_sync");
2360 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2361 assert_eq!(writes, 0);
2362 assert_eq!(full_syncs, 0);
2363 assert_eq!(range_syncs, 0);
2364
2365 deferred.release.send(Ok(())).unwrap();
2367 let sealed = seal.await.unwrap().unwrap();
2368 prior.await.unwrap();
2369 let read = sealed
2370 .read_at(0, b"hello world".len())
2371 .await
2372 .unwrap()
2373 .coalesce();
2374 assert_eq!(read.as_ref(), b"hello world");
2375 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2376 assert_eq!(writes, 1);
2377 assert_eq!(full_syncs, 1);
2378 assert_eq!(range_syncs, 0);
2379 });
2380 }
2381
2382 #[test_traced("DEBUG")]
2383 fn test_snapshot_waits_for_outstanding_start_sync_before_flushing() {
2385 let executor = deterministic::Runner::default();
2386 executor.start(|context: deterministic::Context| async move {
2387 let inner = SyncTrackingBlob::new();
2388 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2389 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2390 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2391
2392 let prior = writer.start_sync().await;
2394 let deferred = next_pending_sync(&pending);
2395 writer.append(b"hello world").await.unwrap();
2396
2397 let snapshot = context.child("snapshot").spawn(move |_| async move {
2398 let snapshot = writer.snapshot().await.unwrap();
2399 let read = snapshot
2400 .read_at(0, b"hello world".len())
2401 .await
2402 .unwrap()
2403 .coalesce();
2404 assert_eq!(read.as_ref(), b"hello world");
2405 writer
2406 });
2407
2408 deferred
2410 .blocked
2411 .await
2412 .expect("snapshot never waited on start_sync");
2413 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2414 assert_eq!(writes, 0);
2415 assert_eq!(full_syncs, 0);
2416 assert_eq!(range_syncs, 0);
2417
2418 deferred.release.send(Ok(())).unwrap();
2420 let _writer = snapshot.await.unwrap();
2421 prior.await.unwrap();
2422 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2423 assert_eq!(writes, 1);
2424 assert_eq!(full_syncs, 1);
2425 assert_eq!(range_syncs, 0);
2426 });
2427 }
2428
2429 #[test_traced("DEBUG")]
2430 fn test_replay_waits_for_outstanding_start_sync_before_flushing() {
2432 let executor = deterministic::Runner::default();
2433 executor.start(|context: deterministic::Context| async move {
2434 let inner = SyncTrackingBlob::new();
2435 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2436 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2437 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2438
2439 let prior = writer.start_sync().await;
2441 let deferred = next_pending_sync(&pending);
2442 writer.append(b"hello world").await.unwrap();
2443
2444 let replay = context.child("replay").spawn(move |_| async move {
2445 {
2446 let mut replay = writer.replay(NZUsize!(BUFFER_SIZE)).await.unwrap();
2447 assert!(replay.ensure(1).await.unwrap());
2448 assert_eq!(replay.chunk()[0], b'h');
2449 }
2450 writer
2451 });
2452
2453 deferred
2455 .blocked
2456 .await
2457 .expect("replay never waited on start_sync");
2458 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2459 assert_eq!(writes, 0);
2460 assert_eq!(full_syncs, 0);
2461 assert_eq!(range_syncs, 0);
2462
2463 deferred.release.send(Ok(())).unwrap();
2465 let _writer = replay.await.unwrap();
2466 prior.await.unwrap();
2467 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2468 assert_eq!(writes, 1);
2469 assert_eq!(full_syncs, 1);
2470 assert_eq!(range_syncs, 0);
2471 });
2472 }
2473
2474 #[test_traced("DEBUG")]
2475 fn test_resize_grow_waits_for_outstanding_start_sync_before_writing() {
2477 let executor = deterministic::Runner::default();
2478 executor.start(|context: deterministic::Context| async move {
2479 let inner = SyncTrackingBlob::new();
2480 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2481 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2482 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2483
2484 let prior = writer.start_sync().await;
2486 let deferred = next_pending_sync(&pending);
2487
2488 let target_size = (BUFFER_SIZE + PAGE_SIZE.get() as usize) as u64;
2489 let resize = context.child("resize_grow").spawn(move |_| async move {
2490 writer.resize(target_size).await.unwrap();
2491 writer
2492 });
2493
2494 deferred
2496 .blocked
2497 .await
2498 .expect("resize grow never waited on start_sync");
2499 let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2500 assert_eq!(writes, 0);
2501 assert_eq!(full_syncs, 0);
2502 assert_eq!(range_syncs, 0);
2503
2504 deferred.release.send(Ok(())).unwrap();
2506 let mut writer = resize.await.unwrap();
2507 prior.await.unwrap();
2508 assert_eq!(writer.size(), target_size);
2509 writer.sync().await.unwrap();
2510 let (_, writes, full_syncs, _) = inner.snapshot();
2511 assert!(writes > 0);
2512 assert!(full_syncs > 0);
2513 });
2514 }
2515
2516 #[test_traced("DEBUG")]
2517 fn test_resize_shrink_waits_for_outstanding_start_sync_before_resizing() {
2519 let executor = deterministic::Runner::default();
2520 executor.start(|context: deterministic::Context| async move {
2521 let inner = SyncTrackingBlob::new();
2522 let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2523 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2524 let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2525
2526 let data = vec![3; PAGE_SIZE.get() as usize * 2];
2528 writer.append(&data).await.unwrap();
2529 writer.sync().await.unwrap();
2530 writer.append(b"x").await.unwrap();
2531 let prior = writer.start_sync().await;
2532 let deferred = next_pending_sync(&pending);
2533 let physical_size = inner.size();
2534
2535 let resize = context.child("resize_shrink").spawn(move |_| async move {
2536 writer.resize(PAGE_SIZE.get() as u64).await.unwrap();
2537 writer
2538 });
2539
2540 deferred
2542 .blocked
2543 .await
2544 .expect("resize shrink never waited on start_sync");
2545 assert_eq!(
2546 inner.size(),
2547 physical_size,
2548 "resize must not shrink the blob before the pending sync finishes"
2549 );
2550
2551 deferred.release.send(Ok(())).unwrap();
2553 let writer = resize.await.unwrap();
2554 prior.await.unwrap();
2555 assert_eq!(writer.size(), PAGE_SIZE.get() as u64);
2556 assert!(inner.size() < physical_size);
2557 });
2558 }
2559
2560 #[test_traced("DEBUG")]
2561 fn test_sync_failed_range_sync_does_not_mark_clean() {
2562 let executor = deterministic::Runner::default();
2563 executor.start(|context: deterministic::Context| async move {
2564 let name = b"failed_range_sync";
2565 let (blob, size) = context.open("test_partition", name).await.unwrap();
2566 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2567 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2568 .await
2569 .unwrap();
2570
2571 append.append(b"abc").await.unwrap();
2573
2574 context.remove("test_partition", Some(name)).await.unwrap();
2576 assert!(append.sync().await.is_err());
2577
2578 assert!(append.sync().await.is_err());
2581 });
2582 }
2583
2584 #[test_traced("DEBUG")]
2585 fn test_sync_uses_full_sync_after_prior_plain_flush() {
2586 let executor = deterministic::Runner::default();
2587 executor.start(|context: deterministic::Context| async move {
2588 let blob = SyncTrackingBlob::new();
2589 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2590 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2591 .await
2592 .unwrap();
2593
2594 let data = vec![7u8; BUFFER_SIZE + 1];
2597 append.append(&data).await.unwrap();
2598 append.sync().await.unwrap();
2599
2600 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2601 assert_eq!(writes, 2);
2602 assert_eq!(full_syncs, 1);
2603 assert_eq!(range_syncs, 0);
2604
2605 append.sync().await.unwrap();
2607 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2608 assert_eq!(writes, 2);
2609 assert_eq!(full_syncs, 1);
2610 assert_eq!(range_syncs, 0);
2611
2612 append.append(b"tip").await.unwrap();
2615 append.sync().await.unwrap();
2616
2617 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2618 assert_eq!(writes, 4);
2619 assert_eq!(full_syncs, 2);
2620 assert_eq!(range_syncs, 0);
2621
2622 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2623 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2624 .await
2625 .unwrap();
2626 let mut expected = data;
2627 expected.extend_from_slice(b"tip");
2628 let read = reopened
2629 .read_at(0, expected.len())
2630 .await
2631 .unwrap()
2632 .coalesce();
2633 assert_eq!(read.as_ref(), expected.as_slice());
2634 });
2635 }
2636
2637 #[test_traced("DEBUG")]
2638 fn test_sync_uses_full_sync_after_replay_plain_flush() {
2639 let executor = deterministic::Runner::default();
2640 executor.start(|context: deterministic::Context| async move {
2641 let blob = SyncTrackingBlob::new();
2642 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2643 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2644 .await
2645 .unwrap();
2646
2647 append.append(b"replayed").await.unwrap();
2649
2650 let mut replay = append.replay(NZUsize!(1024)).await.unwrap();
2652 assert!(replay.ensure(b"replayed".len()).await.unwrap());
2653 assert_eq!(replay.remaining(), b"replayed".len());
2654 assert_eq!(replay.chunk(), b"replayed");
2655
2656 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2657 assert_eq!(writes, 1);
2658 assert_eq!(full_syncs, 0);
2659 assert_eq!(range_syncs, 0);
2660
2661 append.sync().await.unwrap();
2663 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2664 assert_eq!(writes, 1);
2665 assert_eq!(full_syncs, 1);
2666 assert_eq!(range_syncs, 0);
2667 });
2668 }
2669
2670 #[test_traced("DEBUG")]
2671 fn test_recreated_sync_preserves_replay_plain_flush_barrier() {
2672 let executor = deterministic::Runner::default();
2673 executor.start(|context: deterministic::Context| async move {
2674 let blob = SyncTrackingBlob::new();
2675 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2676 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2677 .await
2678 .unwrap();
2679
2680 append.append(b"replayed").await.unwrap();
2681 let mut replay = append.replay(NZUsize!(1024)).await.unwrap();
2682 assert!(replay.ensure(b"replayed".len()).await.unwrap());
2683 assert_eq!(replay.remaining(), b"replayed".len());
2684 assert_eq!(replay.chunk(), b"replayed");
2685 drop(replay);
2686 drop(append);
2687
2688 let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
2689 assert!(durable.is_empty());
2690 assert_eq!(writes, 1);
2691 assert_eq!(full_syncs, 0);
2692 assert_eq!(range_syncs, 0);
2693
2694 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2695 let mut reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2696 .await
2697 .unwrap();
2698 assert_eq!(reopened.size(), b"replayed".len() as u64);
2699 reopened.sync().await.unwrap();
2700
2701 let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
2702 assert_eq!(durable.len(), blob.size() as usize);
2703 assert_eq!(writes, 1);
2704 assert_eq!(full_syncs, 1);
2705 assert_eq!(range_syncs, 0);
2706 });
2707 }
2708
2709 #[test_traced("DEBUG")]
2710 fn test_recreated_sync_skips_barrier_after_invalid_truncation() {
2711 let executor = deterministic::Runner::default();
2712 executor.start(|context: deterministic::Context| async move {
2713 let blob = SyncTrackingBlob::new();
2714 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2715 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2716 .await
2717 .unwrap();
2718 append.sync().await.unwrap();
2719 append.append(b"valid").await.unwrap();
2720 append.sync().await.unwrap();
2721 drop(append);
2722
2723 blob.write_at(blob.size(), b"junk").await.unwrap();
2724
2725 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2726 let mut reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2727 .await
2728 .unwrap();
2729 assert_eq!(reopened.size(), b"valid".len() as u64);
2730
2731 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2732 assert_eq!(writes, 2);
2733 assert_eq!(full_syncs, 2);
2734 assert_eq!(range_syncs, 1);
2735
2736 reopened.sync().await.unwrap();
2737
2738 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2739 assert_eq!(writes, 2);
2740 assert_eq!(full_syncs, 2);
2741 assert_eq!(range_syncs, 1);
2742 });
2743 }
2744
2745 #[test_traced("DEBUG")]
2746 fn test_sync_batches_split_protected_writes_with_full_sync() {
2747 let executor = deterministic::Runner::default();
2748 executor.start(|context: deterministic::Context| async move {
2749 let blob = SyncTrackingBlob::new();
2750 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2751 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2752 .await
2753 .unwrap();
2754 append.sync().await.unwrap();
2755
2756 append.append(b"abc").await.unwrap();
2758 append.sync().await.unwrap();
2759
2760 append.append(b"de").await.unwrap();
2763 append.sync().await.unwrap();
2764
2765 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2766 assert_eq!(writes, 3);
2767 assert_eq!(full_syncs, 2);
2768 assert_eq!(range_syncs, 1);
2769
2770 append.append(b"fg").await.unwrap();
2773 append.sync().await.unwrap();
2774
2775 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2776 assert_eq!(writes, 4);
2777 assert_eq!(full_syncs, 2);
2778 assert_eq!(range_syncs, 2);
2779
2780 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2781 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2782 .await
2783 .unwrap();
2784 let read = reopened.read_at(0, 7).await.unwrap().coalesce();
2785 assert_eq!(read.as_ref(), b"abcdefg");
2786 });
2787 }
2788
2789 #[test_traced("DEBUG")]
2790 fn test_read_up_to_zero_len_truncates_buffer() {
2791 let executor = deterministic::Runner::default();
2792 executor.start(|context: deterministic::Context| async move {
2793 let (blob, blob_size) = context
2795 .open("test_partition", b"read_up_to_zero_len")
2796 .await
2797 .unwrap();
2798 assert_eq!(blob_size, 0);
2799
2800 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2802
2803 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2805 .await
2806 .unwrap();
2807 append.append(&[1, 2, 3, 4]).await.unwrap();
2808
2809 let stale = vec![9, 8, 7, 6];
2811 let (buf, read) = append.read_up_to(0, 0, stale).await.unwrap();
2812
2813 assert_eq!(read, 0);
2814 assert_eq!(buf.len(), 0, "read_up_to must truncate returned buffer");
2815 assert_eq!(buf.freeze().as_ref(), b"");
2816 });
2817 }
2818
2819 fn read_crc_record_from_page(page_bytes: &[u8]) -> Checksum {
2821 let crc_start = page_bytes.len() - CHECKSUM_SIZE as usize;
2822 Checksum::read(&mut &page_bytes[crc_start..]).unwrap()
2823 }
2824
2825 #[derive(Clone)]
2827 struct PartialWriteBlob<B: Blob> {
2828 inner: B,
2829 writes: Arc<AtomicUsize>,
2830 failed_write_len: Arc<AtomicUsize>,
2831 fail_on: usize,
2832 partial_len: usize,
2833 }
2834
2835 impl<B: Blob> PartialWriteBlob<B> {
2836 fn new(inner: B, fail_on: usize, partial_len: usize) -> Self {
2837 Self {
2838 inner,
2839 writes: Arc::new(AtomicUsize::new(0)),
2840 failed_write_len: Arc::new(AtomicUsize::new(0)),
2841 fail_on,
2842 partial_len,
2843 }
2844 }
2845
2846 fn failed_write_len(&self) -> Arc<AtomicUsize> {
2847 self.failed_write_len.clone()
2848 }
2849
2850 fn write_count(&self) -> Arc<AtomicUsize> {
2851 self.writes.clone()
2852 }
2853 }
2854
2855 impl<B: Blob> crate::Blob for PartialWriteBlob<B> {
2856 async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
2857 self.inner.read_at(offset, len).await
2858 }
2859
2860 async fn read_at_buf(
2861 &self,
2862 offset: u64,
2863 len: usize,
2864 bufs: impl Into<IoBufsMut> + Send,
2865 ) -> Result<IoBufsMut, Error> {
2866 self.inner.read_at_buf(offset, len, bufs).await
2867 }
2868
2869 async fn write_at(&self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
2870 let bufs = bufs.into();
2871 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
2872 if write == self.fail_on {
2873 let bytes = bufs.coalesce();
2874 self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
2875 let partial_len = self.partial_len.min(bytes.len());
2876 self.inner
2877 .write_at(offset, bytes.slice(..partial_len))
2878 .await?;
2879 self.inner.sync().await?;
2880 return Err(Error::Io(
2881 std::io::Error::other("injected partial write").into(),
2882 ));
2883 }
2884
2885 self.inner.write_at(offset, bufs).await
2886 }
2887
2888 async fn write_at_sync(
2889 &self,
2890 offset: u64,
2891 bufs: impl Into<IoBufs> + Send,
2892 ) -> Result<(), Error> {
2893 let bufs = bufs.into();
2894 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
2895 if write == self.fail_on {
2896 let bytes = bufs.coalesce();
2897 self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
2898 let partial_len = self.partial_len.min(bytes.len());
2899 self.inner
2900 .write_at_sync(offset, bytes.slice(..partial_len))
2901 .await?;
2902 return Err(Error::Io(
2903 std::io::Error::other("injected partial write").into(),
2904 ));
2905 }
2906
2907 self.inner.write_at_sync(offset, bufs).await
2908 }
2909
2910 async fn resize(&self, len: u64) -> Result<(), Error> {
2911 self.inner.resize(len).await
2912 }
2913
2914 async fn sync(&self) -> Result<(), Error> {
2915 self.inner.sync().await
2916 }
2917
2918 async fn start_sync(&self) -> Handle<()> {
2919 self.inner.start_sync().await
2920 }
2921 }
2922
2923 #[derive(Clone)]
2925 struct DelayedReadBlob<B: Blob> {
2926 inner: B,
2927 offset: u64,
2928 len: usize,
2929 reads: Arc<AtomicUsize>,
2930 started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
2931 release: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
2932 }
2933
2934 impl<B: Blob> DelayedReadBlob<B> {
2935 fn new(
2936 inner: B,
2937 offset: u64,
2938 len: usize,
2939 started: oneshot::Sender<()>,
2940 release: oneshot::Receiver<()>,
2941 ) -> Self {
2942 Self {
2943 inner,
2944 offset,
2945 len,
2946 reads: Arc::new(AtomicUsize::new(0)),
2947 started: Arc::new(Mutex::new(Some(started))),
2948 release: Arc::new(Mutex::new(Some(release))),
2949 }
2950 }
2951 }
2952
2953 impl<B: Blob> crate::Blob for DelayedReadBlob<B> {
2954 async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
2955 if offset == self.offset
2956 && len == self.len
2957 && self.reads.fetch_add(1, Ordering::SeqCst) == 0
2958 {
2959 let bytes = self.inner.read_at(offset, len).await?;
2960
2961 let sender = self
2962 .started
2963 .lock()
2964 .take()
2965 .expect("delayed read start signal consumed more than once");
2966 let _ = sender.send(());
2967
2968 let release = self
2969 .release
2970 .lock()
2971 .take()
2972 .expect("delayed read release receiver consumed more than once");
2973 release.await.expect("release signal dropped");
2974
2975 return Ok(bytes);
2976 }
2977
2978 self.inner.read_at(offset, len).await
2979 }
2980
2981 async fn read_at_buf(
2982 &self,
2983 offset: u64,
2984 len: usize,
2985 bufs: impl Into<IoBufsMut> + Send,
2986 ) -> Result<IoBufsMut, Error> {
2987 if offset == self.offset
2988 && len == self.len
2989 && self.reads.fetch_add(1, Ordering::SeqCst) == 0
2990 {
2991 let bytes = self.inner.read_at_buf(offset, len, bufs).await?;
2992
2993 let sender = self
2994 .started
2995 .lock()
2996 .take()
2997 .expect("delayed read start signal consumed more than once");
2998 let _ = sender.send(());
2999
3000 let release = self
3001 .release
3002 .lock()
3003 .take()
3004 .expect("delayed read release receiver consumed more than once");
3005 release.await.expect("release signal dropped");
3006
3007 return Ok(bytes);
3008 }
3009
3010 self.inner.read_at_buf(offset, len, bufs).await
3011 }
3012
3013 async fn write_at(&self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
3014 self.inner.write_at(offset, bufs).await
3015 }
3016
3017 async fn write_at_sync(
3018 &self,
3019 offset: u64,
3020 bufs: impl Into<IoBufs> + Send,
3021 ) -> Result<(), Error> {
3022 self.inner.write_at_sync(offset, bufs).await
3023 }
3024
3025 async fn resize(&self, len: u64) -> Result<(), Error> {
3026 self.inner.resize(len).await
3027 }
3028
3029 async fn sync(&self) -> Result<(), Error> {
3030 self.inner.sync().await
3031 }
3032
3033 async fn start_sync(&self) -> Handle<()> {
3034 self.inner.start_sync().await
3035 }
3036 }
3037
3038 const DUMMY_MARKER: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
3041
3042 #[test]
3043 fn test_identify_protected_regions_equal_lengths() {
3044 let record = Checksum {
3046 len1: 50,
3047 crc1: 0xAAAAAAAA,
3048 len2: 50,
3049 crc2: 0xBBBBBBBB,
3050 };
3051
3052 let result =
3053 Writer::<crate::storage::memory::Blob>::identify_protected_regions(Some(&record));
3054 assert!(result.is_some());
3055 let (prefix_len, protected_crc) = result.unwrap();
3056 assert_eq!(prefix_len, 50);
3057 assert!(
3058 matches!(protected_crc, Slot::First),
3059 "First CRC should be protected when lengths are equal"
3060 );
3061 }
3062
3063 #[test]
3064 fn test_identify_protected_regions_len1_larger() {
3065 let record = Checksum {
3067 len1: 100,
3068 crc1: 0xAAAAAAAA,
3069 len2: 50,
3070 crc2: 0xBBBBBBBB,
3071 };
3072
3073 let result =
3074 Writer::<crate::storage::memory::Blob>::identify_protected_regions(Some(&record));
3075 assert!(result.is_some());
3076 let (prefix_len, protected_crc) = result.unwrap();
3077 assert_eq!(prefix_len, 100);
3078 assert!(
3079 matches!(protected_crc, Slot::First),
3080 "First CRC should be protected when len1 > len2"
3081 );
3082 }
3083
3084 #[test]
3085 fn test_identify_protected_regions_len2_larger() {
3086 let record = Checksum {
3088 len1: 50,
3089 crc1: 0xAAAAAAAA,
3090 len2: 100,
3091 crc2: 0xBBBBBBBB,
3092 };
3093
3094 let result =
3095 Writer::<crate::storage::memory::Blob>::identify_protected_regions(Some(&record));
3096 assert!(result.is_some());
3097 let (prefix_len, protected_crc) = result.unwrap();
3098 assert_eq!(prefix_len, 100);
3099 assert!(
3100 matches!(protected_crc, Slot::Second),
3101 "Second CRC should be protected when len2 > len1"
3102 );
3103 }
3104
3105 #[test_traced("DEBUG")]
3108 fn test_to_physical_pages_zero_copy_full_pages_and_materialized_partial() {
3109 let executor = deterministic::Runner::default();
3116 executor.start(|context: deterministic::Context| async move {
3117 let (blob, blob_size) = context
3119 .open("test_partition", b"to_physical_pages_zero_copy")
3120 .await
3121 .unwrap();
3122 assert_eq!(blob_size, 0);
3123
3124 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3126
3127 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
3129 .await
3130 .unwrap();
3131
3132 let logical_page_size = PAGE_SIZE.get() as usize;
3135 let partial_len = 17usize;
3136 let data: Vec<u8> = (0..(logical_page_size * 2 + partial_len))
3137 .map(|i| (i % 251) as u8)
3138 .collect();
3139
3140 let mut buffer = Buffer::new(0, data.len(), cache_ref.pool().clone());
3142 let over_capacity = buffer.append(&data);
3143 assert!(!over_capacity);
3144
3145 let (physical_pages, partial_page_state) =
3147 append.to_physical_pages(&buffer, true, None);
3148
3149 assert_eq!(physical_pages.chunk_count(), 5);
3152
3153 let crc_record = partial_page_state.expect("partial page state must be returned");
3155 let (len, _) = crc_record.get_crc();
3156 assert_eq!(len as usize, partial_len);
3157
3158 let physical_page_size = logical_page_size + CHECKSUM_SIZE as usize;
3161 let coalesced = physical_pages.coalesce();
3162 assert_eq!(coalesced.len(), physical_page_size * 3);
3163
3164 assert_eq!(
3166 &coalesced.as_ref()[..logical_page_size],
3167 &data[..logical_page_size]
3168 );
3169 assert_eq!(
3170 &coalesced.as_ref()[physical_page_size..physical_page_size + logical_page_size],
3171 &data[logical_page_size..logical_page_size * 2],
3172 );
3173
3174 let partial_start = physical_page_size * 2;
3177 assert_eq!(
3178 &coalesced.as_ref()[partial_start..partial_start + partial_len],
3179 &data[logical_page_size * 2..],
3180 );
3181 assert!(coalesced.as_ref()
3182 [partial_start + partial_len..partial_start + logical_page_size]
3183 .iter()
3184 .all(|byte| *byte == 0));
3185
3186 assert!(Checksum::validate_page(&coalesced.as_ref()[..physical_page_size]).is_some());
3188 assert!(Checksum::validate_page(
3189 &coalesced.as_ref()[physical_page_size..physical_page_size * 2]
3190 )
3191 .is_some());
3192 assert!(Checksum::validate_page(
3193 &coalesced.as_ref()[physical_page_size * 2..physical_page_size * 3]
3194 )
3195 .is_some());
3196 });
3197 }
3198
3199 #[test_traced("DEBUG")]
3205 fn test_crc_slot1_protected() {
3206 let executor = deterministic::Runner::default();
3207 executor.start(|context: deterministic::Context| async move {
3208 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3209 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3210 let slot0_offset = PAGE_SIZE.get() as u64;
3211 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3212
3213 let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
3215 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3216 .await
3217 .unwrap();
3218 append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3219 append.sync().await.unwrap();
3220 drop(append);
3221
3222 let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
3224 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3225 .await
3226 .unwrap();
3227 append
3228 .append(&(11..=30).collect::<Vec<u8>>())
3229 .await
3230 .unwrap();
3231 append.sync().await.unwrap();
3232 drop(append);
3233
3234 let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
3236 let page = blob
3237 .read_at(0, physical_page_size)
3238 .await
3239 .unwrap()
3240 .coalesce();
3241 let crc = read_crc_record_from_page(page.as_ref());
3242 assert!(
3243 crc.len2 > crc.len1,
3244 "Slot 1 should be authoritative (len2={} > len1={})",
3245 crc.len2,
3246 crc.len1
3247 );
3248
3249 let slot1_before: Vec<u8> = blob
3251 .read_at(slot1_offset, 6)
3252 .await
3253 .unwrap()
3254 .coalesce()
3255 .freeze()
3256 .into();
3257
3258 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec())
3260 .await
3261 .unwrap();
3262 blob.sync().await.unwrap();
3263
3264 let slot0_mangled: Vec<u8> = blob
3266 .read_at(slot0_offset, 6)
3267 .await
3268 .unwrap()
3269 .coalesce()
3270 .freeze()
3271 .into();
3272 assert_eq!(slot0_mangled, DUMMY_MARKER, "Mangle failed");
3273
3274 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3276 .await
3277 .unwrap();
3278 append
3279 .append(&(31..=50).collect::<Vec<u8>>())
3280 .await
3281 .unwrap();
3282 append.sync().await.unwrap();
3283 drop(append);
3284
3285 let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
3287
3288 let slot0_after: Vec<u8> = blob
3290 .read_at(slot0_offset, 6)
3291 .await
3292 .unwrap()
3293 .coalesce()
3294 .freeze()
3295 .into();
3296 assert_ne!(
3297 slot0_after, DUMMY_MARKER,
3298 "Slot 0 should have been overwritten with new CRC"
3299 );
3300
3301 let slot1_after: Vec<u8> = blob
3303 .read_at(slot1_offset, 6)
3304 .await
3305 .unwrap()
3306 .coalesce()
3307 .freeze()
3308 .into();
3309 assert_eq!(
3310 slot1_before, slot1_after,
3311 "Slot 1 was modified! Protected region violated."
3312 );
3313
3314 let page = blob
3316 .read_at(0, physical_page_size)
3317 .await
3318 .unwrap()
3319 .coalesce();
3320 let crc = read_crc_record_from_page(page.as_ref());
3321 assert_eq!(crc.len1, 50, "Slot 0 should have len=50");
3322 });
3323 }
3324
3325 #[test_traced("DEBUG")]
3331 fn test_crc_slot0_protected() {
3332 let executor = deterministic::Runner::default();
3333 executor.start(|context: deterministic::Context| async move {
3334 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3335 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3336 let slot0_offset = PAGE_SIZE.get() as u64;
3337 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3338
3339 let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
3341 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3342 .await
3343 .unwrap();
3344 append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3345 append.sync().await.unwrap();
3346 drop(append);
3347
3348 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
3350 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3351 .await
3352 .unwrap();
3353 append
3354 .append(&(11..=30).collect::<Vec<u8>>())
3355 .await
3356 .unwrap();
3357 append.sync().await.unwrap();
3358 drop(append);
3359
3360 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
3362 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3363 .await
3364 .unwrap();
3365 append
3366 .append(&(31..=50).collect::<Vec<u8>>())
3367 .await
3368 .unwrap();
3369 append.sync().await.unwrap();
3370 drop(append);
3371
3372 let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
3374 let page = blob
3375 .read_at(0, physical_page_size)
3376 .await
3377 .unwrap()
3378 .coalesce();
3379 let crc = read_crc_record_from_page(page.as_ref());
3380 assert!(
3381 crc.len1 > crc.len2,
3382 "Slot 0 should be authoritative (len1={} > len2={})",
3383 crc.len1,
3384 crc.len2
3385 );
3386
3387 let slot0_before: Vec<u8> = blob
3389 .read_at(slot0_offset, 6)
3390 .await
3391 .unwrap()
3392 .coalesce()
3393 .freeze()
3394 .into();
3395
3396 blob.write_at(slot1_offset, DUMMY_MARKER.to_vec())
3398 .await
3399 .unwrap();
3400 blob.sync().await.unwrap();
3401
3402 let slot1_mangled: Vec<u8> = blob
3404 .read_at(slot1_offset, 6)
3405 .await
3406 .unwrap()
3407 .coalesce()
3408 .freeze()
3409 .into();
3410 assert_eq!(slot1_mangled, DUMMY_MARKER, "Mangle failed");
3411
3412 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3414 .await
3415 .unwrap();
3416 append
3417 .append(&(51..=70).collect::<Vec<u8>>())
3418 .await
3419 .unwrap();
3420 append.sync().await.unwrap();
3421 drop(append);
3422
3423 let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
3425
3426 let slot1_after: Vec<u8> = blob
3428 .read_at(slot1_offset, 6)
3429 .await
3430 .unwrap()
3431 .coalesce()
3432 .freeze()
3433 .into();
3434 assert_ne!(
3435 slot1_after, DUMMY_MARKER,
3436 "Slot 1 should have been overwritten with new CRC"
3437 );
3438
3439 let slot0_after: Vec<u8> = blob
3441 .read_at(slot0_offset, 6)
3442 .await
3443 .unwrap()
3444 .coalesce()
3445 .freeze()
3446 .into();
3447 assert_eq!(
3448 slot0_before, slot0_after,
3449 "Slot 0 was modified! Protected region violated."
3450 );
3451
3452 let page = blob
3454 .read_at(0, physical_page_size)
3455 .await
3456 .unwrap()
3457 .coalesce();
3458 let crc = read_crc_record_from_page(page.as_ref());
3459 assert_eq!(crc.len2, 70, "Slot 1 should have len=70");
3460 });
3461 }
3462
3463 #[test_traced("DEBUG")]
3469 fn test_data_prefix_not_overwritten() {
3470 let executor = deterministic::Runner::default();
3471 executor.start(|context: deterministic::Context| async move {
3472 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3473 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3474
3475 let (blob, _) = context
3477 .open("test_partition", b"prefix_test")
3478 .await
3479 .unwrap();
3480 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3481 .await
3482 .unwrap();
3483 let data1: Vec<u8> = (1..=20).collect();
3484 append.append(&data1).await.unwrap();
3485 append.sync().await.unwrap();
3486 drop(append);
3487
3488 let (blob, size) = context
3490 .open("test_partition", b"prefix_test")
3491 .await
3492 .unwrap();
3493 assert_eq!(size, physical_page_size as u64);
3494
3495 let prefix_before: Vec<u8> = blob
3496 .read_at(0, 20)
3497 .await
3498 .unwrap()
3499 .coalesce()
3500 .freeze()
3501 .into();
3502
3503 blob.write_at(25, DUMMY_MARKER.to_vec()).await.unwrap();
3505 blob.sync().await.unwrap();
3506
3507 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3509 .await
3510 .unwrap();
3511 append
3512 .append(&(21..=40).collect::<Vec<u8>>())
3513 .await
3514 .unwrap();
3515 append.sync().await.unwrap();
3516 drop(append);
3517
3518 let (blob, _) = context
3520 .open("test_partition", b"prefix_test")
3521 .await
3522 .unwrap();
3523
3524 let prefix_after: Vec<u8> = blob
3526 .read_at(0, 20)
3527 .await
3528 .unwrap()
3529 .coalesce()
3530 .freeze()
3531 .into();
3532 assert_eq!(prefix_before, prefix_after, "Data prefix was modified!");
3533
3534 let overwritten: Vec<u8> = blob
3536 .read_at(25, 6)
3537 .await
3538 .unwrap()
3539 .coalesce()
3540 .freeze()
3541 .into();
3542 assert_eq!(
3543 overwritten,
3544 vec![26, 27, 28, 29, 30, 31],
3545 "New data should overwrite padding area"
3546 );
3547 });
3548 }
3549
3550 #[test_traced("DEBUG")]
3556 fn test_crc_slot_protection_across_page_boundary() {
3557 let executor = deterministic::Runner::default();
3558 executor.start(|context: deterministic::Context| async move {
3559 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3560 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3561 let slot0_offset = PAGE_SIZE.get() as u64;
3562 let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3563
3564 let (blob, _) = context.open("test_partition", b"boundary").await.unwrap();
3566 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3567 .await
3568 .unwrap();
3569 append.append(&(1..=50).collect::<Vec<u8>>()).await.unwrap();
3570 append.sync().await.unwrap();
3571 drop(append);
3572
3573 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
3575 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3576 .await
3577 .unwrap();
3578 append
3579 .append(&(51..=80).collect::<Vec<u8>>())
3580 .await
3581 .unwrap();
3582 append.sync().await.unwrap();
3583 drop(append);
3584
3585 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
3587 let page = blob
3588 .read_at(0, physical_page_size)
3589 .await
3590 .unwrap()
3591 .coalesce();
3592 let crc = read_crc_record_from_page(page.as_ref());
3593 assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
3594
3595 let slot1_before: Vec<u8> = blob
3597 .read_at(slot1_offset, 6)
3598 .await
3599 .unwrap()
3600 .coalesce()
3601 .freeze()
3602 .into();
3603
3604 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec())
3606 .await
3607 .unwrap();
3608 blob.sync().await.unwrap();
3609
3610 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3612 .await
3613 .unwrap();
3614 append
3615 .append(&(81..=120).collect::<Vec<u8>>())
3616 .await
3617 .unwrap();
3618 append.sync().await.unwrap();
3619 drop(append);
3620
3621 let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
3623 assert_eq!(size, (physical_page_size * 2) as u64, "Should have 2 pages");
3624
3625 let slot0_after: Vec<u8> = blob
3627 .read_at(slot0_offset, 6)
3628 .await
3629 .unwrap()
3630 .coalesce()
3631 .freeze()
3632 .into();
3633 assert_ne!(
3634 slot0_after, DUMMY_MARKER,
3635 "Slot 0 should have full-page CRC"
3636 );
3637
3638 let slot1_after: Vec<u8> = blob
3640 .read_at(slot1_offset, 6)
3641 .await
3642 .unwrap()
3643 .coalesce()
3644 .freeze()
3645 .into();
3646 assert_eq!(
3647 slot1_before, slot1_after,
3648 "Slot 1 was modified during page boundary crossing!"
3649 );
3650
3651 let page0 = blob
3653 .read_at(0, physical_page_size)
3654 .await
3655 .unwrap()
3656 .coalesce();
3657 let crc0 = read_crc_record_from_page(page0.as_ref());
3658 assert_eq!(
3659 crc0.len1,
3660 PAGE_SIZE.get(),
3661 "Slot 0 should have full page length"
3662 );
3663
3664 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3666 .await
3667 .unwrap();
3668 assert_eq!(append.size(), 120);
3669 let all_data: Vec<u8> = append.read_at(0, 120).await.unwrap().coalesce().into();
3670 let expected: Vec<u8> = (1..=120).collect();
3671 assert_eq!(all_data, expected);
3672 });
3673 }
3674
3675 #[test_traced("DEBUG")]
3684 fn test_crc_fallback_on_corrupted_primary() {
3685 let executor = deterministic::Runner::default();
3686 executor.start(|context: deterministic::Context| async move {
3687 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3688 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3689 let crc2_offset = PAGE_SIZE.get() as u64 + 8;
3691
3692 let (blob, _) = context
3694 .open("test_partition", b"crc_fallback")
3695 .await
3696 .unwrap();
3697 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3698 .await
3699 .unwrap();
3700 let data1: Vec<u8> = (1..=10).collect();
3701 append.append(&data1).await.unwrap();
3702 append.sync().await.unwrap();
3703 drop(append);
3704
3705 let (blob, size) = context
3707 .open("test_partition", b"crc_fallback")
3708 .await
3709 .unwrap();
3710 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3711 .await
3712 .unwrap();
3713 append
3714 .append(&(11..=30).collect::<Vec<u8>>())
3715 .await
3716 .unwrap();
3717 append.sync().await.unwrap();
3718 drop(append);
3719
3720 let (blob, size) = context
3722 .open("test_partition", b"crc_fallback")
3723 .await
3724 .unwrap();
3725 assert_eq!(size, physical_page_size as u64);
3726
3727 let page = blob
3728 .read_at(0, physical_page_size)
3729 .await
3730 .unwrap()
3731 .coalesce();
3732 let crc = read_crc_record_from_page(page.as_ref());
3733 assert!(
3734 crc.len2 > crc.len1,
3735 "Slot 1 should be authoritative (len2={} > len1={})",
3736 crc.len2,
3737 crc.len1
3738 );
3739 assert_eq!(crc.len2, 30, "Slot 1 should have len=30");
3740 assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
3741
3742 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
3744 .await
3745 .unwrap();
3746 assert_eq!(append.size(), 30);
3747 let all_data: Vec<u8> = append.read_at(0, 30).await.unwrap().coalesce().into();
3748 let expected: Vec<u8> = (1..=30).collect();
3749 assert_eq!(all_data, expected);
3750 drop(append);
3751
3752 blob.write_at(crc2_offset, vec![0xDE, 0xAD, 0xBE, 0xEF])
3755 .await
3756 .unwrap();
3757 blob.sync().await.unwrap();
3758
3759 let page = blob
3761 .read_at(0, physical_page_size)
3762 .await
3763 .unwrap()
3764 .coalesce();
3765 let crc = read_crc_record_from_page(page.as_ref());
3766 assert_eq!(crc.len2, 30, "len2 should still be 30 after corruption");
3767 assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be our corrupted value");
3768
3769 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3771 .await
3772 .unwrap();
3773
3774 assert_eq!(
3776 append.size(),
3777 10,
3778 "Should fall back to slot 0's 10 bytes after primary CRC corruption"
3779 );
3780
3781 let fallback_data: Vec<u8> = append.read_at(0, 10).await.unwrap().coalesce().into();
3783 assert_eq!(
3784 fallback_data, data1,
3785 "Fallback data should match original 10 bytes"
3786 );
3787
3788 let result = append.read_at(0, 11).await;
3790 assert!(result.is_err(), "Reading beyond fallback size should fail");
3791 });
3792 }
3793
3794 #[test_traced("DEBUG")]
3806 fn test_non_last_page_rejects_partial_fallback() {
3807 let executor = deterministic::Runner::default();
3808 executor.start(|context: deterministic::Context| async move {
3809 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3810 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3811 let page0_crc2_offset = PAGE_SIZE.get() as u64 + 8;
3813
3814 let (blob, _) = context
3816 .open("test_partition", b"non_last_page")
3817 .await
3818 .unwrap();
3819 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3820 .await
3821 .unwrap();
3822 append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3823 append.sync().await.unwrap();
3824 drop(append);
3825
3826 let (blob, size) = context
3828 .open("test_partition", b"non_last_page")
3829 .await
3830 .unwrap();
3831 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3832 .await
3833 .unwrap();
3834 append
3836 .append(&(11..=PAGE_SIZE.get() as u8).collect::<Vec<u8>>())
3837 .await
3838 .unwrap();
3839 append.sync().await.unwrap();
3840 drop(append);
3841
3842 let (blob, size) = context
3844 .open("test_partition", b"non_last_page")
3845 .await
3846 .unwrap();
3847 let page = blob
3848 .read_at(0, physical_page_size)
3849 .await
3850 .unwrap()
3851 .coalesce();
3852 let crc = read_crc_record_from_page(page.as_ref());
3853 assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
3854 assert_eq!(
3855 crc.len2,
3856 PAGE_SIZE.get(),
3857 "Slot 1 should have len=103 (full page)"
3858 );
3859 assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
3860
3861 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3863 .await
3864 .unwrap();
3865 append
3867 .append(&(104..=113).collect::<Vec<u8>>())
3868 .await
3869 .unwrap();
3870 append.sync().await.unwrap();
3871 drop(append);
3872
3873 let (blob, size) = context
3875 .open("test_partition", b"non_last_page")
3876 .await
3877 .unwrap();
3878 assert_eq!(
3879 size,
3880 (physical_page_size * 2) as u64,
3881 "Should have 2 physical pages"
3882 );
3883
3884 let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
3886 .await
3887 .unwrap();
3888 assert_eq!(append.size(), 113);
3889 let all_data: Vec<u8> = append.read_at(0, 113).await.unwrap().coalesce().into();
3890 let expected: Vec<u8> = (1..=113).collect();
3891 assert_eq!(all_data, expected);
3892 drop(append);
3893
3894 blob.write_at(page0_crc2_offset, vec![0xDE, 0xAD, 0xBE, 0xEF])
3896 .await
3897 .unwrap();
3898 blob.sync().await.unwrap();
3899
3900 let page = blob
3902 .read_at(0, physical_page_size)
3903 .await
3904 .unwrap()
3905 .coalesce();
3906 let crc = read_crc_record_from_page(page.as_ref());
3907 assert_eq!(crc.len2, PAGE_SIZE.get(), "len2 should still be 103");
3908 assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be corrupted");
3909 assert_eq!(crc.len1, 10, "Fallback slot 0 has partial length");
3911
3912 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3918 .await
3919 .unwrap();
3920
3921 assert_eq!(append.size(), 113);
3924
3925 let result = append.read_at(0, 10).await;
3928 assert!(
3929 result.is_err(),
3930 "Reading from corrupted non-last page via Append should fail, but got: {:?}",
3931 result
3932 );
3933 drop(append);
3934
3935 let (blob, size) = context
3937 .open("test_partition", b"non_last_page")
3938 .await
3939 .unwrap();
3940 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3941 .await
3942 .unwrap();
3943 let mut replay = append.replay(NZUsize!(1024)).await.unwrap();
3944
3945 let result = replay.ensure(1).await;
3947 assert!(
3948 result.is_err(),
3949 "Reading from corrupted non-last page via Replay should fail, but got: {:?}",
3950 result
3951 );
3952 });
3953 }
3954
3955 #[test]
3956 fn test_resize_shrink_validates_crc() {
3957 let executor = deterministic::Runner::default();
3960
3961 executor.start(|context| async move {
3962 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3963 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3964
3965 let (blob, size) = context
3966 .open("test_partition", b"resize_crc_test")
3967 .await
3968 .unwrap();
3969
3970 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3971 .await
3972 .unwrap();
3973
3974 let data: Vec<u8> = (0..=249).collect();
3977 append.append(&data).await.unwrap();
3978 append.sync().await.unwrap();
3979 assert_eq!(append.size(), 250);
3980 drop(append);
3981
3982 let (blob, size) = context
3984 .open("test_partition", b"resize_crc_test")
3985 .await
3986 .unwrap();
3987 assert_eq!(size as usize, physical_page_size * 3);
3988
3989 let page1_crc_offset = (physical_page_size * 2 - CHECKSUM_SIZE as usize) as u64;
3991 blob.write_at(page1_crc_offset, vec![0xFF; CHECKSUM_SIZE as usize])
3992 .await
3993 .unwrap();
3994 blob.sync().await.unwrap();
3995
3996 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3999 .await
4000 .unwrap();
4001 assert_eq!(append.size(), 250);
4002
4003 let result = append.resize(150).await;
4007 assert!(
4008 matches!(result, Err(crate::Error::InvalidChecksum)),
4009 "Expected InvalidChecksum when shrinking to corrupted page, got: {:?}",
4010 result
4011 );
4012 });
4013 }
4014
4015 #[test]
4016 fn test_resize_invalidates_cache() {
4017 let executor = deterministic::Runner::default();
4022 executor.start(|context: deterministic::Context| async move {
4023 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4024 let (blob, blob_size) = context
4025 .open("test_partition", b"resize_invalidates_cache")
4026 .await
4027 .unwrap();
4028 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4029 .await
4030 .unwrap();
4031
4032 let page_size = PAGE_SIZE.get() as usize;
4035 let old_bytes = vec![0xAAu8; page_size];
4036 append.append(&old_bytes).await.unwrap();
4037 append.sync().await.unwrap();
4038
4039 let mut probe = vec![0u8; 16];
4041 assert!(append.try_read_sync_into(&mut probe, 0));
4042 assert_eq!(probe, vec![0xAAu8; 16]);
4043
4044 append.resize(0).await.unwrap();
4046 let new_bytes = vec![0xBBu8; 16];
4047 append.append(&new_bytes).await.unwrap();
4048
4049 let mut probe = vec![0u8; 16];
4052 let hit = append.try_read_sync_into(&mut probe, 0);
4053 assert!(
4054 !hit || probe == new_bytes,
4055 "try_read_sync_into served stale pre-resize bytes: {probe:?}"
4056 );
4057 });
4058 }
4059
4060 #[test]
4061 fn test_snapshot_fetch_cannot_repopulate_live_cache_after_resize() {
4062 let executor = deterministic::Runner::default();
4063 executor.start(|context: deterministic::Context| async move {
4064 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4065 let page_size = PAGE_SIZE.get() as usize;
4066 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
4067 let (inner, blob_size) = context
4068 .open("test_partition", b"snapshot_resize_cache")
4069 .await
4070 .unwrap();
4071 let (started_tx, started_rx) = oneshot::channel();
4072 let (release_tx, release_rx) = oneshot::channel();
4073 let blob = DelayedReadBlob::new(
4074 inner,
4075 physical_page_size as u64,
4076 physical_page_size,
4077 started_tx,
4078 release_rx,
4079 );
4080 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4081 .await
4082 .unwrap();
4083
4084 let old_page0 = vec![0x11u8; page_size];
4085 let old_page1 = vec![0x22u8; page_size];
4086 writer.append(&old_page0).await.unwrap();
4087 writer.append(&old_page1).await.unwrap();
4088 writer.sync().await.unwrap();
4089
4090 writer.cache_ref.invalidate_from(writer.id, 1);
4091
4092 let snapshot = writer.snapshot().await.unwrap();
4093 let snapshot_task = context
4094 .child("snapshot")
4095 .spawn(move |_| async move { snapshot.read_at(page_size as u64, page_size).await });
4096 started_rx.await.expect("snapshot read never started");
4097
4098 writer.resize(page_size as u64).await.unwrap();
4099 let new_page1 = vec![0x33u8; page_size];
4100 writer.append(&new_page1).await.unwrap();
4101 writer.sync().await.unwrap();
4102
4103 let _ = release_tx.send(());
4104 let stale = snapshot_task
4105 .await
4106 .expect("snapshot task failed")
4107 .expect("snapshot read failed")
4108 .coalesce();
4109 assert_eq!(stale.as_ref(), old_page1.as_slice());
4110
4111 let mut probe = vec![0u8; page_size];
4112 assert!(writer.try_read_sync_into(&mut probe, page_size as u64));
4113 assert_eq!(probe, new_page1);
4114 });
4115 }
4116
4117 #[test]
4118 fn test_resize_shrink_allowed_while_snapshot_alive() {
4119 let executor = deterministic::Runner::default();
4120 executor.start(|context: deterministic::Context| async move {
4121 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4122 let (blob, blob_size) = context
4123 .open("test_partition", b"snapshot_blocks_shrink")
4124 .await
4125 .unwrap();
4126 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4127 .await
4128 .unwrap();
4129
4130 let page_size = PAGE_SIZE.get() as usize;
4131 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2 + 7).collect();
4132 append.append(&data).await.unwrap();
4133 append.sync().await.unwrap();
4134
4135 let snapshot = append.snapshot().await.unwrap();
4136 let snapshot_clone = snapshot.clone();
4137 let snapshot_size = snapshot.size();
4138
4139 let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
4140 assert_eq!(read.as_ref(), data.as_slice());
4141
4142 append.resize(snapshot_size + 3).await.unwrap();
4144 assert_eq!(append.size(), snapshot_size + 3);
4145
4146 append.resize(snapshot_size - 1).await.unwrap();
4149 assert_eq!(append.size(), snapshot_size - 1);
4150 assert_eq!(snapshot_clone.size(), snapshot_size);
4151 });
4152 }
4153
4154 #[test]
4155 fn test_snapshot_read_many_into_matches_read_at() {
4156 let executor = deterministic::Runner::default();
4157 executor.start(|context: deterministic::Context| async move {
4158 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4159 let (blob, blob_size) = context
4160 .open("test_partition", b"snapshot_read_many")
4161 .await
4162 .unwrap();
4163 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4164 .await
4165 .unwrap();
4166
4167 let item_size = 4;
4168 let page_size = PAGE_SIZE.get() as usize;
4169 let total = page_size * 3 + 13;
4170 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
4171 append.append(&data).await.unwrap();
4172 append.sync().await.unwrap();
4173
4174 let snapshot = append.snapshot().await.unwrap();
4175 let offsets = [
4176 0,
4177 (page_size - item_size) as u64,
4178 (page_size + 5) as u64,
4179 (page_size * 3 + 2) as u64,
4180 ];
4181 let mut batch = vec![0u8; offsets.len() * item_size];
4183 snapshot
4184 .read_many_into(&mut batch, &offsets, NZUsize!(item_size))
4185 .await
4186 .unwrap();
4187
4188 for (item, offset) in batch.chunks_exact(item_size).zip(offsets) {
4189 let single = snapshot
4190 .read_at(offset, item_size)
4191 .await
4192 .unwrap()
4193 .coalesce();
4194 assert_eq!(item, single.as_ref());
4195 assert_eq!(item, &data[offset as usize..offset as usize + item_size]);
4196 }
4197 });
4198 }
4199
4200 #[test]
4201 fn test_snapshot_try_read_sync_prefers_snapshot_tail() {
4202 let executor = deterministic::Runner::default();
4203 executor.start(|context: deterministic::Context| async move {
4204 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4205 let (blob, blob_size) = context
4206 .open("test_partition", b"snapshot_try_read_tail")
4207 .await
4208 .unwrap();
4209 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4210 .await
4211 .unwrap();
4212
4213 let page_size = PAGE_SIZE.get() as usize;
4214 append.append(&vec![0xAA; page_size]).await.unwrap();
4215 append.sync().await.unwrap();
4216
4217 let tail = b"oldtail";
4218 append.append(tail).await.unwrap();
4219 let snapshot = append.snapshot().await.unwrap();
4220
4221 let poison = vec![0xBB; page_size];
4222 assert_eq!(
4224 append.cache_ref.cache(append.id, &poison, page_size as u64),
4225 0
4226 );
4227
4228 let mut read = vec![0; tail.len()];
4229 assert!(snapshot.try_read_sync_into(&mut read, page_size as u64));
4230 assert_eq!(read.as_slice(), tail);
4231 });
4232 }
4233
4234 #[test]
4235 fn test_resize_same_size_is_noop() {
4236 let executor = deterministic::Runner::default();
4237 executor.start(|context: deterministic::Context| async move {
4238 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4239 let (blob, blob_size) = context
4240 .open("test_partition", b"resize_same_size")
4241 .await
4242 .unwrap();
4243 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4244 .await
4245 .unwrap();
4246
4247 append.append(b"hello world").await.unwrap();
4248 assert_eq!(append.size(), 11);
4249
4250 append.resize(11).await.unwrap();
4252 assert_eq!(append.size(), 11);
4253
4254 let read = append.read_at(0, 11).await.unwrap().coalesce();
4256 assert_eq!(read.as_ref(), b"hello world");
4257 });
4258 }
4259
4260 #[test]
4261 fn test_resize_same_page_shrink_reopens_at_shorter_size() {
4262 let executor = deterministic::Runner::default();
4263
4264 executor.start(|context| async move {
4265 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4266 let data: Vec<u8> = (0..50).collect();
4267
4268 let (blob, size) = context
4269 .open("test_partition", b"same_page_shrink")
4270 .await
4271 .unwrap();
4272 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4273 .await
4274 .unwrap();
4275
4276 append.append(&data).await.unwrap();
4279 append.sync().await.unwrap();
4280
4281 append.resize(45).await.unwrap();
4282 drop(append);
4283
4284 let (blob, size) = context
4285 .open("test_partition", b"same_page_shrink")
4286 .await
4287 .unwrap();
4288 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4289 .await
4290 .unwrap();
4291 assert_eq!(append.size(), 45);
4292 let read = append.read_at(0, 45).await.unwrap().coalesce();
4293 assert_eq!(read.as_ref(), &data[..45]);
4294 });
4295 }
4296
4297 #[test]
4298 fn test_resize_same_page_shrink_survives_interrupted_crc_stage() {
4299 let executor = deterministic::Runner::default();
4300
4301 executor.start(|context| async move {
4302 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4303 let data: Vec<u8> = (0..50).collect();
4304
4305 let (blob, size) = context
4306 .open("test_partition", b"same_page_shrink_interrupted")
4307 .await
4308 .unwrap();
4309 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4310 .await
4311 .unwrap();
4312 append.append(&data[..40]).await.unwrap();
4313 append.sync().await.unwrap();
4314 append.append(&data[40..]).await.unwrap();
4315 append.sync().await.unwrap();
4316 drop(append);
4317
4318 let (blob, size) = context
4319 .open("test_partition", b"same_page_shrink_interrupted")
4320 .await
4321 .unwrap();
4322 let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
4323 let write_count = faulty_blob.write_count();
4324 let failed_write_len = faulty_blob.failed_write_len();
4325 let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
4326 .await
4327 .unwrap();
4328
4329 assert!(
4330 append.resize(45).await.is_err(),
4331 "phase-1 partial write should fail"
4332 );
4333 assert_eq!(write_count.load(Ordering::SeqCst), 1);
4334 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
4335 drop(append);
4336
4337 let (blob, size) = context
4338 .open("test_partition", b"same_page_shrink_interrupted")
4339 .await
4340 .unwrap();
4341 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4342 .await
4343 .unwrap();
4344 assert_eq!(append.size(), 50);
4345 let read = append.read_at(0, 50).await.unwrap().coalesce();
4346 assert_eq!(read.as_ref(), &data);
4347 });
4348 }
4349
4350 #[test]
4351 fn test_resize_same_page_shrink_survives_interrupted_len_stage() {
4352 let executor = deterministic::Runner::default();
4353
4354 executor.start(|context| async move {
4355 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
4356 const LARGE_BUFFER_SIZE: usize = 1_200;
4357
4358 let cache_ref =
4359 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
4360 let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
4361
4362 let (blob, size) = context
4363 .open("test_partition", b"same_page_shrink_len_stage")
4364 .await
4365 .unwrap();
4366 let mut append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4367 .await
4368 .unwrap();
4369 append.append(&data[..255]).await.unwrap();
4370 append.sync().await.unwrap();
4371 append.append(&data[255..]).await.unwrap();
4372 append.sync().await.unwrap();
4373 drop(append);
4374
4375 let (blob, size) = context
4376 .open("test_partition", b"same_page_shrink_len_stage")
4377 .await
4378 .unwrap();
4379 let faulty_blob = PartialWriteBlob::new(blob, 2, 1);
4380 let write_count = faulty_blob.write_count();
4381 let failed_write_len = faulty_blob.failed_write_len();
4382 let mut append = Writer::new(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4383 .await
4384 .unwrap();
4385
4386 assert!(
4387 append.resize(257).await.is_err(),
4388 "length-stage partial write should fail"
4389 );
4390 assert_eq!(write_count.load(Ordering::SeqCst), 2);
4391 assert_eq!(failed_write_len.load(Ordering::SeqCst), 2);
4392 drop(append);
4393
4394 let (blob, size) = context
4395 .open("test_partition", b"same_page_shrink_len_stage")
4396 .await
4397 .unwrap();
4398 let append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref)
4399 .await
4400 .unwrap();
4401 assert_eq!(append.size(), 300);
4402 let read = append.read_at(0, 300).await.unwrap().coalesce();
4403 assert_eq!(read.as_ref(), &data);
4404 });
4405 }
4406
4407 #[test]
4408 fn test_resize_same_page_shrink_preserves_validated_fallback_slot() {
4409 let executor = deterministic::Runner::default();
4410
4411 executor.start(|context| async move {
4412 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4413 let data: Vec<u8> = (0..52).collect();
4414
4415 let (blob, size) = context
4416 .open("test_partition", b"same_page_shrink_fallback_slot")
4417 .await
4418 .unwrap();
4419 let faulty_blob = PartialWriteBlob::new(blob.clone(), 5, 3);
4420 let write_count = faulty_blob.write_count();
4421 let failed_write_len = faulty_blob.failed_write_len();
4422 let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
4423 .await
4424 .unwrap();
4425 append.append(&data[..48]).await.unwrap();
4426 append.sync().await.unwrap();
4427 assert_eq!(write_count.load(Ordering::SeqCst), 1);
4428
4429 append.append(&data[48..50]).await.unwrap();
4430 append.sync().await.unwrap();
4431 assert_eq!(write_count.load(Ordering::SeqCst), 3);
4432
4433 append.append(&data[50..]).await.unwrap();
4434 append.sync().await.unwrap();
4435 assert_eq!(write_count.load(Ordering::SeqCst), 4);
4436
4437 let slot0_offset = PAGE_SIZE.get() as u64;
4442 blob.write_at(slot0_offset, DUMMY_MARKER.to_vec())
4443 .await
4444 .unwrap();
4445 blob.sync().await.unwrap();
4446
4447 assert!(
4448 append.resize(45).await.is_err(),
4449 "phase-1 partial write should fail"
4450 );
4451 assert_eq!(write_count.load(Ordering::SeqCst), 5);
4452 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
4453 drop(append);
4454
4455 let (blob, size) = context
4456 .open("test_partition", b"same_page_shrink_fallback_slot")
4457 .await
4458 .unwrap();
4459 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4460 .await
4461 .unwrap();
4462 assert_eq!(append.size(), 50);
4463 let read = append.read_at(0, 50).await.unwrap().coalesce();
4464 assert_eq!(read.as_ref(), &data[..50]);
4465 });
4466 }
4467
4468 #[test]
4469 fn test_resize_full_page_to_partial_reopens_at_shorter_size() {
4470 let executor = deterministic::Runner::default();
4471
4472 executor.start(|context| async move {
4473 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4474 let page_size = PAGE_SIZE.get() as u64;
4475 let target = page_size + 45;
4476 let data: Vec<u8> = (0..page_size * 2).map(|i| (i % 251) as u8).collect();
4477
4478 let (blob, size) = context
4479 .open("test_partition", b"full_page_to_partial")
4480 .await
4481 .unwrap();
4482 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4483 .await
4484 .unwrap();
4485 append.append(&data).await.unwrap();
4486 append.sync().await.unwrap();
4487
4488 append.resize(target).await.unwrap();
4489 drop(append);
4490
4491 let (blob, size) = context
4492 .open("test_partition", b"full_page_to_partial")
4493 .await
4494 .unwrap();
4495 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4496 .await
4497 .unwrap();
4498 assert_eq!(append.size(), target);
4499 let read = append.read_at(0, target as usize).await.unwrap().coalesce();
4500 assert_eq!(read.as_ref(), &data[..target as usize]);
4501 });
4502 }
4503
4504 #[test]
4505 fn test_resize_full_page_to_partial_survives_interrupted_crc_stage() {
4506 let executor = deterministic::Runner::default();
4507
4508 executor.start(|context| async move {
4509 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4510 let page_size = PAGE_SIZE.get() as u64;
4511 let target = page_size + 45;
4512 let data: Vec<u8> = (0..page_size * 3).map(|i| (i % 251) as u8).collect();
4513
4514 let (blob, size) = context
4515 .open("test_partition", b"full_page_to_partial_interrupted")
4516 .await
4517 .unwrap();
4518 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4519 .await
4520 .unwrap();
4521 append.append(&data).await.unwrap();
4522 append.sync().await.unwrap();
4523 drop(append);
4524
4525 let (blob, size) = context
4526 .open("test_partition", b"full_page_to_partial_interrupted")
4527 .await
4528 .unwrap();
4529 let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
4530 let write_count = faulty_blob.write_count();
4531 let failed_write_len = faulty_blob.failed_write_len();
4532 let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
4533 .await
4534 .unwrap();
4535
4536 assert!(
4537 append.resize(target).await.is_err(),
4538 "phase-1 partial write should fail"
4539 );
4540 assert_eq!(write_count.load(Ordering::SeqCst), 1);
4541 assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
4542 drop(append);
4543
4544 let (blob, size) = context
4545 .open("test_partition", b"full_page_to_partial_interrupted")
4546 .await
4547 .unwrap();
4548 let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4549 .await
4550 .unwrap();
4551 assert_eq!(append.size(), page_size * 2);
4552 let read = append
4553 .read_at(0, (page_size * 2) as usize)
4554 .await
4555 .unwrap()
4556 .coalesce();
4557 assert_eq!(read.as_ref(), &data[..(page_size * 2) as usize]);
4558 });
4559 }
4560
4561 #[test]
4562 fn test_resize_same_page_shrink_survives_interrupted_length_invalidation() {
4563 let executor = deterministic::Runner::default();
4564
4565 executor.start(|context| async move {
4566 const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
4567 const LARGE_BUFFER_SIZE: usize = 1_200;
4568
4569 let cache_ref =
4570 CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
4571 let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
4572
4573 let (blob, size) = context
4574 .open(
4575 "test_partition",
4576 b"same_page_shrink_interrupted_len_invalidation",
4577 )
4578 .await
4579 .unwrap();
4580 let mut append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4581 .await
4582 .unwrap();
4583 append.append(&data[..255]).await.unwrap();
4586 append.sync().await.unwrap();
4587 append.append(&data[255..]).await.unwrap();
4588 append.sync().await.unwrap();
4589 drop(append);
4590
4591 let (blob, size) = context
4592 .open(
4593 "test_partition",
4594 b"same_page_shrink_interrupted_len_invalidation",
4595 )
4596 .await
4597 .unwrap();
4598 let faulty_blob = PartialWriteBlob::new(blob, 3, 1);
4599 let write_count = faulty_blob.write_count();
4600 let failed_write_len = faulty_blob.failed_write_len();
4601 let mut append = Writer::new(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4602 .await
4603 .unwrap();
4604
4605 assert!(
4606 append.resize(40).await.is_err(),
4607 "old-slot length invalidation should fail"
4608 );
4609 assert_eq!(write_count.load(Ordering::SeqCst), 3);
4610 assert_eq!(
4611 failed_write_len.load(Ordering::SeqCst),
4612 CHECKSUM_SLOT_LEN_SIZE
4613 );
4614 drop(append);
4615
4616 let (blob, size) = context
4617 .open(
4618 "test_partition",
4619 b"same_page_shrink_interrupted_len_invalidation",
4620 )
4621 .await
4622 .unwrap();
4623 let append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref)
4624 .await
4625 .unwrap();
4626 assert_eq!(append.size(), 40);
4627 let read = append.read_at(0, 40).await.unwrap().coalesce();
4628 assert_eq!(read.as_ref(), &data[..40]);
4629 });
4630 }
4631
4632 #[test_traced("DEBUG")]
4633 fn test_resize_partial_shrink_without_physical_resize_uses_range_sync() {
4634 let executor = deterministic::Runner::default();
4635 executor.start(|context: deterministic::Context| async move {
4636 let blob = SyncTrackingBlob::new();
4637 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4638 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
4639 .await
4640 .unwrap();
4641 append.sync().await.unwrap();
4642
4643 let data = vec![5u8; PAGE_SIZE.get() as usize];
4644 append.append(&data).await.unwrap();
4645 append.sync().await.unwrap();
4646
4647 append.resize(50).await.unwrap();
4649 append.sync().await.unwrap();
4650
4651 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4652 assert_eq!(writes, 4);
4653 assert_eq!(full_syncs, 1);
4654 assert_eq!(range_syncs, 4);
4655 });
4656 }
4657
4658 #[test_traced("DEBUG")]
4659 fn test_resize_partial_shrink_with_physical_resize_clears_full_sync_requirement() {
4660 let executor = deterministic::Runner::default();
4661 executor.start(|context: deterministic::Context| async move {
4662 let blob = SyncTrackingBlob::new();
4663 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4664 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
4665 .await
4666 .unwrap();
4667 append.sync().await.unwrap();
4668
4669 let data = vec![9u8; PAGE_SIZE.get() as usize * 2];
4670 append.append(&data).await.unwrap();
4671 append.sync().await.unwrap();
4672
4673 append.resize(50).await.unwrap();
4676 append.sync().await.unwrap();
4677
4678 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4679 assert_eq!(writes, 4);
4680 assert_eq!(full_syncs, 2);
4681 assert_eq!(range_syncs, 3);
4682
4683 append.append(b"x").await.unwrap();
4685 append.sync().await.unwrap();
4686
4687 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4688 assert_eq!(writes, 5);
4689 assert_eq!(full_syncs, 2);
4690 assert_eq!(range_syncs, 4);
4691
4692 let mut expected = data[..50].to_vec();
4693 expected.push(b'x');
4694 let read = append.read_at(0, expected.len()).await.unwrap().coalesce();
4695 assert_eq!(read.as_ref(), expected.as_slice());
4696 });
4697 }
4698
4699 #[test_traced("DEBUG")]
4700 fn test_resize_page_boundary_shrink_uses_full_sync() {
4701 let executor = deterministic::Runner::default();
4702 executor.start(|context: deterministic::Context| async move {
4703 let blob = SyncTrackingBlob::new();
4704 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4705 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
4706 .await
4707 .unwrap();
4708 append.sync().await.unwrap();
4709
4710 let page_size = PAGE_SIZE.get() as usize;
4713 let data = vec![11u8; page_size * 2];
4714 append.append(&data).await.unwrap();
4715 append.sync().await.unwrap();
4716
4717 append.resize(PAGE_SIZE.get() as u64).await.unwrap();
4719 append.sync().await.unwrap();
4720
4721 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4723 assert_eq!(writes, 1);
4724 assert_eq!(full_syncs, 2);
4725 assert_eq!(range_syncs, 1);
4726
4727 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4728 let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
4729 .await
4730 .unwrap();
4731 assert_eq!(reopened.size(), PAGE_SIZE.get() as u64);
4732 let read = reopened.read_at(0, page_size).await.unwrap().coalesce();
4733 assert_eq!(read.as_ref(), &data[..page_size]);
4734 });
4735 }
4736
4737 #[test]
4738 fn test_reopen_partial_tail_append_and_resize() {
4739 let executor = deterministic::Runner::default();
4740
4741 executor.start(|context| async move {
4742 const PAGE_SIZE: NonZeroU16 = NZU16!(64);
4743 const BUFFER_SIZE: usize = 256;
4744
4745 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(4));
4746
4747 let (blob, size) = context
4748 .open("test_partition", b"partial_tail_test")
4749 .await
4750 .unwrap();
4751
4752 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4753 .await
4754 .unwrap();
4755
4756 append.append(&[1, 2, 3, 4, 5]).await.unwrap();
4758 append.sync().await.unwrap();
4759 assert_eq!(append.size(), 5);
4760 drop(append);
4761
4762 let (blob, size) = context
4763 .open("test_partition", b"partial_tail_test")
4764 .await
4765 .unwrap();
4766
4767 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4768 .await
4769 .unwrap();
4770 assert_eq!(append.size(), 5);
4771
4772 append.append(&[6, 7, 8]).await.unwrap();
4773 append.resize(6).await.unwrap();
4774 append.sync().await.unwrap();
4775
4776 let data: Vec<u8> = append.read_at(0, 6).await.unwrap().coalesce().into();
4777 assert_eq!(data, vec![1, 2, 3, 4, 5, 6]);
4778 });
4779 }
4780
4781 #[test]
4782 fn test_corrupted_crc_len_too_large() {
4783 let executor = deterministic::Runner::default();
4784
4785 executor.start(|context| async move {
4786 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4787 let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4788
4789 let (blob, size) = context
4791 .open("test_partition", b"crc_len_test")
4792 .await
4793 .unwrap();
4794
4795 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4796 .await
4797 .unwrap();
4798
4799 append.append(&[0x42; 50]).await.unwrap();
4800 append.sync().await.unwrap();
4801 drop(append);
4802
4803 let (blob, size) = context
4805 .open("test_partition", b"crc_len_test")
4806 .await
4807 .unwrap();
4808 assert_eq!(size as usize, physical_page_size);
4809
4810 let crc_offset = PAGE_SIZE.get() as u64;
4812
4813 let bad_crc_record: [u8; 12] = [
4816 0xFF, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
4821 blob.write_at(crc_offset, bad_crc_record.to_vec())
4822 .await
4823 .unwrap();
4824 blob.sync().await.unwrap();
4825
4826 let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
4828
4829 match result {
4832 Ok(append) => {
4833 let recovered_size = append.size();
4835 assert_eq!(
4836 recovered_size, 0,
4837 "Corrupted page should be truncated, size should be 0"
4838 );
4839 }
4840 Err(e) => {
4841 assert!(
4843 matches!(e, crate::Error::InvalidChecksum),
4844 "Expected InvalidChecksum error, got: {:?}",
4845 e
4846 );
4847 }
4848 }
4849 });
4850 }
4851
4852 #[test]
4853 fn test_corrupted_crc_both_slots_len_too_large() {
4854 let executor = deterministic::Runner::default();
4855
4856 executor.start(|context| async move {
4857 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4858
4859 let (blob, size) = context
4861 .open("test_partition", b"crc_both_bad")
4862 .await
4863 .unwrap();
4864
4865 let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4866 .await
4867 .unwrap();
4868
4869 append.append(&[0x42; 50]).await.unwrap();
4870 append.sync().await.unwrap();
4871 drop(append);
4872
4873 let (blob, size) = context
4875 .open("test_partition", b"crc_both_bad")
4876 .await
4877 .unwrap();
4878
4879 let crc_offset = PAGE_SIZE.get() as u64;
4880
4881 let bad_crc_record: [u8; 12] = [
4883 0x01, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x02, 0x00, 0xCA, 0xFE, 0xBA, 0xBE, ];
4888 blob.write_at(crc_offset, bad_crc_record.to_vec())
4889 .await
4890 .unwrap();
4891 blob.sync().await.unwrap();
4892
4893 let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
4895
4896 match result {
4897 Ok(append) => {
4898 assert_eq!(append.size(), 0);
4900 }
4901 Err(e) => {
4902 assert!(
4903 matches!(e, crate::Error::InvalidChecksum),
4904 "Expected InvalidChecksum, got: {:?}",
4905 e
4906 );
4907 }
4908 }
4909 });
4910 }
4911
4912 #[test_traced("DEBUG")]
4914 fn test_reader_sees_buffered_bytes() {
4915 let executor = deterministic::Runner::default();
4916 executor.start(|context: deterministic::Context| async move {
4917 let (blob, blob_size) = context.open("test_partition", b"rdr_buf").await.unwrap();
4918 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4919 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4920 .await
4921 .unwrap();
4922
4923 let data: Vec<u8> = (0u8..50).collect();
4924 writer.append(&data).await.unwrap();
4925
4926 assert_eq!(writer.size(), 50);
4928 let read = writer.read_at(0, 50).await.unwrap().coalesce();
4929 assert_eq!(read.as_ref(), data.as_slice());
4930
4931 let mut buf = vec![0u8; 10];
4932 assert!(writer.try_read_sync_into(&mut buf, 20));
4933 assert_eq!(buf, data[20..30]);
4934 });
4935 }
4936
4937 #[test_traced("DEBUG")]
4940 fn test_reader_read_past_resize_errors_cleanly() {
4941 let executor = deterministic::Runner::default();
4942 executor.start(|context: deterministic::Context| async move {
4943 let (blob, blob_size) = context.open("test_partition", b"rdr_rsz").await.unwrap();
4944 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4945 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4946 .await
4947 .unwrap();
4948
4949 let page_size = PAGE_SIZE.get() as usize;
4950 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
4951 writer.append(&data).await.unwrap();
4952 writer.sync().await.unwrap();
4953 assert_eq!(writer.size(), (page_size * 2) as u64);
4954
4955 let new_size = (page_size / 2) as u64;
4957 writer.resize(new_size).await.unwrap();
4958
4959 let err = writer
4961 .read_at(new_size, page_size)
4962 .await
4963 .expect_err("read past resized end must fail");
4964 assert!(matches!(err, crate::Error::BlobInsufficientLength));
4965
4966 let read = writer
4968 .read_at(0, new_size as usize)
4969 .await
4970 .unwrap()
4971 .coalesce();
4972 assert_eq!(read.as_ref(), &data[..new_size as usize]);
4973 });
4974 }
4975}