1use super::{
16 CHECKSUM_SIZE, CacheRef, Replay,
17 read::PageReader,
18 view::{Tail, View},
19};
20use crate::{Blob, Error, IoBuf, IoBufMut, IoBufs, ReadOptions};
21use commonware_utils::Widen;
22use std::{num::NonZeroUsize, sync::Arc};
23
24pub struct Sealed<B: Blob> {
27 inner: Arc<SealedInner<B>>,
28}
29
30impl<B: Blob> Clone for Sealed<B> {
31 fn clone(&self) -> Self {
32 Self {
33 inner: self.inner.clone(),
34 }
35 }
36}
37
38struct SealedInner<B: Blob> {
39 blob: Arc<B>,
41
42 size: u64,
44
45 partial_page: Option<IoBuf>,
49
50 cache_ref: CacheRef,
52
53 id: u64,
57}
58
59impl<B: Blob> Sealed<B> {
60 pub(super) fn new(
62 blob: Arc<B>,
63 size: u64,
64 partial_page: Option<IoBuf>,
65 cache_ref: CacheRef,
66 id: u64,
67 ) -> Self {
68 Self {
69 inner: Arc::new(SealedInner {
70 blob,
71 size,
72 partial_page,
73 cache_ref,
74 id,
75 }),
76 }
77 }
78
79 pub fn size(&self) -> u64 {
81 self.inner.size
82 }
83
84 fn partial_offset(&self) -> u64 {
87 self.inner.size
88 - self
89 .inner
90 .partial_page
91 .as_ref()
92 .map_or(0, |p| p.len() as u64)
93 }
94
95 fn view(&self) -> View<'_, B> {
97 View {
98 blob: &self.inner.blob,
99 cache_ref: &self.inner.cache_ref,
100 id: self.inner.id,
101 size: self.inner.size,
102 tail_offset: self.partial_offset(),
103 tail: Tail::Sealed(
104 self.inner
105 .partial_page
106 .as_ref()
107 .map_or(&[][..], |p| p.as_ref()),
108 ),
109 }
110 }
111
112 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
114 self.view().read_at(offset, len).await
115 }
116
117 pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
121 self.view().try_read_sync_into(buf, offset)
122 }
123
124 pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
126 self.view().read_into(buf, offset).await
127 }
128
129 pub async fn read_up_to(
134 &self,
135 offset: u64,
136 len: usize,
137 bufs: impl Into<IoBufMut> + Send,
138 ) -> Result<(IoBufMut, usize), Error> {
139 self.view().read_up_to(offset, len, bufs).await
140 }
141
142 pub async fn read_many_into(
150 &self,
151 buf: &mut [u8],
152 offsets: &[u64],
153 item_size: NonZeroUsize,
154 ) -> Result<usize, Error> {
155 self.view().read_many_into(buf, offsets, item_size).await
156 }
157
158 pub async fn read_ranges_into(
169 &self,
170 buf: &mut [u8],
171 ranges: &[(u64, usize)],
172 ) -> Result<usize, Error> {
173 self.view().read_ranges_into(buf, ranges).await
174 }
175
176 pub fn try_read_many_sync_into(
179 &self,
180 buf: &mut [u8],
181 offsets: &[u64],
182 item_size: NonZeroUsize,
183 ) -> Vec<usize> {
184 self.view().try_read_many_sync_into(buf, offsets, item_size)
185 }
186
187 pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
190 self.view().try_read_ranges_sync_into(buf, ranges)
191 }
192
193 pub fn replay(
206 &self,
207 buffer_size: NonZeroUsize,
208 read_options: ReadOptions,
209 ) -> Result<Replay<B>, Error> {
210 let page_size_nz = self.inner.cache_ref.page_size();
211 let page_size: u64 = page_size_nz.widen();
212 let physical_page_size = page_size
213 .checked_add(CHECKSUM_SIZE)
214 .ok_or(Error::OffsetOverflow)?;
215 let buffer_pages = (buffer_size.get() / physical_page_size as usize).max(1);
216
217 let partial_len = self
218 .inner
219 .partial_page
220 .as_ref()
221 .map_or(0, |p| p.len() as u64);
222 let full_pages = (self.inner.size - partial_len) / page_size;
223 let pages = full_pages + u64::from(partial_len > 0);
224 let physical_blob_size = physical_page_size
225 .checked_mul(pages)
226 .ok_or(Error::OffsetOverflow)?;
227 let logical_blob_size = self.inner.size;
228
229 let reader = PageReader::new(
230 self.inner.blob.clone(),
231 physical_blob_size,
232 logical_blob_size,
233 self.inner.partial_page.clone(),
234 buffer_pages,
235 page_size_nz,
236 read_options,
237 );
238 Ok(Replay::new(reader))
239 }
240
241 #[cfg(test)]
244 pub(super) fn cache_id(&self) -> u64 {
245 self.inner.id
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::{
253 Buf, Handle, IoBufsMut, Runner as _, Storage as _, WriteOptions,
254 buffer::{
255 paged::{CHECKSUM_SLOT_LEN_SIZE, Checksum, Writer},
256 tests::SyncTrackingBlob,
257 },
258 deterministic,
259 mocks::{DelayedSyncBlob, next_pending_sync},
260 };
261 use commonware_macros::test_traced;
262 use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
263 use std::num::NonZeroU16;
264
265 #[rstest::rstest]
268 fn test_replay_with_dont_cache_leaves_cache_cold(#[values(false, true)] snapshot: bool) {
269 deterministic::Runner::default().start(|context| async move {
270 let (blob, size) = context.open("review-cold", b"blob").await.unwrap();
271 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(32));
272 let writer = Writer::new(blob, size, BUFFER_SIZE, cache.clone())
273 .await
274 .unwrap();
275 let page = PAGE_SIZE.get() as usize;
276 let total = page * 9 + 25;
277 let data: Vec<u8> = (0..total).map(|i| (i % 251) as u8).collect();
278 let (writer, _) = writer.append(&data).await.unwrap();
279 let writer = writer.sync().await.unwrap();
280 let sealed = if snapshot {
281 writer.try_snapshot().unwrap()
282 } else {
283 let (sealed, sync) = writer.seal().await.unwrap();
284 sync.await.unwrap();
285 sealed
286 };
287 cache.clear();
288 let mut probe = vec![0; page];
289 for i in 0..9 {
290 assert!(!sealed.try_read_sync_into(&mut probe, (i * page) as u64));
291 }
292 let mut replay = sealed
293 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::DONT_CACHE)
294 .unwrap();
295 assert!(replay.ensure(total).await.unwrap());
296 assert_eq!(replay.copy_to_bytes(total).as_ref(), data);
297 assert!(!replay.ensure(1).await.unwrap());
298 for i in 0..9 {
299 assert!(!sealed.try_read_sync_into(&mut probe, (i * page) as u64));
300 }
301 assert_eq!(
302 sealed.read_at(0, page).await.unwrap().coalesce().as_ref(),
303 &data[..page]
304 );
305 assert!(sealed.try_read_sync_into(&mut probe, 0));
306 assert_eq!(probe, data[..page]);
307 });
308 }
309
310 const PAGE_SIZE: NonZeroU16 = NZU16!(103); const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
312
313 struct WritePause {
314 offset: u64,
315 prefix_len: usize,
316 started: oneshot::Sender<()>,
317 resume: oneshot::Receiver<()>,
318 }
319
320 struct SplitWriteBlob<B> {
322 inner: B,
323 pause: Mutex<Option<WritePause>>,
324 }
325
326 impl<B: Blob> Blob for SplitWriteBlob<B> {
327 async fn read_at_buf(
328 &self,
329 offset: u64,
330 len: usize,
331 bufs: impl Into<IoBufsMut> + Send,
332 options: ReadOptions,
333 ) -> Result<IoBufsMut, Error> {
334 self.inner.read_at_buf(offset, len, bufs, options).await
335 }
336
337 async fn read_at(
338 &self,
339 offset: u64,
340 len: usize,
341 options: ReadOptions,
342 ) -> Result<IoBufsMut, Error> {
343 self.inner.read_at(offset, len, options).await
344 }
345
346 async fn write_at(
347 &self,
348 offset: u64,
349 bufs: impl Into<IoBufs> + Send,
350 options: WriteOptions,
351 ) -> Result<(), Error> {
352 let mut bufs = bufs.into();
353 let pause = self.pause.lock().take();
354 let Some(pause) = pause else {
355 return self.inner.write_at(offset, bufs, options).await;
356 };
357 assert_eq!(offset, pause.offset);
358 assert!(pause.prefix_len < bufs.len());
359 self.inner
360 .write_at(offset, bufs.split_to(pause.prefix_len), options)
361 .await?;
362 pause.started.send(()).unwrap();
363 pause.resume.await.unwrap();
364 self.inner
365 .write_at(offset + Widen::widen(pause.prefix_len), bufs, options)
366 .await
367 }
368
369 async fn resize(&self, len: u64) -> Result<(), Error> {
370 self.inner.resize(len).await
371 }
372
373 async fn sync(&self) -> Result<(), Error> {
374 self.inner.sync().await
375 }
376
377 async fn start_sync(&self) -> Handle<()> {
378 self.inner.start_sync().await
379 }
380 }
381
382 #[test_traced("DEBUG")]
384 fn test_seal_starts_sync() {
385 let executor = deterministic::Runner::default();
386 executor.start(|_: deterministic::Context| async move {
387 let blob = SyncTrackingBlob::new();
388 let blob = Arc::new(blob);
389 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
390 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
391 .await
392 .unwrap();
393
394 let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
396 (append, _) = append.append(&data).await.unwrap();
397
398 let (durable_before, _writes_before, full_before, range_before) = blob.snapshot();
399 assert!(
400 durable_before.is_empty(),
401 "no bytes should be durable before the seal's sync"
402 );
403
404 let (sealed, sync) = append.seal().await.unwrap();
405 sync.await.unwrap();
406
407 let (durable_after, _writes_after, full_after, range_after) = blob.snapshot();
408 assert_eq!(full_after, full_before + 1);
409 assert!(
410 !durable_after.is_empty(),
411 "the seal's sync handle must make the appended bytes durable"
412 );
413 assert_eq!(
414 range_after, range_before,
415 "seal must not invoke a range-scoped write"
416 );
417
418 assert_eq!(sealed.size(), 300);
419 });
420 }
421
422 #[test_traced("DEBUG")]
425 fn test_seal_succeeds_with_readers() {
426 let executor = deterministic::Runner::default();
427 executor.start(|context: deterministic::Context| async move {
428 let (blob, blob_size) = context.open("test_partition", b"readers").await.unwrap();
429 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
430 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
431 .await
432 .unwrap();
433 (writer, _) = writer.append(b"hello world").await.unwrap();
434
435 let (writer, reader) = writer.snapshot().await.unwrap();
437 let reader_clone = reader.clone();
438 assert_eq!(reader.size(), 11);
439
440 let (sealed, sync) = writer.seal().await.unwrap();
442 sync.await.unwrap();
443 assert_eq!(sealed.size(), 11);
444
445 for r in [&reader, &reader_clone] {
447 assert_eq!(r.size(), 11);
448 let via_reader = r.read_at(0, 11).await.unwrap().coalesce();
449 let via_sealed = sealed.read_at(0, 11).await.unwrap().coalesce();
450 assert_eq!(via_reader.as_ref(), b"hello world");
451 assert_eq!(via_sealed.as_ref(), via_reader.as_ref());
452 }
453 });
454 }
455
456 #[test_traced("DEBUG")]
459 fn test_reader_full_pages_after_seal() {
460 let executor = deterministic::Runner::default();
461 executor.start(|context: deterministic::Context| async move {
462 let (blob, blob_size) = context.open("test_partition", b"rdr_pages").await.unwrap();
463
464 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
466 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
467 .await
468 .unwrap();
469
470 let page_size = PAGE_SIZE.get() as usize;
471 let total = page_size * 3 + 7;
472 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
473 (writer, _) = writer.append(&data).await.unwrap();
474
475 let (writer, reader) = writer.snapshot().await.unwrap();
476 let (sealed, sync) = writer.seal().await.unwrap();
477 sync.await.unwrap();
478 assert_eq!(reader.size(), total as u64);
479
480 let cases = [
483 (0u64, total),
484 (page_size as u64 - 3, 6),
485 ((page_size * 3) as u64, 7),
486 ];
487 for (offset, len) in cases {
488 let via_reader = reader.read_at(offset, len).await.unwrap().coalesce();
489 let via_sealed = sealed.read_at(offset, len).await.unwrap().coalesce();
490 assert_eq!(
491 via_reader.as_ref(),
492 &data[offset as usize..offset as usize + len]
493 );
494 assert_eq!(via_sealed.as_ref(), via_reader.as_ref());
495 }
496 });
497 }
498
499 #[test_traced("DEBUG")]
501 fn test_seal_preserves_cache_id() {
502 let executor = deterministic::Runner::default();
503 executor.start(|context: deterministic::Context| async move {
504 let (blob, blob_size) = context.open("test_partition", b"cache_id").await.unwrap();
505 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
506 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
507 .await
508 .unwrap();
509 let append_id = append.cache_id();
510 let (sealed, sync) = append.seal().await.unwrap();
511 sync.await.unwrap();
512 assert_eq!(sealed.cache_id(), append_id);
513 });
514 }
515
516 #[test_traced("DEBUG")]
518 fn test_seal_empty_blob() {
519 let executor = deterministic::Runner::default();
520 executor.start(|context: deterministic::Context| async move {
521 let (blob, blob_size) = context.open("test_partition", b"empty").await.unwrap();
522 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
523 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
524 .await
525 .unwrap();
526 let (sealed, sync) = append.seal().await.unwrap();
527 sync.await.unwrap();
528
529 assert_eq!(sealed.size(), 0);
530
531 let mut buf = [0u8; 1];
533 let err = sealed.read_into(&mut buf, 0).await.unwrap_err();
534 assert!(matches!(err, Error::BlobInsufficientLength));
535 });
536 }
537
538 #[test_traced("DEBUG")]
540 fn test_seal_full_pages_only() {
541 let executor = deterministic::Runner::default();
542 executor.start(|context: deterministic::Context| async move {
543 let (blob, blob_size) = context.open("test_partition", b"full").await.unwrap();
544 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
545 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
546 .await
547 .unwrap();
548
549 let page_size = PAGE_SIZE.get() as usize;
551 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
552 (append, _) = append.append(&data).await.unwrap();
553 let (sealed, sync) = append.seal().await.unwrap();
554 sync.await.unwrap();
555
556 assert_eq!(sealed.size(), data.len() as u64);
557
558 let mut buf = vec![0u8; data.len()];
560 sealed.read_into(&mut buf, 0).await.unwrap();
561 assert_eq!(buf, data);
562 });
563 }
564
565 #[test_traced("DEBUG")]
567 fn test_seal_partial_only() {
568 let executor = deterministic::Runner::default();
569 executor.start(|context: deterministic::Context| async move {
570 let (blob, blob_size) = context.open("test_partition", b"partial").await.unwrap();
571 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
572 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
573 .await
574 .unwrap();
575
576 let data: Vec<u8> = (0u8..=50).collect();
578 (append, _) = append.append(&data).await.unwrap();
579 let (sealed, sync) = append.seal().await.unwrap();
580 sync.await.unwrap();
581
582 assert_eq!(sealed.size(), data.len() as u64);
583
584 let mut buf = vec![0u8; data.len()];
585 sealed.read_into(&mut buf, 0).await.unwrap();
586 assert_eq!(buf, data);
587 });
588 }
589
590 #[test_traced("DEBUG")]
592 fn test_seal_full_plus_partial_straddle() {
593 let executor = deterministic::Runner::default();
594 executor.start(|context: deterministic::Context| async move {
595 let (blob, blob_size) = context.open("test_partition", b"straddle").await.unwrap();
596 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
597 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
598 .await
599 .unwrap();
600
601 let page_size = PAGE_SIZE.get() as usize;
603 let total = page_size + 17;
604 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
605 (append, _) = append.append(&data).await.unwrap();
606 let (sealed, sync) = append.seal().await.unwrap();
607 sync.await.unwrap();
608
609 assert_eq!(sealed.size(), total as u64);
610
611 let off = (page_size - 5) as u64;
613 let len = 15usize;
614 let mut buf = vec![0u8; len];
615 sealed.read_into(&mut buf, off).await.unwrap();
616 assert_eq!(buf, data[page_size - 5..page_size - 5 + len]);
617
618 let off = page_size as u64;
620 let mut buf = vec![0u8; 10];
621 sealed.read_into(&mut buf, off).await.unwrap();
622 assert_eq!(buf, data[page_size..page_size + 10]);
623
624 let mut buf = vec![0u8; 20];
626 sealed.read_into(&mut buf, 0).await.unwrap();
627 assert_eq!(buf, data[..20]);
628 });
629 }
630
631 #[test_traced("DEBUG")]
633 fn test_sealed_read_at() {
634 let executor = deterministic::Runner::default();
635 executor.start(|context: deterministic::Context| async move {
636 let (blob, blob_size) = context.open("test_partition", b"read_at").await.unwrap();
637 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
638 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
639 .await
640 .unwrap();
641
642 let data: Vec<u8> = (0u8..=255).cycle().take(250).collect();
643 (append, _) = append.append(&data).await.unwrap();
644 let (sealed, sync) = append.seal().await.unwrap();
645 sync.await.unwrap();
646
647 let bufs = sealed.read_at(0, data.len()).await.unwrap();
648 let coalesced = bufs.coalesce();
649 assert_eq!(coalesced.as_ref(), data.as_slice());
650 });
651 }
652
653 #[test_traced("DEBUG")]
655 fn test_sealed_read_many_into() {
656 let executor = deterministic::Runner::default();
657 executor.start(|context: deterministic::Context| async move {
658 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
659 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
660 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
661 .await
662 .unwrap();
663
664 let page_size = PAGE_SIZE.get() as usize;
666 let total = page_size + 50;
667 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
668 (append, _) = append.append(&data).await.unwrap();
669 let (sealed, sync) = append.seal().await.unwrap();
670 sync.await.unwrap();
671
672 let offsets = [0u64, (page_size - 2) as u64, (page_size + 10) as u64];
674 let item_size = 4usize;
675 let mut out = vec![0u8; offsets.len() * item_size];
676 sealed
677 .read_many_into(&mut out, &offsets, NZUsize!(item_size))
678 .await
679 .unwrap();
680
681 for (i, &off) in offsets.iter().enumerate() {
682 assert_eq!(
683 &out[i * item_size..(i + 1) * item_size],
684 &data[off as usize..off as usize + item_size],
685 );
686 }
687 });
688 }
689
690 #[test_traced("DEBUG")]
693 fn test_sealed_try_read_many_sync_into() {
694 let executor = deterministic::Runner::default();
695 executor.start(|context: deterministic::Context| async move {
696 let (blob, blob_size) = context.open("test_partition", b"rmany_sync").await.unwrap();
697
698 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
701 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
702 .await
703 .unwrap();
704
705 let page_size = PAGE_SIZE.get() as usize;
707 let total = page_size * 2 + 50;
708 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
709 (append, _) = append.append(&data).await.unwrap();
710 let (sealed, sync) = append.seal().await.unwrap();
711 sync.await.unwrap();
712
713 let offsets = [
716 0u64,
717 (page_size - 2) as u64,
718 (page_size + 2) as u64,
719 (page_size * 2 - 2) as u64,
720 (page_size * 2 + 10) as u64,
721 ];
722 let item_size = 4usize;
723 let check = |out: &[u8], indices: &[usize]| {
724 for &i in indices {
725 let off = offsets[i] as usize;
726 assert_eq!(
727 &out[i * item_size..(i + 1) * item_size],
728 &data[off..off + item_size],
729 );
730 }
731 };
732
733 sealed.read_at(0, page_size).await.unwrap();
736 let mut out = vec![0u8; offsets.len() * item_size];
737 let misses = sealed.try_read_many_sync_into(&mut out, &offsets, NZUsize!(item_size));
738 assert_eq!(misses, vec![1, 2, 3]);
739 check(&out, &[0, 4]);
740
741 sealed.read_at(page_size as u64, page_size).await.unwrap();
743 let mut out = vec![0u8; offsets.len() * item_size];
744 let misses = sealed.try_read_many_sync_into(&mut out, &offsets, NZUsize!(item_size));
745 assert_eq!(misses, vec![0, 1]);
746 check(&out, &[2, 3, 4]);
747 });
748 }
749
750 #[test_traced("DEBUG")]
754 fn test_sealed_try_read_ranges_sync_into() {
755 let executor = deterministic::Runner::default();
756 executor.start(|context: deterministic::Context| async move {
757 let (blob, blob_size) = context
758 .open("test_partition", b"rranges_sync")
759 .await
760 .unwrap();
761
762 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
765 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
766 .await
767 .unwrap();
768
769 let page_size = PAGE_SIZE.get() as usize;
771 let total = page_size * 2 + 50;
772 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
773 (append, _) = append.append(&data).await.unwrap();
774 let (sealed, sync) = append.seal().await.unwrap();
775 sync.await.unwrap();
776
777 let ranges = [
780 (0u64, 3usize),
781 (page_size as u64 + 5, 0),
782 (page_size as u64 + 5, 7),
783 ((page_size * 2 - 2) as u64, 4),
784 ((page_size * 2 + 10) as u64, 4),
785 ];
786 let total_len: usize = ranges.iter().map(|&(_, len)| len).sum();
787 let check = |out: &[u8], indices: &[usize]| {
788 let mut start = 0;
789 for (i, &(off, len)) in ranges.iter().enumerate() {
790 if indices.contains(&i) {
791 let off = off as usize;
792 assert_eq!(&out[start..start + len], &data[off..off + len]);
793 }
794 start += len;
795 }
796 };
797
798 sealed.read_at(0, page_size).await.unwrap();
801 let mut out = vec![0u8; total_len];
802 let misses = sealed.try_read_ranges_sync_into(&mut out, &ranges);
803 assert_eq!(misses, vec![2, 3]);
804 check(&out, &[0, 4]);
805
806 sealed.read_at(page_size as u64, page_size).await.unwrap();
808 let mut out = vec![0u8; total_len];
809 let misses = sealed.try_read_ranges_sync_into(&mut out, &ranges);
810 assert_eq!(misses, vec![0]);
811 check(&out, &[1, 2, 3, 4]);
812 });
813 }
814
815 #[test_traced("DEBUG")]
817 fn test_sealed_read_many_into_cache_miss() {
818 let executor = deterministic::Runner::default();
819 executor.start(|context: deterministic::Context| async move {
820 let (blob, blob_size) = context.open("test_partition", b"rmany_miss").await.unwrap();
821 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
822 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
823 .await
824 .unwrap();
825
826 let page_size = PAGE_SIZE.get() as usize;
827 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
828 (append, _) = append.append(&data).await.unwrap();
829 let (sealed, sync) = append.seal().await.unwrap();
830 sync.await.unwrap();
831
832 let offsets = [0u64, page_size as u64];
833 let item_size = 4usize;
834 let mut out = vec![0u8; offsets.len() * item_size];
835 sealed
836 .read_many_into(&mut out, &offsets, NZUsize!(item_size))
837 .await
838 .unwrap();
839
840 for (i, &off) in offsets.iter().enumerate() {
841 assert_eq!(
842 &out[i * item_size..(i + 1) * item_size],
843 &data[off as usize..off as usize + item_size],
844 );
845 }
846 });
847 }
848
849 #[test_traced("DEBUG")]
851 fn test_sealed_read_ranges_into_reports_misses() {
852 let executor = deterministic::Runner::default();
853 executor.start(|context: deterministic::Context| async move {
854 let (blob, blob_size) = context.open("test_partition", b"ranges").await.unwrap();
855 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
856 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
857 .await
858 .unwrap();
859
860 let page_size = PAGE_SIZE.get() as u64;
861 let data: Vec<u8> = (0u8..=255).cycle().take(page_size as usize * 3).collect();
862 (append, _) = append.append(&data).await.unwrap();
863 let (sealed, sync) = append.seal().await.unwrap();
864 sync.await.unwrap();
865
866 sealed.read_at(page_size, 1).await.unwrap();
868
869 let ranges = [
872 (1, 3),
873 (page_size + 1, 5),
874 (2 * page_size - 1, 4),
875 (2 * page_size + 4, 7),
876 ];
877 let mut out = vec![0u8; ranges.iter().map(|&(_, len)| len).sum()];
878 let served = sealed.read_ranges_into(&mut out, &ranges).await.unwrap();
879 assert_eq!(served, 1);
880
881 let mut slot = 0;
883 for (offset, len) in ranges {
884 let offset = offset as usize;
885 assert_eq!(&out[slot..slot + len], &data[offset..offset + len]);
886 slot += len;
887 }
888 });
889 }
890
891 #[test_traced("DEBUG")]
892 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
893 fn test_sealed_read_many_into_rejects_unsorted_offsets() {
894 let executor = deterministic::Runner::default();
895 executor.start(|context: deterministic::Context| async move {
896 let (blob, blob_size) = context.open("test_partition", b"rmany_bad").await.unwrap();
897 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
898 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
899 .await
900 .unwrap();
901 (append, _) = append.append(&[7; 32]).await.unwrap();
902 let (sealed, sync) = append.seal().await.unwrap();
903 sync.await.unwrap();
904
905 let mut out = vec![0u8; 8];
906 let _ = sealed.read_many_into(&mut out, &[8, 4], NZUsize!(4)).await;
907 });
908 }
909
910 #[test_traced("DEBUG")]
912 fn test_sealed_read_many_into_rejects_invalid_offsets() {
913 let executor = deterministic::Runner::default();
914 executor.start(|context: deterministic::Context| async move {
915 let (blob, blob_size) = context.open("test_partition", b"rmany_bad").await.unwrap();
916 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
917 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
918 .await
919 .unwrap();
920 (append, _) = append.append(&[7; 32]).await.unwrap();
921 let (sealed, sync) = append.seal().await.unwrap();
922 sync.await.unwrap();
923
924 let mut out = vec![0u8; 8];
925 let err = sealed
926 .read_many_into(&mut out, &[u64::MAX - 1, 8], NZUsize!(4))
927 .await
928 .unwrap_err();
929 assert!(matches!(err, Error::OffsetOverflow));
930
931 let err = sealed
932 .read_many_into(&mut out, &[28, 32], NZUsize!(4))
933 .await
934 .unwrap_err();
935 assert!(matches!(err, Error::BlobInsufficientLength));
936 });
937 }
938
939 #[test_traced("DEBUG")]
941 fn test_sealed_try_read_sync_partial() {
942 let executor = deterministic::Runner::default();
943 executor.start(|context: deterministic::Context| async move {
944 let (blob, blob_size) = context
945 .open("test_partition", b"trs_partial")
946 .await
947 .unwrap();
948 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
949 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
950 .await
951 .unwrap();
952
953 let page_size = PAGE_SIZE.get() as usize;
954 let total = page_size + 30;
955 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
956 (append, _) = append.append(&data).await.unwrap();
957 let (sealed, sync) = append.seal().await.unwrap();
958 sync.await.unwrap();
959
960 let mut buf = vec![0u8; 10];
962 assert!(sealed.try_read_sync_into(&mut buf, page_size as u64));
963 assert_eq!(buf, data[page_size..page_size + 10]);
964
965 let mut buf = vec![0u8; 10];
967 assert!(!sealed.try_read_sync_into(&mut buf, total as u64));
968 });
969 }
970
971 #[test_traced("DEBUG")]
973 fn test_sealed_try_read_sync_straddles_cached_and_partial() {
974 let executor = deterministic::Runner::default();
975 executor.start(|context: deterministic::Context| async move {
976 let (blob, blob_size) = context
977 .open("test_partition", b"trs_straddle")
978 .await
979 .unwrap();
980 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
981 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
982 .await
983 .unwrap();
984
985 let page_size = PAGE_SIZE.get() as usize;
986 let total = page_size + 30;
987 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
988 (append, _) = append.append(&data).await.unwrap();
989 let (sealed, sync) = append.seal().await.unwrap();
990 sync.await.unwrap();
991
992 let mut buf = vec![0u8; 12];
993 assert!(sealed.try_read_sync_into(&mut buf, (page_size - 4) as u64));
994 assert_eq!(buf, data[page_size - 4..page_size + 8]);
995 });
996 }
997
998 #[test_traced("DEBUG")]
1000 fn test_sealed_try_read_sync_out_of_bounds() {
1001 let executor = deterministic::Runner::default();
1002 executor.start(|context: deterministic::Context| async move {
1003 let (blob, blob_size) = context.open("test_partition", b"trs_fail").await.unwrap();
1004 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1005 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1006 .await
1007 .unwrap();
1008
1009 let page_size = PAGE_SIZE.get() as usize;
1010 let data: Vec<u8> = (0u8..=255).cycle().take(page_size + 5).collect();
1011 (append, _) = append.append(&data).await.unwrap();
1012 let (sealed, sync) = append.seal().await.unwrap();
1013 sync.await.unwrap();
1014
1015 let mut buf = vec![9u8; 10];
1016 assert!(!sealed.try_read_sync_into(&mut buf, data.len() as u64));
1017 });
1018 }
1019
1020 #[test_traced("DEBUG")]
1022 fn test_sealed_replay() {
1023 let executor = deterministic::Runner::default();
1024 executor.start(|context: deterministic::Context| async move {
1025 let (blob, blob_size) = context.open("test_partition", b"replay").await.unwrap();
1026 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1027 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1028 .await
1029 .unwrap();
1030
1031 let page_size = PAGE_SIZE.get() as usize;
1033 let total = page_size * 2 + 25;
1034 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1035 (append, _) = append.append(&data).await.unwrap();
1036 append = append.sync().await.unwrap();
1037 let (sealed, sync) = append.seal().await.unwrap();
1038 sync.await.unwrap();
1039
1040 let mut replay = sealed
1041 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1042 .unwrap();
1043 assert_eq!(replay.blob_size(), total as u64);
1044
1045 let mut out = Vec::with_capacity(total);
1047 while replay.ensure(1).await.unwrap() {
1048 let chunk = replay.chunk();
1049 let copy_len = chunk.len();
1050 out.extend_from_slice(chunk);
1051 replay.advance(copy_len);
1052 }
1053 assert_eq!(out, data);
1054 });
1055 }
1056
1057 #[test_traced("DEBUG")]
1060 fn test_snapshot_replay_stays_frozen_after_writer_growth() {
1061 let executor = deterministic::Runner::default();
1062 executor.start(|context: deterministic::Context| async move {
1063 let (blob, blob_size) = context
1064 .open("test_partition", b"snapshot_replay_growth")
1065 .await
1066 .unwrap();
1067 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1068 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1069 .await
1070 .unwrap();
1071
1072 let page_size = PAGE_SIZE.get() as usize;
1073 let mut original = vec![0xAA; page_size];
1074 original.extend_from_slice(b"old");
1075 (writer, _) = writer.append(&original).await.unwrap();
1076 writer = writer.sync().await.unwrap();
1077
1078 let (mut writer, snapshot) = writer.snapshot().await.unwrap();
1079 let snapshot_bytes = snapshot
1080 .read_at(0, snapshot.size() as usize)
1081 .await
1082 .unwrap()
1083 .coalesce();
1084 let mut replay = snapshot
1085 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1086 .unwrap();
1087 assert_eq!(replay.blob_size(), original.len() as u64);
1088
1089 (writer, _) = writer.append(b"newtail").await.unwrap();
1090 writer.sync().await.unwrap();
1091
1092 let mut out = Vec::new();
1093 while replay.ensure(1).await.unwrap() {
1094 let chunk = replay.chunk();
1095 let copy_len = chunk.len();
1096 out.extend_from_slice(chunk);
1097 replay.advance(copy_len);
1098 }
1099
1100 assert_eq!(out.as_slice(), snapshot_bytes.as_ref());
1101 assert_eq!(out, original);
1102 });
1103 }
1104
1105 #[rstest::rstest]
1109 #[case::partial(0, 13, false)]
1110 #[case::page_boundary(1, PAGE_SIZE.get() as usize, false)]
1111 #[case::next_page(1, PAGE_SIZE.get() as usize + 7, false)]
1112 #[case::synced_snapshot(1, 13, true)]
1113 fn test_replay_preserves_tail_during_partial_page_rewrite(
1114 #[case] full_pages: usize,
1115 #[case] next_len: usize,
1116 #[case] sync_snapshot: bool,
1117 #[values(false, true)] writer_replay: bool,
1118 ) {
1119 deterministic::Runner::default().start(|context| async move {
1120 const DURABLE_TAIL: usize = 3;
1121 const SNAPSHOT_TAIL: usize = 7;
1122
1123 let page_size = PAGE_SIZE.get() as usize;
1126 let physical_page_size = page_size + CHECKSUM_SIZE as usize;
1127 let offset = Widen::widen(full_pages * physical_page_size);
1128 let (inner, size) = context.open("snapshot-replay-race", b"blob").await.unwrap();
1129 let blob = Arc::new(SplitWriteBlob {
1130 inner,
1131 pause: Mutex::new(None),
1132 });
1133 let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1134 let mut writer = Writer::new(blob.clone(), size, BUFFER_SIZE, cache)
1135 .await
1136 .unwrap();
1137 let mut expected = vec![0xAA; full_pages * page_size + DURABLE_TAIL];
1138 (writer, _) = writer.append(&expected).await.unwrap();
1139 writer = writer.sync().await.unwrap();
1140 let extension = vec![0xBB; SNAPSHOT_TAIL - DURABLE_TAIL];
1141 (writer, _) = writer.append(&extension).await.unwrap();
1142 expected.extend_from_slice(&extension);
1143 let (writer, snapshot) = writer.snapshot().await.unwrap();
1144
1145 let (mut writer, _) = writer
1147 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1148 .await
1149 .unwrap();
1150
1151 if sync_snapshot {
1153 writer = writer.sync().await.unwrap();
1154 }
1155
1156 let page = blob
1159 .read_at(offset, physical_page_size, ReadOptions::default())
1160 .await
1161 .unwrap()
1162 .coalesce();
1163 let active = Checksum::validate_page(page.as_ref()).unwrap();
1164 assert_eq!(active.len as usize, SNAPSHOT_TAIL);
1165 let rewritten_slot = if sync_snapshot {
1166 active.slot.other()
1167 } else {
1168 active.slot
1169 };
1170
1171 let replay_len = expected.len();
1173 let mut replay = if writer_replay {
1174 let replay;
1175 (writer, replay) = writer
1176 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1177 .await
1178 .unwrap();
1179 replay
1180 } else {
1181 snapshot
1182 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1183 .unwrap()
1184 };
1185 assert_eq!(replay.blob_size(), Widen::widen(replay_len));
1186
1187 (writer, _) = writer
1190 .append(&vec![0xCC; next_len - SNAPSHOT_TAIL])
1191 .await
1192 .unwrap();
1193 let (started, entered) = oneshot::channel();
1194 let (resume, released) = oneshot::channel();
1195 *blob.pause.lock() = Some(WritePause {
1196 offset,
1197 prefix_len: page_size + rewritten_slot.offset() + CHECKSUM_SLOT_LEN_SIZE,
1198 started,
1199 resume: released,
1200 });
1201 let mut flushing =
1202 Box::pin(writer.replay(NZUsize!(BUFFER_SIZE), ReadOptions::default()));
1203 commonware_macros::select! {
1204 _ = entered => {},
1205 _ = flushing.as_mut() => panic!("write completed before its suffix was released"),
1206 }
1207
1208 let page = blob
1210 .read_at(offset, physical_page_size, ReadOptions::default())
1211 .await
1212 .unwrap()
1213 .coalesce();
1214 let fallback_len = Checksum::validate_page(page.as_ref()).unwrap().len as usize;
1215 let read_at = snapshot
1216 .read_at(0, expected.len())
1217 .await
1218 .unwrap()
1219 .coalesce();
1220 let replayed = async {
1221 let mut out = Vec::new();
1222 while replay.ensure(1).await? {
1223 let chunk = replay.chunk();
1224 let len = chunk.len();
1225 out.extend_from_slice(chunk);
1226 replay.advance(len);
1227 }
1228 Ok::<_, Error>(out)
1229 }
1230 .await;
1231
1232 resume.send(()).unwrap();
1234 (writer, _) = flushing.await.unwrap();
1235 writer.sync().await.unwrap();
1236 assert_eq!(
1237 fallback_len,
1238 if sync_snapshot {
1239 SNAPSHOT_TAIL
1240 } else {
1241 DURABLE_TAIL
1242 }
1243 );
1244 assert_eq!(read_at.as_ref(), expected);
1245 let replayed = replayed.unwrap();
1246 assert_eq!(
1247 replayed.len(),
1248 replay_len,
1249 "replay shortened the immutable snapshot"
1250 );
1251 assert_eq!(replayed, expected[..replay_len]);
1252 });
1253 }
1254
1255 #[test_traced("DEBUG")]
1258 fn test_seal_replay_without_sync() {
1259 let executor = deterministic::Runner::default();
1260 executor.start(|_: deterministic::Context| async move {
1261 let blob = SyncTrackingBlob::new();
1262 let blob = Arc::new(blob);
1263 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1264 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
1265 .await
1266 .unwrap();
1267
1268 let page_size = PAGE_SIZE.get() as usize;
1269 let total = page_size * 2 + 25;
1270 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1271 (append, _) = append.append(&data).await.unwrap();
1272 let (sealed, sync) = append.seal().await.unwrap();
1273
1274 let (_durable, _writes, full_syncs, range_syncs) = blob.snapshot();
1275 assert_eq!(full_syncs, 1);
1276 assert_eq!(range_syncs, 0);
1277 sync.await.unwrap();
1278
1279 let mut replay = sealed
1280 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1281 .unwrap();
1282 assert_eq!(replay.blob_size(), total as u64);
1283
1284 let mut out = Vec::with_capacity(total);
1285 while replay.ensure(1).await.unwrap() {
1286 let chunk = replay.chunk();
1287 let copy_len = chunk.len();
1288 out.extend_from_slice(chunk);
1289 replay.advance(copy_len);
1290 }
1291 assert_eq!(out, data);
1292 });
1293 }
1294
1295 #[test_traced("DEBUG")]
1298 fn test_sealed_reads_while_seal_sync_pending() {
1299 let executor = deterministic::Runner::default();
1300 executor.start(|_: deterministic::Context| async move {
1301 let inner = SyncTrackingBlob::new();
1302 let (blob, pending) = DelayedSyncBlob::new(inner);
1303 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1304 let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
1305
1306 let page_size = PAGE_SIZE.get() as usize;
1307 let total = page_size * 2 + 50;
1308 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1309 (append, _) = append.append(&data).await.unwrap();
1310
1311 let (sealed, sync) = append.seal().await.unwrap();
1313 assert_eq!(pending.lock().len(), 1, "the seal sync should be parked");
1314
1315 let read = sealed.read_at(0, total).await.unwrap().coalesce();
1316 assert_eq!(read.as_ref(), &data[..]);
1317
1318 let mut replay = sealed
1319 .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1320 .unwrap();
1321 assert_eq!(replay.blob_size(), total as u64);
1322 let mut replayed = Vec::new();
1323 while replay.ensure(1).await.unwrap() {
1324 let n = {
1325 let chunk = replay.chunk();
1326 replayed.extend_from_slice(chunk);
1327 chunk.len()
1328 };
1329 replay.advance(n);
1330 }
1331 assert_eq!(replayed, data);
1332 assert_eq!(
1333 pending.lock().len(),
1334 1,
1335 "reads must not consume the pending sync"
1336 );
1337
1338 next_pending_sync(&pending).release.send(Ok(())).unwrap();
1340 sync.await.unwrap();
1341 });
1342 }
1343
1344 #[test_traced("DEBUG")]
1346 fn test_seal_recovered_synced_partial_page_no_write() {
1347 let executor = deterministic::Runner::default();
1348 executor.start(|_: deterministic::Context| async move {
1349 let blob = SyncTrackingBlob::new();
1350 let blob = Arc::new(blob);
1351 let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1352 let data: Vec<u8> = (0u8..=255)
1353 .cycle()
1354 .take(PAGE_SIZE.get() as usize - 17)
1355 .collect();
1356
1357 {
1358 let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref.clone())
1359 .await
1360 .unwrap();
1361 (writer, _) = writer.append(&data).await.unwrap();
1362 writer.sync().await.unwrap();
1363 }
1364
1365 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
1366 let mut recovered = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
1367 .await
1368 .unwrap();
1369 assert_eq!(recovered.size(), data.len() as u64);
1370
1371 recovered = recovered.sync().await.unwrap();
1372 let (_, writes_after_sync, full_after_sync, range_after_sync) = blob.snapshot();
1373 assert_eq!(
1374 writes_after_sync, writes,
1375 "syncing an unchanged recovered partial page must not rewrite it"
1376 );
1377 assert_eq!(full_after_sync, full_syncs + 1);
1378 assert_eq!(range_after_sync, range_syncs);
1379
1380 let (sealed, sync) = recovered.seal().await.unwrap();
1381 sync.await.unwrap();
1382 let (_, writes_after_seal, full_after_seal, range_after_seal) = blob.snapshot();
1383 assert_eq!(
1384 writes_after_seal, writes_after_sync,
1385 "sealing an unchanged recovered partial page must not rewrite it"
1386 );
1387 assert_eq!(full_after_seal, full_after_sync);
1388 assert_eq!(range_after_seal, range_after_sync);
1389
1390 let read = sealed.read_at(0, data.len()).await.unwrap().coalesce();
1391 assert_eq!(read.as_ref(), data.as_slice());
1392 });
1393 }
1394}