1use super::{read::PageReader, view::View, CacheRef, Replay, CHECKSUM_SIZE};
19use crate::{Blob, Error, IoBuf, IoBufMut, IoBufs};
20use std::{
21 num::{NonZeroU16, NonZeroUsize},
22 sync::Arc,
23};
24
25pub struct Sealed<B: Blob> {
28 inner: Arc<SealedInner<B>>,
29}
30
31impl<B: Blob> Clone for Sealed<B> {
32 fn clone(&self) -> Self {
33 Self {
34 inner: self.inner.clone(),
35 }
36 }
37}
38
39struct SealedInner<B: Blob> {
40 blob: B,
42
43 size: u64,
45
46 partial_page: Option<IoBuf>,
50
51 cache_ref: CacheRef,
53
54 id: u64,
58}
59
60impl<B: Blob> Sealed<B> {
61 pub(super) fn new(
63 blob: B,
64 size: u64,
65 partial_page: Option<IoBuf>,
66 cache_ref: CacheRef,
67 id: u64,
68 ) -> Self {
69 Self {
70 inner: Arc::new(SealedInner {
71 blob,
72 size,
73 partial_page,
74 cache_ref,
75 id,
76 }),
77 }
78 }
79
80 pub fn size(&self) -> u64 {
82 self.inner.size
83 }
84
85 pub async fn sync(&self) -> Result<(), Error> {
87 self.inner.blob.sync().await
88 }
89
90 fn partial_offset(&self) -> u64 {
93 self.inner.size
94 - self
95 .inner
96 .partial_page
97 .as_ref()
98 .map_or(0, |p| p.len() as u64)
99 }
100
101 fn view(&self) -> View<'_, B> {
103 View {
104 blob: &self.inner.blob,
105 cache_ref: &self.inner.cache_ref,
106 id: self.inner.id,
107 size: self.inner.size,
108 tail_offset: self.partial_offset(),
109 tail: self
110 .inner
111 .partial_page
112 .as_ref()
113 .map_or(&[][..], |p| p.as_ref()),
114 }
115 }
116
117 pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
119 self.view().read_at(offset, len).await
120 }
121
122 pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
126 self.view().try_read_sync_into(buf, offset)
127 }
128
129 pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
131 self.view().read_into(buf, offset).await
132 }
133
134 pub async fn read_up_to(
139 &self,
140 offset: u64,
141 len: usize,
142 bufs: impl Into<IoBufMut> + Send,
143 ) -> Result<(IoBufMut, usize), Error> {
144 self.view().read_up_to(offset, len, bufs).await
145 }
146
147 pub async fn read_many_into(
155 &self,
156 buf: &mut [u8],
157 offsets: &[u64],
158 item_size: NonZeroUsize,
159 ) -> Result<usize, Error> {
160 self.view().read_many_into(buf, offsets, item_size).await
161 }
162
163 pub fn try_read_many_sync_into(
166 &self,
167 buf: &mut [u8],
168 offsets: &[u64],
169 item_size: NonZeroUsize,
170 ) -> Vec<usize> {
171 self.view().try_read_many_sync_into(buf, offsets, item_size)
172 }
173
174 pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
177 self.view().try_read_ranges_sync_into(buf, ranges)
178 }
179
180 pub fn replay(&self, buffer_size: NonZeroUsize) -> Result<Replay<B>, Error> {
185 let logical_page_size = self.inner.cache_ref.page_size();
186 let logical_page_size_nz =
187 NonZeroU16::new(logical_page_size as u16).expect("page_size is non-zero");
188 let physical_page_size = logical_page_size
189 .checked_add(CHECKSUM_SIZE)
190 .ok_or(Error::OffsetOverflow)?;
191 let prefetch_pages = (buffer_size.get() / physical_page_size as usize).max(1);
192
193 let partial_len = self
194 .inner
195 .partial_page
196 .as_ref()
197 .map_or(0, |p| p.len() as u64);
198 let full_pages = (self.inner.size - partial_len) / logical_page_size;
199 let pages = full_pages + u64::from(partial_len > 0);
200 let physical_blob_size = physical_page_size
201 .checked_mul(pages)
202 .ok_or(Error::OffsetOverflow)?;
203 let logical_blob_size = self.inner.size;
204
205 let reader = PageReader::new(
206 self.inner.blob.clone(),
207 physical_blob_size,
208 logical_blob_size,
209 prefetch_pages,
210 logical_page_size_nz,
211 );
212 Ok(Replay::new(reader))
213 }
214
215 #[cfg(test)]
218 pub(super) fn cache_id(&self) -> u64 {
219 self.inner.id
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::{
227 buffer::{paged::Writer, tests::SyncTrackingBlob},
228 deterministic, Buf, Runner as _, Storage as _,
229 };
230 use commonware_macros::test_traced;
231 use commonware_utils::{NZUsize, NZU16};
232
233 const PAGE_SIZE: NonZeroU16 = NZU16!(103); const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
235
236 #[test_traced("DEBUG")]
238 fn test_seal_no_fsync() {
239 let executor = deterministic::Runner::default();
240 executor.start(|context: deterministic::Context| async move {
241 let blob = SyncTrackingBlob::new();
242 let cache_ref =
243 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
244 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
245 .await
246 .unwrap();
247
248 let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
250 append.append(&data).await.unwrap();
251
252 let (_durable_before, _writes_before, full_before, range_before) = blob.snapshot();
253
254 let sealed = append.seal().await.unwrap();
256
257 let (_durable_after, _writes_after, full_after, range_after) = blob.snapshot();
258 assert_eq!(full_after, full_before, "seal must not invoke Blob::sync");
259 assert_eq!(
260 range_after, range_before,
261 "seal must not invoke Blob::write_at_sync"
262 );
263
264 assert_eq!(sealed.size(), 300);
265 });
266 }
267
268 #[test_traced("DEBUG")]
271 fn test_seal_succeeds_with_readers() {
272 let executor = deterministic::Runner::default();
273 executor.start(|context: deterministic::Context| async move {
274 let (blob, blob_size) = context.open("test_partition", b"readers").await.unwrap();
275 let cache_ref =
276 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
277 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
278 .await
279 .unwrap();
280 writer.append(b"hello world").await.unwrap();
281
282 let reader = writer.snapshot().await.unwrap();
284 let reader_clone = reader.clone();
285 assert_eq!(reader.size(), 11);
286
287 let sealed = writer.seal().await.unwrap();
289 assert_eq!(sealed.size(), 11);
290
291 for r in [&reader, &reader_clone] {
293 assert_eq!(r.size(), 11);
294 let via_reader = r.read_at(0, 11).await.unwrap().coalesce();
295 let via_sealed = sealed.read_at(0, 11).await.unwrap().coalesce();
296 assert_eq!(via_reader.as_ref(), b"hello world");
297 assert_eq!(via_sealed.as_ref(), via_reader.as_ref());
298 }
299 });
300 }
301
302 #[test_traced("DEBUG")]
305 fn test_reader_full_pages_after_seal() {
306 let executor = deterministic::Runner::default();
307 executor.start(|context: deterministic::Context| async move {
308 let (blob, blob_size) = context.open("test_partition", b"rdr_pages").await.unwrap();
309 let cache_ref = super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
311 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
312 .await
313 .unwrap();
314
315 let page_size = PAGE_SIZE.get() as usize;
316 let total = page_size * 3 + 7;
317 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
318 writer.append(&data).await.unwrap();
319
320 let reader = writer.snapshot().await.unwrap();
321 let sealed = writer.seal().await.unwrap();
322 assert_eq!(reader.size(), total as u64);
323
324 let cases = [
327 (0u64, total),
328 (page_size as u64 - 3, 6),
329 ((page_size * 3) as u64, 7),
330 ];
331 for (offset, len) in cases {
332 let via_reader = reader.read_at(offset, len).await.unwrap().coalesce();
333 let via_sealed = sealed.read_at(offset, len).await.unwrap().coalesce();
334 assert_eq!(
335 via_reader.as_ref(),
336 &data[offset as usize..offset as usize + len]
337 );
338 assert_eq!(via_sealed.as_ref(), via_reader.as_ref());
339 }
340 });
341 }
342
343 #[test_traced("DEBUG")]
345 fn test_sealed_sync_makes_blob_durable() {
346 let executor = deterministic::Runner::default();
347 executor.start(|context: deterministic::Context| async move {
348 let blob = SyncTrackingBlob::new();
349 let cache_ref =
350 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
351 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
352 .await
353 .unwrap();
354
355 let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
357 append.append(&data).await.unwrap();
358 let sealed = append.seal().await.unwrap();
359
360 let (durable_before, _, full_before, _) = blob.snapshot();
361 assert!(
362 durable_before.is_empty(),
363 "no bytes should be durable before Sealed::sync"
364 );
365
366 sealed.sync().await.unwrap();
367
368 let (durable_after, _, full_after, _) = blob.snapshot();
369 assert_eq!(
370 full_after,
371 full_before + 1,
372 "Sealed::sync must invoke Blob::sync exactly once"
373 );
374 assert!(
375 !durable_after.is_empty(),
376 "blob bytes must be durable after Sealed::sync"
377 );
378 });
379 }
380
381 #[test_traced("DEBUG")]
383 fn test_seal_preserves_cache_id() {
384 let executor = deterministic::Runner::default();
385 executor.start(|context: deterministic::Context| async move {
386 let (blob, blob_size) = context.open("test_partition", b"cache_id").await.unwrap();
387 let cache_ref =
388 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
389 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
390 .await
391 .unwrap();
392 let append_id = append.cache_id();
393 let sealed = append.seal().await.unwrap();
394 assert_eq!(sealed.cache_id(), append_id);
395 });
396 }
397
398 #[test_traced("DEBUG")]
400 fn test_seal_empty_blob() {
401 let executor = deterministic::Runner::default();
402 executor.start(|context: deterministic::Context| async move {
403 let (blob, blob_size) = context.open("test_partition", b"empty").await.unwrap();
404 let cache_ref =
405 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
406 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
407 .await
408 .unwrap();
409 let sealed = append.seal().await.unwrap();
410
411 assert_eq!(sealed.size(), 0);
412
413 let mut buf = [0u8; 1];
415 let err = sealed.read_into(&mut buf, 0).await.unwrap_err();
416 assert!(matches!(err, Error::BlobInsufficientLength));
417 });
418 }
419
420 #[test_traced("DEBUG")]
422 fn test_seal_full_pages_only() {
423 let executor = deterministic::Runner::default();
424 executor.start(|context: deterministic::Context| async move {
425 let (blob, blob_size) = context.open("test_partition", b"full").await.unwrap();
426 let cache_ref =
427 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
428 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
429 .await
430 .unwrap();
431
432 let page_size = PAGE_SIZE.get() as usize;
434 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
435 append.append(&data).await.unwrap();
436 let sealed = append.seal().await.unwrap();
437
438 assert_eq!(sealed.size(), data.len() as u64);
439
440 let mut buf = vec![0u8; data.len()];
442 sealed.read_into(&mut buf, 0).await.unwrap();
443 assert_eq!(buf, data);
444 });
445 }
446
447 #[test_traced("DEBUG")]
449 fn test_seal_partial_only() {
450 let executor = deterministic::Runner::default();
451 executor.start(|context: deterministic::Context| async move {
452 let (blob, blob_size) = context.open("test_partition", b"partial").await.unwrap();
453 let cache_ref =
454 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
455 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
456 .await
457 .unwrap();
458
459 let data: Vec<u8> = (0u8..=50).collect();
461 append.append(&data).await.unwrap();
462 let sealed = append.seal().await.unwrap();
463
464 assert_eq!(sealed.size(), data.len() as u64);
465
466 let mut buf = vec![0u8; data.len()];
467 sealed.read_into(&mut buf, 0).await.unwrap();
468 assert_eq!(buf, data);
469 });
470 }
471
472 #[test_traced("DEBUG")]
474 fn test_seal_full_plus_partial_straddle() {
475 let executor = deterministic::Runner::default();
476 executor.start(|context: deterministic::Context| async move {
477 let (blob, blob_size) = context.open("test_partition", b"straddle").await.unwrap();
478 let cache_ref =
479 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
480 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
481 .await
482 .unwrap();
483
484 let page_size = PAGE_SIZE.get() as usize;
486 let total = page_size + 17;
487 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
488 append.append(&data).await.unwrap();
489 let sealed = append.seal().await.unwrap();
490
491 assert_eq!(sealed.size(), total as u64);
492
493 let off = (page_size - 5) as u64;
495 let len = 15usize;
496 let mut buf = vec![0u8; len];
497 sealed.read_into(&mut buf, off).await.unwrap();
498 assert_eq!(buf, data[page_size - 5..page_size - 5 + len]);
499
500 let off = page_size as u64;
502 let mut buf = vec![0u8; 10];
503 sealed.read_into(&mut buf, off).await.unwrap();
504 assert_eq!(buf, data[page_size..page_size + 10]);
505
506 let mut buf = vec![0u8; 20];
508 sealed.read_into(&mut buf, 0).await.unwrap();
509 assert_eq!(buf, data[..20]);
510 });
511 }
512
513 #[test_traced("DEBUG")]
515 fn test_sealed_read_at() {
516 let executor = deterministic::Runner::default();
517 executor.start(|context: deterministic::Context| async move {
518 let (blob, blob_size) = context.open("test_partition", b"read_at").await.unwrap();
519 let cache_ref =
520 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
521 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
522 .await
523 .unwrap();
524
525 let data: Vec<u8> = (0u8..=255).cycle().take(250).collect();
526 append.append(&data).await.unwrap();
527 let sealed = append.seal().await.unwrap();
528
529 let bufs = sealed.read_at(0, data.len()).await.unwrap();
530 let coalesced = bufs.coalesce();
531 assert_eq!(coalesced.as_ref(), data.as_slice());
532 });
533 }
534
535 #[test_traced("DEBUG")]
537 fn test_sealed_read_many_into() {
538 let executor = deterministic::Runner::default();
539 executor.start(|context: deterministic::Context| async move {
540 let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
541 let cache_ref =
542 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
543 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
544 .await
545 .unwrap();
546
547 let page_size = PAGE_SIZE.get() as usize;
549 let total = page_size + 50;
550 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
551 append.append(&data).await.unwrap();
552 let sealed = append.seal().await.unwrap();
553
554 let offsets = [0u64, (page_size - 2) as u64, (page_size + 10) as u64];
556 let item_size = 4usize;
557 let mut out = vec![0u8; offsets.len() * item_size];
558 sealed
559 .read_many_into(&mut out, &offsets, NZUsize!(item_size))
560 .await
561 .unwrap();
562
563 for (i, &off) in offsets.iter().enumerate() {
564 assert_eq!(
565 &out[i * item_size..(i + 1) * item_size],
566 &data[off as usize..off as usize + item_size],
567 );
568 }
569 });
570 }
571
572 #[test_traced("DEBUG")]
575 fn test_sealed_try_read_many_sync_into() {
576 let executor = deterministic::Runner::default();
577 executor.start(|context: deterministic::Context| async move {
578 let (blob, blob_size) = context.open("test_partition", b"rmany_sync").await.unwrap();
579 let cache_ref = super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
582 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
583 .await
584 .unwrap();
585
586 let page_size = PAGE_SIZE.get() as usize;
588 let total = page_size * 2 + 50;
589 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
590 append.append(&data).await.unwrap();
591 let sealed = append.seal().await.unwrap();
592
593 let offsets = [
595 0u64,
596 page_size as u64,
597 (page_size * 2 - 2) as u64,
598 (page_size * 2 + 10) as u64,
599 ];
600 let item_size = 4usize;
601 let check = |out: &[u8], indices: &[usize]| {
602 for &i in indices {
603 let off = offsets[i] as usize;
604 assert_eq!(
605 &out[i * item_size..(i + 1) * item_size],
606 &data[off..off + item_size],
607 );
608 }
609 };
610
611 sealed.read_at(0, page_size).await.unwrap();
614 let mut out = vec![0u8; offsets.len() * item_size];
615 let misses = sealed.try_read_many_sync_into(&mut out, &offsets, NZUsize!(item_size));
616 assert_eq!(misses, vec![1, 2]);
617 check(&out, &[0, 3]);
618
619 sealed.read_at(page_size as u64, page_size).await.unwrap();
621 let mut out = vec![0u8; offsets.len() * item_size];
622 let misses = sealed.try_read_many_sync_into(&mut out, &offsets, NZUsize!(item_size));
623 assert_eq!(misses, vec![0]);
624 check(&out, &[1, 2, 3]);
625 });
626 }
627
628 #[test_traced("DEBUG")]
632 fn test_sealed_try_read_ranges_sync_into() {
633 let executor = deterministic::Runner::default();
634 executor.start(|context: deterministic::Context| async move {
635 let (blob, blob_size) = context
636 .open("test_partition", b"rranges_sync")
637 .await
638 .unwrap();
639 let cache_ref = super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
642 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
643 .await
644 .unwrap();
645
646 let page_size = PAGE_SIZE.get() as usize;
648 let total = page_size * 2 + 50;
649 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
650 append.append(&data).await.unwrap();
651 let sealed = append.seal().await.unwrap();
652
653 let ranges = [
656 (0u64, 3usize),
657 (page_size as u64 + 5, 0),
658 (page_size as u64 + 5, 7),
659 ((page_size * 2 - 2) as u64, 4),
660 ((page_size * 2 + 10) as u64, 4),
661 ];
662 let total_len: usize = ranges.iter().map(|&(_, len)| len).sum();
663 let check = |out: &[u8], indices: &[usize]| {
664 let mut start = 0;
665 for (i, &(off, len)) in ranges.iter().enumerate() {
666 if indices.contains(&i) {
667 let off = off as usize;
668 assert_eq!(&out[start..start + len], &data[off..off + len]);
669 }
670 start += len;
671 }
672 };
673
674 sealed.read_at(0, page_size).await.unwrap();
677 let mut out = vec![0u8; total_len];
678 let misses = sealed.try_read_ranges_sync_into(&mut out, &ranges);
679 assert_eq!(misses, vec![2, 3]);
680 check(&out, &[0, 4]);
681
682 sealed.read_at(page_size as u64, page_size).await.unwrap();
684 let mut out = vec![0u8; total_len];
685 let misses = sealed.try_read_ranges_sync_into(&mut out, &ranges);
686 assert_eq!(misses, vec![0]);
687 check(&out, &[1, 2, 3, 4]);
688 });
689 }
690
691 #[test_traced("DEBUG")]
693 fn test_sealed_read_many_into_cache_miss() {
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_miss").await.unwrap();
697 let cache_ref = super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
698 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
699 .await
700 .unwrap();
701
702 let page_size = PAGE_SIZE.get() as usize;
703 let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
704 append.append(&data).await.unwrap();
705 let sealed = append.seal().await.unwrap();
706
707 let offsets = [0u64, page_size as u64];
708 let item_size = 4usize;
709 let mut out = vec![0u8; offsets.len() * item_size];
710 sealed
711 .read_many_into(&mut out, &offsets, NZUsize!(item_size))
712 .await
713 .unwrap();
714
715 for (i, &off) in offsets.iter().enumerate() {
716 assert_eq!(
717 &out[i * item_size..(i + 1) * item_size],
718 &data[off as usize..off as usize + item_size],
719 );
720 }
721 });
722 }
723
724 #[test_traced("DEBUG")]
725 #[should_panic(expected = "ranges must be sorted and non-overlapping")]
726 fn test_sealed_read_many_into_rejects_unsorted_offsets() {
727 let executor = deterministic::Runner::default();
728 executor.start(|context: deterministic::Context| async move {
729 let (blob, blob_size) = context.open("test_partition", b"rmany_bad").await.unwrap();
730 let cache_ref =
731 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
732 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
733 .await
734 .unwrap();
735 append.append(&[7; 32]).await.unwrap();
736 let sealed = append.seal().await.unwrap();
737
738 let mut out = vec![0u8; 8];
739 let _ = sealed.read_many_into(&mut out, &[8, 4], NZUsize!(4)).await;
740 });
741 }
742
743 #[test_traced("DEBUG")]
745 fn test_sealed_read_many_into_rejects_invalid_offsets() {
746 let executor = deterministic::Runner::default();
747 executor.start(|context: deterministic::Context| async move {
748 let (blob, blob_size) = context.open("test_partition", b"rmany_bad").await.unwrap();
749 let cache_ref =
750 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
751 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
752 .await
753 .unwrap();
754 append.append(&[7; 32]).await.unwrap();
755 let sealed = append.seal().await.unwrap();
756
757 let mut out = vec![0u8; 8];
758 let err = sealed
759 .read_many_into(&mut out, &[u64::MAX - 1, 8], NZUsize!(4))
760 .await
761 .unwrap_err();
762 assert!(matches!(err, Error::OffsetOverflow));
763
764 let err = sealed
765 .read_many_into(&mut out, &[28, 32], NZUsize!(4))
766 .await
767 .unwrap_err();
768 assert!(matches!(err, Error::BlobInsufficientLength));
769 });
770 }
771
772 #[test_traced("DEBUG")]
774 fn test_sealed_try_read_sync_partial() {
775 let executor = deterministic::Runner::default();
776 executor.start(|context: deterministic::Context| async move {
777 let (blob, blob_size) = context
778 .open("test_partition", b"trs_partial")
779 .await
780 .unwrap();
781 let cache_ref =
782 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
783 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
784 .await
785 .unwrap();
786
787 let page_size = PAGE_SIZE.get() as usize;
788 let total = page_size + 30;
789 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
790 append.append(&data).await.unwrap();
791 let sealed = append.seal().await.unwrap();
792
793 let mut buf = vec![0u8; 10];
795 assert!(sealed.try_read_sync_into(&mut buf, page_size as u64));
796 assert_eq!(buf, data[page_size..page_size + 10]);
797
798 let mut buf = vec![0u8; 10];
800 assert!(!sealed.try_read_sync_into(&mut buf, total as u64));
801 });
802 }
803
804 #[test_traced("DEBUG")]
806 fn test_sealed_try_read_sync_straddles_cached_and_partial() {
807 let executor = deterministic::Runner::default();
808 executor.start(|context: deterministic::Context| async move {
809 let (blob, blob_size) = context
810 .open("test_partition", b"trs_straddle")
811 .await
812 .unwrap();
813 let cache_ref =
814 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
815 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
816 .await
817 .unwrap();
818
819 let page_size = PAGE_SIZE.get() as usize;
820 let total = page_size + 30;
821 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
822 append.append(&data).await.unwrap();
823 let sealed = append.seal().await.unwrap();
824
825 let mut buf = vec![0u8; 12];
826 assert!(sealed.try_read_sync_into(&mut buf, (page_size - 4) as u64));
827 assert_eq!(buf, data[page_size - 4..page_size + 8]);
828 });
829 }
830
831 #[test_traced("DEBUG")]
833 fn test_sealed_try_read_sync_out_of_bounds() {
834 let executor = deterministic::Runner::default();
835 executor.start(|context: deterministic::Context| async move {
836 let (blob, blob_size) = context.open("test_partition", b"trs_fail").await.unwrap();
837 let cache_ref =
838 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
839 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
840 .await
841 .unwrap();
842
843 let page_size = PAGE_SIZE.get() as usize;
844 let data: Vec<u8> = (0u8..=255).cycle().take(page_size + 5).collect();
845 append.append(&data).await.unwrap();
846 let sealed = append.seal().await.unwrap();
847
848 let mut buf = vec![9u8; 10];
849 assert!(!sealed.try_read_sync_into(&mut buf, data.len() as u64));
850 });
851 }
852
853 #[test_traced("DEBUG")]
855 fn test_sealed_replay() {
856 let executor = deterministic::Runner::default();
857 executor.start(|context: deterministic::Context| async move {
858 let (blob, blob_size) = context.open("test_partition", b"replay").await.unwrap();
859 let cache_ref =
860 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
861 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
862 .await
863 .unwrap();
864
865 let page_size = PAGE_SIZE.get() as usize;
867 let total = page_size * 2 + 25;
868 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
869 append.append(&data).await.unwrap();
870 append.sync().await.unwrap();
871 let sealed = append.seal().await.unwrap();
872
873 let mut replay = sealed.replay(NZUsize!(BUFFER_SIZE)).unwrap();
874 assert_eq!(replay.blob_size(), total as u64);
875
876 let mut out = Vec::with_capacity(total);
878 while replay.ensure(1).await.unwrap() {
879 let chunk = replay.chunk();
880 let copy_len = chunk.len();
881 out.extend_from_slice(chunk);
882 replay.advance(copy_len);
883 }
884 assert_eq!(out, data);
885 });
886 }
887
888 #[test_traced("DEBUG")]
891 fn test_snapshot_replay_stays_frozen_after_writer_growth() {
892 let executor = deterministic::Runner::default();
893 executor.start(|context: deterministic::Context| async move {
894 let (blob, blob_size) = context
895 .open("test_partition", b"snapshot_replay_growth")
896 .await
897 .unwrap();
898 let cache_ref =
899 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
900 let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
901 .await
902 .unwrap();
903
904 let page_size = PAGE_SIZE.get() as usize;
905 let mut original = vec![0xAA; page_size];
906 original.extend_from_slice(b"old");
907 writer.append(&original).await.unwrap();
908 writer.sync().await.unwrap();
909
910 let snapshot = writer.snapshot().await.unwrap();
911 let snapshot_bytes = snapshot
912 .read_at(0, snapshot.size() as usize)
913 .await
914 .unwrap()
915 .coalesce();
916 let mut replay = snapshot.replay(NZUsize!(BUFFER_SIZE)).unwrap();
917 assert_eq!(replay.blob_size(), original.len() as u64);
918
919 writer.append(b"newtail").await.unwrap();
920 writer.sync().await.unwrap();
921
922 let mut out = Vec::new();
923 while replay.ensure(1).await.unwrap() {
924 let chunk = replay.chunk();
925 let copy_len = chunk.len();
926 out.extend_from_slice(chunk);
927 replay.advance(copy_len);
928 }
929
930 assert_eq!(out.as_slice(), snapshot_bytes.as_ref());
931 assert_eq!(out, original);
932 });
933 }
934
935 #[test_traced("DEBUG")]
938 fn test_seal_replay_without_sync() {
939 let executor = deterministic::Runner::default();
940 executor.start(|context: deterministic::Context| async move {
941 let blob = SyncTrackingBlob::new();
942 let cache_ref =
943 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
944 let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
945 .await
946 .unwrap();
947
948 let page_size = PAGE_SIZE.get() as usize;
949 let total = page_size * 2 + 25;
950 let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
951 append.append(&data).await.unwrap();
952 let sealed = append.seal().await.unwrap();
954
955 let (_durable, _writes, full_syncs, range_syncs) = blob.snapshot();
957 assert_eq!(full_syncs, 0, "seal must not invoke Blob::sync");
958 assert_eq!(range_syncs, 0, "seal must not invoke Blob::write_at_sync");
959
960 let mut replay = sealed.replay(NZUsize!(BUFFER_SIZE)).unwrap();
962 assert_eq!(replay.blob_size(), total as u64);
963
964 let mut out = Vec::with_capacity(total);
965 while replay.ensure(1).await.unwrap() {
966 let chunk = replay.chunk();
967 let copy_len = chunk.len();
968 out.extend_from_slice(chunk);
969 replay.advance(copy_len);
970 }
971 assert_eq!(out, data);
972 });
973 }
974
975 #[test_traced("DEBUG")]
977 fn test_sealed_sync_reopens() {
978 let executor = deterministic::Runner::default();
979 executor.start(|context: deterministic::Context| async move {
980 let cache_ref =
981 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
982 let data: Vec<u8> = (0u8..=255)
983 .cycle()
984 .take(PAGE_SIZE.get() as usize + 17)
985 .collect();
986 {
987 let (blob, blob_size) = context.open("test_partition", b"reopen").await.unwrap();
988 let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
989 .await
990 .unwrap();
991 append.append(&data).await.unwrap();
992 let sealed = append.seal().await.unwrap();
993 sealed.sync().await.unwrap();
994 }
995
996 let (blob, blob_size) = context.open("test_partition", b"reopen").await.unwrap();
997 let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
998 .await
999 .unwrap();
1000 let mut buf = vec![0; data.len()];
1001 append.read_into(&mut buf, 0).await.unwrap();
1002 assert_eq!(buf, data);
1003 });
1004 }
1005
1006 #[test_traced("DEBUG")]
1008 fn test_seal_recovered_synced_partial_page_no_write() {
1009 let executor = deterministic::Runner::default();
1010 executor.start(|context: deterministic::Context| async move {
1011 let blob = SyncTrackingBlob::new();
1012 let cache_ref =
1013 super::CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1014 let data: Vec<u8> = (0u8..=255)
1015 .cycle()
1016 .take(PAGE_SIZE.get() as usize - 17)
1017 .collect();
1018
1019 {
1020 let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref.clone())
1021 .await
1022 .unwrap();
1023 writer.append(&data).await.unwrap();
1024 writer.sync().await.unwrap();
1025 }
1026
1027 let (_, writes, full_syncs, range_syncs) = blob.snapshot();
1028 let mut recovered = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
1029 .await
1030 .unwrap();
1031 assert_eq!(recovered.size(), data.len() as u64);
1032
1033 recovered.sync().await.unwrap();
1034 let (_, writes_after_sync, full_after_sync, range_after_sync) = blob.snapshot();
1035 assert_eq!(
1036 writes_after_sync, writes,
1037 "syncing an unchanged recovered partial page must not rewrite it"
1038 );
1039 assert_eq!(full_after_sync, full_syncs + 1);
1040 assert_eq!(range_after_sync, range_syncs);
1041
1042 let sealed = recovered.seal().await.unwrap();
1043 let (_, writes_after_seal, full_after_seal, range_after_seal) = blob.snapshot();
1044 assert_eq!(
1045 writes_after_seal, writes_after_sync,
1046 "sealing an unchanged recovered partial page must not rewrite it"
1047 );
1048 assert_eq!(full_after_seal, full_after_sync);
1049 assert_eq!(range_after_seal, range_after_sync);
1050
1051 let read = sealed.read_at(0, data.len()).await.unwrap().coalesce();
1052 assert_eq!(read.as_ref(), data.as_slice());
1053 });
1054 }
1055}