1use super::{CHECKSUM_SIZE, STORAGE_PAGE_SIZE, get_page_from_blob};
5use crate::{Blob, BufferPool, BufferPooler, Error, IoBuf, IoBufMut, ReadOptions};
6use ahash::AHashMap;
7use commonware_utils::{Widen, cache::Clock, sync::RwLock};
8use futures::{FutureExt, future::Shared};
9use std::{
10 collections::hash_map::Entry,
11 future::Future,
12 num::{NonZeroU16, NonZeroUsize},
13 pin::Pin,
14 sync::{
15 Arc,
16 atomic::{AtomicU64, Ordering},
17 },
18};
19use tracing::{debug, error, trace};
20
21type PageFetchFuture = Shared<Pin<Box<dyn Future<Output = Result<IoBuf, Arc<Error>>> + Send>>>;
24
25type PageFetch = Arc<PageFetchFuture>;
28
29struct PageFetchEntry {
37 fetch: PageFetch,
39 waiters: usize,
41}
42
43struct PageFetchGuard {
45 cache: Arc<RwLock<Cache>>,
46 key: (u64, u64),
47 fetch: PageFetch,
48 armed: bool,
49}
50
51impl PageFetchGuard {
52 const fn new(cache: Arc<RwLock<Cache>>, key: (u64, u64), fetch: PageFetch) -> Self {
53 Self {
54 cache,
55 key,
56 fetch,
57 armed: true,
58 }
59 }
60
61 const fn disarm(&mut self) {
62 self.armed = false;
63 }
64}
65
66impl Drop for PageFetchGuard {
67 fn drop(&mut self) {
68 if !self.armed {
69 return;
70 }
71
72 let mut cache = self.cache.write();
79 let Entry::Occupied(mut current) = cache.page_fetches.entry(self.key) else {
80 return;
81 };
82 if !Arc::ptr_eq(¤t.get().fetch, &self.fetch) {
83 return;
84 }
85 if current.get().waiters == 1 {
86 current.remove();
87 } else {
88 current.get_mut().waiters -= 1;
89 }
90 }
91}
92
93struct Cache {
112 cache: Clock<(u64, u64), IoBufMut>,
114
115 hints: Vec<usize>,
121
122 page_size: NonZeroU16,
125
126 pool: BufferPool,
128
129 page_fetches: AHashMap<(u64, u64), PageFetchEntry>,
132}
133
134#[derive(Clone)]
138pub struct CacheRef {
139 page_size: NonZeroU16,
150
151 next_id: Arc<AtomicU64>,
153
154 cache: Arc<RwLock<Cache>>,
156
157 pool: BufferPool,
159}
160
161impl CacheRef {
162 pub fn new(pool: BufferPool, page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
172 let page_size_u64: u64 = page_size.widen();
173 let physical_page_size = page_size_u64 + CHECKSUM_SIZE;
174 if !physical_page_size.is_multiple_of(STORAGE_PAGE_SIZE)
175 && !STORAGE_PAGE_SIZE.is_multiple_of(physical_page_size)
176 {
177 debug!(
178 page_size = page_size.get(),
179 physical_page_size, "physical pages do not align with storage pages"
180 );
181 }
182
183 Self {
184 page_size,
185 next_id: Arc::new(AtomicU64::new(0)),
186 cache: Arc::new(RwLock::new(Cache::new(pool.clone(), page_size, capacity))),
187 pool,
188 }
189 }
190
191 pub fn from_pooler(
194 pooler: &impl BufferPooler,
195 page_size: NonZeroU16,
196 capacity: NonZeroUsize,
197 ) -> Self {
198 Self::new(pooler.storage_buffer_pool().clone(), page_size, capacity)
199 }
200
201 #[inline]
204 pub const fn page_size(&self) -> NonZeroU16 {
205 self.page_size
206 }
207
208 #[inline]
210 pub const fn pool(&self) -> &BufferPool {
211 &self.pool
212 }
213
214 pub fn next_id(&self) -> u64 {
216 self.next_id.fetch_add(1, Ordering::Relaxed)
217 }
218
219 pub fn offset_to_page(&self, offset: u64) -> (u64, u64) {
222 Cache::offset_to_page(self.page_size, offset)
223 }
224
225 pub(super) fn read_cached(
228 &self,
229 blob_id: u64,
230 mut buf: &mut [u8],
231 mut logical_offset: u64,
232 ) -> usize {
233 let original_len = buf.len();
234 let page_cache = self.cache.read();
235 while !buf.is_empty() {
236 let count = page_cache.read_at(blob_id, buf, logical_offset);
237 if count == 0 {
238 break;
240 }
241 logical_offset += count as u64;
242 buf = &mut buf[count..];
243 }
244 original_len - buf.len()
245 }
246
247 pub(super) fn read_cached_many(&self, blob_id: u64, ranges: &mut Vec<(&mut [u8], u64)>) {
254 let page_cache = self.cache.read();
255 let page_size = page_cache.page_size;
256
257 let mut srcs: Vec<Option<&[u8]>> = Vec::with_capacity(ranges.len());
261 for (buf, offset) in ranges.iter() {
262 let (page_num, offset_in_page, remaining) = Cache::locate(page_size, *offset);
263 let seg = std::cmp::min(buf.len(), remaining);
264 srcs.push(
265 page_cache
266 .get_page(blob_id, page_num)
267 .map(|page| &page.as_ref()[offset_in_page..offset_in_page + seg]),
268 );
269 }
270
271 let mut next = 0;
275 ranges.retain_mut(|(buf, offset)| {
276 let src = srcs[next];
277 next += 1;
278 if buf.is_empty() {
279 return false;
280 }
281 let Some(src) = src else {
282 return true;
283 };
284 buf[..src.len()].copy_from_slice(src);
285 let mut done = src.len();
286 while done < buf.len() {
287 let count = page_cache.read_at(blob_id, &mut buf[done..], *offset + done as u64);
288 if count == 0 {
289 return true;
290 }
291 done += count;
292 }
293 false
294 });
295 }
296
297 pub(super) async fn read<B: Blob>(
300 &self,
301 blob: &B,
302 blob_id: u64,
303 mut buf: &mut [u8],
304 mut offset: u64,
305 ) -> Result<(), Error> {
306 while !buf.is_empty() {
309 {
311 let page_cache = self.cache.read();
312 let count = page_cache.read_at(blob_id, buf, offset);
313 if count != 0 {
314 offset += count as u64;
315 buf = &mut buf[count..];
316 continue;
317 }
318 }
319
320 let count = self
322 .read_after_page_fault(blob, blob_id, buf, offset)
323 .await?;
324 offset += count as u64;
325 buf = &mut buf[count..];
326 }
327
328 Ok(())
329 }
330
331 pub(super) async fn read_after_page_fault<B: Blob>(
335 &self,
336 blob: &B,
337 blob_id: u64,
338 buf: &mut [u8],
339 offset: u64,
340 ) -> Result<usize, Error> {
341 assert!(!buf.is_empty());
342
343 let (page_num, offset_in_page, _) = Cache::locate(self.page_size, offset);
344 trace!(page_num, blob_id, "page fault");
345
346 let (fetch_future, mut fetch_guard) = {
350 let mut cache = self.cache.write();
351
352 let count = cache.read_at(blob_id, buf, offset);
355 if count != 0 {
356 return Ok(count);
357 }
358
359 let key = (blob_id, page_num);
360 match cache.page_fetches.entry(key) {
361 Entry::Occupied(o) => {
362 let entry = o.into_mut();
364 entry.waiters += 1;
365 let fetch_future = entry.fetch.as_ref().clone();
366 let fetch = Arc::clone(&entry.fetch);
367 (
368 fetch_future,
369 PageFetchGuard::new(Arc::clone(&self.cache), key, fetch),
370 )
371 }
372 Entry::Vacant(v) => {
373 let blob = blob.clone();
376 let cache = Arc::clone(&self.cache);
377 let page_size = self.page_size;
378 let future = async move {
379 let result = fetch_cacheable_page(&blob, page_num, page_size).await;
380 if let Err(err) = &result {
381 error!(page_num, ?err, "Page fetch failed");
382 }
383
384 let mut cache = cache.write();
390 if let Ok(page) = &result {
391 cache.cache(blob_id, page.as_ref(), page_num);
392 }
393 let _ = cache.page_fetches.remove(&key);
394 result
395 };
396
397 let fetch_future = future.boxed().shared();
399 let fetch = Arc::new(fetch_future.clone());
400 v.insert(PageFetchEntry {
401 fetch: Arc::clone(&fetch),
402 waiters: 1,
403 });
404
405 (
406 fetch_future,
407 PageFetchGuard::new(Arc::clone(&self.cache), key, fetch),
408 )
409 }
410 }
411 };
412
413 let fetch_result = fetch_future.await;
417 fetch_guard.disarm();
418 let page_buf = match fetch_result {
419 Ok(page_buf) => page_buf,
420 Err(err) => return Err(err.as_ref().clone()),
421 };
422
423 let bytes_to_copy = std::cmp::min(buf.len(), page_buf.len() - offset_in_page);
425 buf[..bytes_to_copy]
426 .copy_from_slice(&page_buf.as_ref()[offset_in_page..offset_in_page + bytes_to_copy]);
427
428 Ok(bytes_to_copy)
429 }
430
431 pub fn cache(&self, blob_id: u64, mut buf: &[u8], offset: u64) -> usize {
439 let (mut page_num, offset_in_page) = self.offset_to_page(offset);
440 assert_eq!(offset_in_page, 0);
441 {
442 let page_size: usize = self.page_size.widen();
444 let mut page_cache = self.cache.write();
445 while buf.len() >= page_size {
446 page_cache.cache(blob_id, &buf[..page_size], page_num);
447 buf = &buf[page_size..];
448 page_num = match page_num.checked_add(1) {
449 Some(next) => next,
450 None => break,
451 };
452 }
453 }
454
455 buf.len()
456 }
457
458 #[cfg(any(test, feature = "test-utils"))]
462 pub fn clear(&self) {
463 self.cache.write().clear();
464 }
465
466 pub(super) fn invalidate_from(&self, blob_id: u64, start_page: u64) {
470 self.cache.write().invalidate_from(blob_id, start_page);
471 }
472}
473
474impl Cache {
475 pub fn new(pool: BufferPool, page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
478 let slot_size: usize = page_size.widen();
479 let mut cache = Clock::new(capacity);
480 cache.prefill(|| pool.alloc_zeroed(slot_size));
481 let hints = capacity.get().saturating_mul(2).next_power_of_two();
482 Self {
483 cache,
484 hints: vec![usize::MAX; hints],
485 page_size,
486 pool,
487 page_fetches: AHashMap::new(),
488 }
489 }
490
491 fn offset_to_page(page_size: NonZeroU16, offset: u64) -> (u64, u64) {
494 let page_size: u64 = page_size.widen();
495 (offset / page_size, offset % page_size)
496 }
497
498 fn locate(page_size: NonZeroU16, offset: u64) -> (u64, usize, usize) {
501 let (page_num, offset_in_page) = Self::offset_to_page(page_size, offset);
502 let offset_in_page = offset_in_page as usize;
503 let width: usize = page_size.widen();
504 let remaining = width - offset_in_page;
505 (page_num, offset_in_page, remaining)
506 }
507
508 fn read_at(&self, blob_id: u64, buf: &mut [u8], logical_offset: u64) -> usize {
514 let (page_num, offset_in_page, remaining) = Self::locate(self.page_size, logical_offset);
515 let Some(page) = self.get_page(blob_id, page_num) else {
516 return 0;
517 };
518 let page = page.as_ref();
519
520 let bytes_to_copy = std::cmp::min(buf.len(), remaining);
521 buf[..bytes_to_copy].copy_from_slice(&page[offset_in_page..offset_in_page + bytes_to_copy]);
522
523 bytes_to_copy
524 }
525
526 fn cache(&mut self, blob_id: u64, page: &[u8], page_num: u64) {
528 let page_size: usize = self.page_size.widen();
529 assert_eq!(page.len(), page_size);
530 let pool = &self.pool;
531 let (slot, buf) = self
532 .cache
533 .get_or_insert_mut((blob_id, page_num), || pool.alloc_zeroed(page_size));
534 buf.as_mut().copy_from_slice(page);
535 let hint = self.hint_index(blob_id, page_num);
536 self.hints[hint] = slot;
537 }
538
539 #[inline]
548 const fn hint_index(&self, blob_id: u64, page_num: u64) -> usize {
549 let salted = page_num.wrapping_add(blob_id.wrapping_mul(commonware_utils::GOLDEN_RATIO));
550 (salted & (self.hints.len() as u64 - 1)) as usize
551 }
552
553 #[inline]
555 fn get_page(&self, blob_id: u64, page_num: u64) -> Option<&IoBufMut> {
556 let key = (blob_id, page_num);
557 let slot = self.hints[self.hint_index(blob_id, page_num)];
558 if let Some(page) = self.cache.get_at(slot, &key) {
559 return Some(page);
560 }
561 self.cache.get(&key)
562 }
563
564 fn invalidate_from(&mut self, blob_id: u64, start_page: u64) {
566 self.cache
567 .retain(|&(bid, page_num), _| bid != blob_id || page_num < start_page);
568 }
569
570 #[cfg(any(test, feature = "test-utils"))]
572 fn clear(&mut self) {
573 self.cache.retain(|_, _| false);
574 self.page_fetches.clear();
575 }
576}
577
578async fn fetch_cacheable_page(
581 blob: &impl Blob,
582 page_num: u64,
583 page_size: NonZeroU16,
584) -> Result<IoBuf, Arc<Error>> {
585 let width: u64 = page_size.widen();
587 let page = get_page_from_blob(blob, page_num, width, ReadOptions::DONT_CACHE)
588 .await
589 .map_err(Arc::new)?;
590
591 let len = page.len();
594 let expected: usize = page_size.widen();
595 if len != expected {
596 error!(
597 page_num,
598 expected = page_size,
599 actual = len,
600 "attempted to fetch partial page from blob"
601 );
602 return Err(Arc::new(Error::InvalidChecksum));
603 }
604
605 Ok(page)
606}
607
608#[cfg(test)]
609mod tests {
610 use super::{super::Checksum, *};
611 use crate::{
612 BufferPool, BufferPoolConfig, Clock as _, Handle, IoBufMut, IoBufs, IoBufsMut, Runner as _,
613 Spawner as _, Storage as _, Supervisor as _, WriteOptions, buffer::paged::CHECKSUM_SIZE,
614 deterministic, telemetry::metrics::Registry,
615 };
616 use commonware_cryptography::Crc32;
617 use commonware_macros::test_traced;
618 use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
619 use futures::future::pending;
620 use rstest::rstest;
621 use std::{
622 num::NonZeroU16,
623 sync::{
624 Arc,
625 atomic::{AtomicUsize, Ordering},
626 },
627 time::Duration,
628 };
629
630 fn test_pool() -> BufferPool {
631 let mut registry = Registry::default();
632 BufferPool::new(BufferPoolConfig::for_storage(), &mut registry)
633 }
634
635 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
637 const PAGE_SIZE_U64: u64 = PAGE_SIZE.get() as u64;
638
639 fn expected_cached_bytes(logical_offset: u64, len: usize) -> Vec<u8> {
640 (0..len)
641 .map(|i| {
642 let page = (logical_offset + i as u64) / PAGE_SIZE_U64;
643 page as u8 + 1
644 })
645 .collect()
646 }
647
648 #[derive(Clone)]
650 struct BlockingBlob {
651 started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
652 }
653
654 impl Blob for BlockingBlob {
655 async fn read_at(
656 &self,
657 offset: u64,
658 len: usize,
659 options: ReadOptions,
660 ) -> Result<IoBufsMut, Error> {
661 self.read_at_buf(offset, len, IoBufMut::with_capacity(len), options)
662 .await
663 }
664
665 async fn read_at_buf(
666 &self,
667 _offset: u64,
668 _len: usize,
669 _bufs: impl Into<IoBufsMut> + Send,
670 _options: ReadOptions,
671 ) -> Result<IoBufsMut, Error> {
672 let sender = self
673 .started
674 .lock()
675 .take()
676 .expect("blocking blob read started more than once");
677 let _ = sender.send(());
678 pending::<()>().await;
679 unreachable!()
680 }
681
682 async fn write_at(
683 &self,
684 _offset: u64,
685 _bufs: impl Into<crate::IoBufs> + Send,
686 _options: WriteOptions,
687 ) -> Result<(), Error> {
688 Ok(())
689 }
690
691 async fn resize(&self, _len: u64) -> Result<(), Error> {
692 Ok(())
693 }
694
695 async fn sync(&self) -> Result<(), Error> {
696 Ok(())
697 }
698
699 async fn start_sync(&self) -> Handle<()> {
700 Handle::ready(self.sync().await)
701 }
702 }
703
704 #[derive(Clone)]
705 enum ControlledBlobResult {
706 Success(Arc<Vec<u8>>),
707 Error,
708 }
709
710 #[derive(Clone)]
712 struct ControlledBlob {
713 started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
714 release: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
715 reads: Arc<AtomicUsize>,
716 result: ControlledBlobResult,
717 }
718
719 impl Blob for ControlledBlob {
720 async fn read_at(
721 &self,
722 offset: u64,
723 len: usize,
724 options: ReadOptions,
725 ) -> Result<IoBufsMut, Error> {
726 self.read_at_buf(offset, len, IoBufMut::with_capacity(len), options)
727 .await
728 }
729
730 async fn read_at_buf(
731 &self,
732 _offset: u64,
733 _len: usize,
734 _bufs: impl Into<IoBufsMut> + Send,
735 _options: ReadOptions,
736 ) -> Result<IoBufsMut, Error> {
737 if self.reads.fetch_add(1, Ordering::Relaxed) == 0 {
738 let sender = self
739 .started
740 .lock()
741 .take()
742 .expect("controlled blob start signal consumed more than once");
743 let _ = sender.send(());
744
745 let release = self
746 .release
747 .lock()
748 .take()
749 .expect("controlled blob release receiver consumed more than once");
750 release.await.expect("release signal dropped");
751 }
752
753 match &self.result {
754 ControlledBlobResult::Success(page) => Ok(IoBufsMut::from(page.as_ref().clone())),
755 ControlledBlobResult::Error => Err(Error::ReadFailed),
756 }
757 }
758
759 async fn write_at(
760 &self,
761 _offset: u64,
762 _bufs: impl Into<crate::IoBufs> + Send,
763 _options: WriteOptions,
764 ) -> Result<(), Error> {
765 Ok(())
766 }
767
768 async fn resize(&self, _len: u64) -> Result<(), Error> {
769 Ok(())
770 }
771
772 async fn sync(&self) -> Result<(), Error> {
773 Ok(())
774 }
775
776 async fn start_sync(&self) -> Handle<()> {
777 Handle::ready(self.sync().await)
778 }
779 }
780
781 #[test_traced]
782 fn test_cache_basic() {
783 let pool = test_pool();
784 let mut cache: Cache = Cache::new(pool, PAGE_SIZE, NZUsize!(10));
785
786 let mut buf = vec![0; PAGE_SIZE.get() as usize];
788 let bytes_read = cache.read_at(0, &mut buf, 0);
789 assert_eq!(bytes_read, 0);
790
791 cache.cache(0, &[1; PAGE_SIZE.get() as usize], 0);
792 let bytes_read = cache.read_at(0, &mut buf, 0);
793 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
794 assert_eq!(buf, [1; PAGE_SIZE.get() as usize]);
795
796 cache.cache(0, &[2; PAGE_SIZE.get() as usize], 0);
798 let bytes_read = cache.read_at(0, &mut buf, 0);
799 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
800 assert_eq!(buf, [2; PAGE_SIZE.get() as usize]);
801
802 for i in 0u64..11 {
804 cache.cache(0, &[i as u8; PAGE_SIZE.get() as usize], i);
805 }
806 let bytes_read = cache.read_at(0, &mut buf, 0);
808 assert_eq!(bytes_read, 0);
809 for i in 1u64..11 {
811 let bytes_read = cache.read_at(0, &mut buf, i * PAGE_SIZE_U64);
812 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
813 assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
814 }
815
816 let mut buf = vec![0; PAGE_SIZE.get() as usize];
819 let bytes_read = cache.read_at(0, &mut buf, PAGE_SIZE_U64 + 2);
820 assert_eq!(bytes_read, PAGE_SIZE.get() as usize - 2);
821 assert_eq!(
822 &buf[..PAGE_SIZE.get() as usize - 2],
823 [1; PAGE_SIZE.get() as usize - 2]
824 );
825 }
826
827 #[test_traced]
828 fn test_invalidate_from_does_not_orphan_re_cached_page() {
829 let mut registry = Registry::default();
833 let pool = BufferPool::new(BufferPoolConfig::for_storage(), &mut registry);
834 let mut cache: Cache = Cache::new(pool, PAGE_SIZE, NZUsize!(2));
835 let blob_id = 0u64;
836 let page_size = PAGE_SIZE.get() as usize;
837
838 cache.cache(blob_id, &vec![0xAA; page_size], 0);
840 cache.cache(blob_id, &vec![0xBB; page_size], 1);
841 cache.invalidate_from(blob_id, 0);
842
843 cache.cache(blob_id, &vec![0xCC; page_size], 1);
845 let mut buf = vec![0u8; page_size];
846 assert_eq!(
847 cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64),
848 page_size,
849 "page 1 should be readable after re-cache"
850 );
851 assert_eq!(buf, vec![0xCC; page_size]);
852
853 cache.cache(blob_id, &vec![0xDD; page_size], 2);
855
856 let mut buf = vec![0u8; page_size];
858 assert_eq!(
859 cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64),
860 page_size,
861 "live page 1 was orphaned by stale-slot eviction"
862 );
863 assert_eq!(buf, vec![0xCC; page_size]);
864
865 let mut buf = vec![0u8; page_size];
867 assert_eq!(
868 cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64 * 2),
869 page_size
870 );
871 assert_eq!(buf, vec![0xDD; page_size]);
872 }
873
874 #[test_traced]
875 fn test_cache_read_with_blob() {
876 let executor = deterministic::Runner::default();
878 executor.start(|context| async move {
880 let physical_page_size = PAGE_SIZE_U64 + CHECKSUM_SIZE;
882
883 let (blob, size) = context
885 .open("test", "blob".as_bytes())
886 .await
887 .expect("Failed to open blob");
888 assert_eq!(size, 0);
889 for i in 0..11 {
890 let logical_data = vec![i as u8; PAGE_SIZE.get() as usize];
892 let crc = Crc32::checksum(&logical_data);
893 let record = Checksum::new(PAGE_SIZE.get(), crc);
894 let mut page_data = logical_data;
895 page_data.extend_from_slice(&record.to_bytes());
896 blob.write_at(i * physical_page_size, page_data, WriteOptions::default())
897 .await
898 .unwrap();
899 }
900
901 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
903 assert_eq!(cache_ref.next_id(), 0);
904 assert_eq!(cache_ref.next_id(), 1);
905 for i in 0..11 {
906 let mut buf = vec![0; PAGE_SIZE.get() as usize];
908 cache_ref
909 .read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
910 .await
911 .unwrap();
912 assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
913 }
914
915 for i in 1..11 {
918 let mut buf = vec![0; PAGE_SIZE.get() as usize];
919 cache_ref
920 .read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
921 .await
922 .unwrap();
923 assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
924 }
925
926 blob.sync().await.unwrap();
928 });
929 }
930
931 #[test_traced]
932 fn test_cache_clear_forces_uncached_blob_read() {
933 #[derive(Clone)]
934 struct CountingBlob {
935 reads: Arc<AtomicUsize>,
936 read_options: Arc<Mutex<Vec<ReadOptions>>>,
937 page: Arc<Vec<u8>>,
938 }
939
940 impl Blob for CountingBlob {
941 async fn read_at(
942 &self,
943 offset: u64,
944 len: usize,
945 options: ReadOptions,
946 ) -> Result<IoBufsMut, Error> {
947 self.read_at_buf(offset, len, IoBufsMut::default(), options)
948 .await
949 }
950
951 async fn read_at_buf(
952 &self,
953 _offset: u64,
954 _len: usize,
955 _bufs: impl Into<IoBufsMut> + Send,
956 options: ReadOptions,
957 ) -> Result<IoBufsMut, Error> {
958 self.reads.fetch_add(1, Ordering::Relaxed);
959 self.read_options.lock().push(options);
960 Ok(IoBufsMut::from(self.page.as_ref().clone()))
961 }
962
963 async fn write_at(
964 &self,
965 _offset: u64,
966 _bufs: impl Into<IoBufs> + Send,
967 _options: WriteOptions,
968 ) -> Result<(), Error> {
969 Ok(())
970 }
971
972 async fn resize(&self, _len: u64) -> Result<(), Error> {
973 Ok(())
974 }
975
976 async fn sync(&self) -> Result<(), Error> {
977 Ok(())
978 }
979
980 async fn start_sync(&self) -> Handle<()> {
981 Handle::ready(self.sync().await)
982 }
983 }
984
985 let executor = deterministic::Runner::default();
986 executor.start(|context| async move {
987 let page = vec![7u8; PAGE_SIZE.get() as usize];
988 let crc = Crc32::checksum(&page);
989 let record = Checksum::new(PAGE_SIZE.get(), crc);
990 let mut physical_page = page.clone();
991 physical_page.extend_from_slice(&record.to_bytes());
992 let physical_page = Arc::new(physical_page);
993 let blob = CountingBlob {
994 reads: Arc::new(AtomicUsize::new(0)),
995 read_options: Arc::new(Mutex::new(Vec::new())),
996 page: physical_page,
997 };
998 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
999
1000 let mut buf = vec![0u8; page.len()];
1001 cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
1002 assert_eq!(buf, page);
1003 assert_eq!(blob.reads.load(Ordering::Relaxed), 1);
1004
1005 let mut buf = vec![0u8; page.len()];
1006 cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
1007 assert_eq!(buf, page);
1008 assert_eq!(blob.reads.load(Ordering::Relaxed), 1);
1009
1010 cache_ref.clear();
1011
1012 let mut buf = vec![0u8; page.len()];
1013 cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
1014 assert_eq!(buf, page);
1015 assert_eq!(blob.reads.load(Ordering::Relaxed), 2);
1016 assert_eq!(
1017 *blob.read_options.lock(),
1018 vec![ReadOptions::DONT_CACHE, ReadOptions::DONT_CACHE]
1019 );
1020 });
1021 }
1022
1023 #[test_traced]
1024 fn test_cache_max_page() {
1025 let executor = deterministic::Runner::default();
1026 executor.start(|context| async move {
1027 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
1028
1029 let aligned_max_offset = u64::MAX - (u64::MAX % PAGE_SIZE_U64);
1031
1032 let logical_data = vec![42u8; PAGE_SIZE.get() as usize];
1034
1035 let remaining = cache_ref.cache(0, logical_data.as_slice(), aligned_max_offset);
1037 assert_eq!(remaining, 0);
1038
1039 let mut buf = vec![0u8; PAGE_SIZE.get() as usize];
1041 let page_cache = cache_ref.cache.read();
1042 let bytes_read = page_cache.read_at(0, &mut buf, aligned_max_offset);
1043 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
1044 assert!(buf.iter().all(|b| *b == 42));
1045 });
1046 }
1047
1048 #[test_traced]
1049 fn test_cache_at_high_offset() {
1050 let executor = deterministic::Runner::default();
1051 executor.start(|context| async move {
1052 const MIN_PAGE_SIZE: u64 = CHECKSUM_SIZE + 1;
1054 let cache_ref =
1055 CacheRef::from_pooler(&context, NZU16!(MIN_PAGE_SIZE as u16), NZUsize!(2));
1056
1057 let data = vec![1u8; MIN_PAGE_SIZE as usize * 2];
1060
1061 let aligned_max_offset = u64::MAX - (u64::MAX % MIN_PAGE_SIZE);
1064 let high_offset = aligned_max_offset - (MIN_PAGE_SIZE * 2);
1065 let remaining = cache_ref.cache(0, &data, high_offset);
1066 assert_eq!(remaining, 0);
1068
1069 let mut buf = vec![0u8; MIN_PAGE_SIZE as usize];
1071 let page_cache = cache_ref.cache.read();
1072 assert_eq!(
1073 page_cache.read_at(0, &mut buf, high_offset),
1074 MIN_PAGE_SIZE as usize
1075 );
1076 assert!(buf.iter().all(|b| *b == 1));
1077
1078 assert_eq!(
1080 page_cache.read_at(0, &mut buf, high_offset + MIN_PAGE_SIZE),
1081 MIN_PAGE_SIZE as usize
1082 );
1083 assert!(buf.iter().all(|b| *b == 1));
1084 });
1085 }
1086
1087 #[test_traced]
1088 fn test_page_fetches_entry_removed_when_first_fetcher_cancelled() {
1089 let executor = deterministic::Runner::default();
1090 executor.start(|context| async move {
1091 let blob_id = 0;
1093 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1094 let (started_tx, started_rx) = oneshot::channel();
1095 let blob = BlockingBlob {
1096 started: Arc::new(Mutex::new(Some(started_tx))),
1097 };
1098 let mut read_buf = vec![0u8; PAGE_SIZE.get() as usize];
1099
1100 let cache_ref_for_task = cache_ref.clone();
1102 let blob_for_task = blob.clone();
1103 let handle = context.spawn(move |_| async move {
1104 let _ = cache_ref_for_task
1105 .read(&blob_for_task, blob_id, &mut read_buf, 0)
1106 .await;
1107 });
1108
1109 started_rx.await.expect("blocking read never started");
1111 {
1112 let page_cache = cache_ref.cache.read();
1113 assert!(page_cache.page_fetches.contains_key(&(blob_id, 0)));
1114 }
1115
1116 handle.abort();
1118 assert!(matches!(handle.await, Err(Error::Closed)));
1119
1120 let page_cache = cache_ref.cache.read();
1122 assert!(
1123 !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1124 "cancelled first fetcher should not leave stale page_fetches entry"
1125 );
1126 });
1127 }
1128
1129 #[test_traced]
1130 fn test_followers_keep_single_flight_after_first_fetcher_cancellation() {
1131 let executor = deterministic::Runner::default();
1132 executor.start(|context| async move {
1133 let blob_id = 0;
1134 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1135
1136 let logical_page = vec![7u8; PAGE_SIZE.get() as usize];
1138 let crc = Crc32::checksum(&logical_page);
1139 let mut physical_page = logical_page.clone();
1140 physical_page.extend_from_slice(&Checksum::new(PAGE_SIZE.get(), crc).to_bytes());
1141 let (started_tx, started_rx) = oneshot::channel();
1142 let (release_tx, release_rx) = oneshot::channel();
1143 let reads = Arc::new(AtomicUsize::new(0));
1144 let blob = ControlledBlob {
1145 started: Arc::new(Mutex::new(Some(started_tx))),
1146 release: Arc::new(Mutex::new(Some(release_rx))),
1147 reads: reads.clone(),
1148 result: ControlledBlobResult::Success(Arc::new(physical_page)),
1149 };
1150
1151 let mut first_buf = vec![0u8; PAGE_SIZE.get() as usize];
1153 let cache_ref_for_first = cache_ref.clone();
1154 let blob_for_first = blob.clone();
1155 let first = context.child("first").spawn(move |_| async move {
1156 let _ = cache_ref_for_first
1157 .read(&blob_for_first, blob_id, &mut first_buf, 0)
1158 .await;
1159 });
1160 started_rx.await.expect("first read never started");
1161
1162 let mut second_buf = vec![0u8; PAGE_SIZE.get() as usize];
1164 let cache_ref_for_second = cache_ref.clone();
1165 let blob_for_second = blob.clone();
1166 let second = context.child("second").spawn(move |_| async move {
1167 cache_ref_for_second
1168 .read(&blob_for_second, blob_id, &mut second_buf, 0)
1169 .await
1170 .expect("second read failed");
1171 second_buf
1172 });
1173
1174 loop {
1176 let joined = {
1177 let page_cache = cache_ref.cache.read();
1178 page_cache
1179 .page_fetches
1180 .get(&(blob_id, 0))
1181 .map(|fetch| fetch.waiters == 2)
1182 .unwrap_or(false)
1183 };
1184 if joined {
1185 break;
1186 }
1187 context.sleep(Duration::from_millis(1)).await;
1188 }
1189
1190 first.abort();
1192 assert!(matches!(first.await, Err(Error::Closed)));
1193
1194 let mut third_buf = vec![0u8; PAGE_SIZE.get() as usize];
1197 let cache_ref_for_third = cache_ref.clone();
1198 let blob_for_third = blob.clone();
1199 let third = context.child("third").spawn(move |_| async move {
1200 cache_ref_for_third
1201 .read(&blob_for_third, blob_id, &mut third_buf, 0)
1202 .await
1203 .expect("third read failed");
1204 third_buf
1205 });
1206
1207 loop {
1210 let third_entered = {
1211 let page_cache = cache_ref.cache.read();
1212 reads.load(Ordering::Relaxed) > 1
1213 || page_cache
1214 .page_fetches
1215 .get(&(blob_id, 0))
1216 .map(|fetch| fetch.waiters == 2)
1217 .unwrap_or(false)
1218 };
1219 if third_entered {
1220 break;
1221 }
1222 context.sleep(Duration::from_millis(1)).await;
1223 }
1224
1225 let _ = release_tx.send(());
1227 let second_buf = second.await.expect("second task failed");
1228 let third_buf = third.await.expect("third task failed");
1229 assert_eq!(second_buf, logical_page);
1230 assert_eq!(third_buf, logical_page);
1231
1232 assert_eq!(reads.load(Ordering::Relaxed), 1);
1234
1235 let mut cached = vec![0u8; PAGE_SIZE.get() as usize];
1237 assert_eq!(
1238 cache_ref.read_cached(blob_id, &mut cached, 0),
1239 PAGE_SIZE.get() as usize
1240 );
1241 assert_eq!(cached, logical_page);
1242
1243 let mut fourth_buf = vec![0u8; PAGE_SIZE.get() as usize];
1245 cache_ref
1246 .read(&blob, blob_id, &mut fourth_buf, 0)
1247 .await
1248 .unwrap();
1249 assert_eq!(fourth_buf, logical_page);
1250 assert_eq!(reads.load(Ordering::Relaxed), 1);
1251
1252 let page_cache = cache_ref.cache.read();
1253 assert!(
1254 !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1255 "completed fetch should leave no stale page_fetches entry"
1256 );
1257 });
1258 }
1259
1260 #[test_traced]
1261 fn test_page_fetch_error_removes_entry_for_all_waiters() {
1262 let executor = deterministic::Runner::default();
1263 executor.start(|context| async move {
1264 let blob_id = 0;
1265 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1266
1267 let (started_tx, started_rx) = oneshot::channel();
1269 let (release_tx, release_rx) = oneshot::channel();
1270 let reads = Arc::new(AtomicUsize::new(0));
1271 let blob = ControlledBlob {
1272 started: Arc::new(Mutex::new(Some(started_tx))),
1273 release: Arc::new(Mutex::new(Some(release_rx))),
1274 reads: reads.clone(),
1275 result: ControlledBlobResult::Error,
1276 };
1277
1278 let mut first_buf = vec![0u8; PAGE_SIZE.get() as usize];
1280 let cache_ref_for_first = cache_ref.clone();
1281 let blob_for_first = blob.clone();
1282 let first = context.child("first").spawn(move |_| async move {
1283 cache_ref_for_first
1284 .read(&blob_for_first, blob_id, &mut first_buf, 0)
1285 .await
1286 });
1287 started_rx.await.expect("first erroring read never started");
1288
1289 let mut second_buf = vec![0u8; PAGE_SIZE.get() as usize];
1291 let cache_ref_for_second = cache_ref.clone();
1292 let blob_for_second = blob.clone();
1293 let second = context.child("second").spawn(move |_| async move {
1294 cache_ref_for_second
1295 .read(&blob_for_second, blob_id, &mut second_buf, 0)
1296 .await
1297 });
1298
1299 loop {
1301 let joined = {
1302 let page_cache = cache_ref.cache.read();
1303 page_cache
1304 .page_fetches
1305 .get(&(blob_id, 0))
1306 .map(|fetch| fetch.waiters == 2)
1307 .unwrap_or(false)
1308 };
1309 if joined {
1310 break;
1311 }
1312 context.sleep(Duration::from_millis(1)).await;
1313 }
1314
1315 let _ = release_tx.send(());
1317
1318 assert!(matches!(first.await, Ok(Err(Error::ReadFailed))));
1319 assert!(matches!(second.await, Ok(Err(Error::ReadFailed))));
1320 assert_eq!(reads.load(Ordering::Relaxed), 1);
1322
1323 {
1325 let page_cache = cache_ref.cache.read();
1326 assert!(
1327 !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1328 "erroring fetch should leave no stale page_fetches entry"
1329 );
1330 }
1331 let mut cached = vec![0u8; PAGE_SIZE.get() as usize];
1332 assert_eq!(cache_ref.read_cached(blob_id, &mut cached, 0), 0);
1333
1334 let mut third_buf = vec![0u8; PAGE_SIZE.get() as usize];
1336 assert!(matches!(
1337 cache_ref.read(&blob, blob_id, &mut third_buf, 0).await,
1338 Err(Error::ReadFailed)
1339 ));
1340 assert_eq!(reads.load(Ordering::Relaxed), 2);
1341 });
1342 }
1343
1344 #[test_traced]
1345 fn test_read_cached_many_all_cached() {
1346 let pool = test_pool();
1347 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1348 let blob_id = cache_ref.next_id();
1349 let page0 = vec![0xAA; PAGE_SIZE.get() as usize];
1350 let page1 = vec![0xBB; PAGE_SIZE.get() as usize];
1351
1352 {
1354 let mut cache = cache_ref.cache.write();
1355 cache.cache(blob_id, &page0, 0);
1356 cache.cache(blob_id, &page1, 1);
1357 }
1358
1359 let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1360 let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1361 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf0, 0), (&mut buf1, PAGE_SIZE_U64)];
1362
1363 cache_ref.read_cached_many(blob_id, &mut ranges);
1364
1365 assert!(ranges.is_empty());
1367 drop(ranges);
1368
1369 assert!(buf0 == page0);
1371 assert!(buf1 == page1);
1372 }
1373
1374 #[test_traced]
1375 fn test_read_cached_many_none_cached() {
1376 let pool = test_pool();
1377 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1378 let blob_id = cache_ref.next_id();
1379
1380 let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1381 let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1382 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf0, 0), (&mut buf1, PAGE_SIZE_U64)];
1383
1384 cache_ref.read_cached_many(blob_id, &mut ranges);
1386 assert_eq!(ranges.len(), 2);
1387 assert_eq!(ranges[0].1, 0);
1388 assert_eq!(ranges[1].1, PAGE_SIZE_U64);
1389 }
1390
1391 #[test_traced]
1392 fn test_read_cached_many_scattered_misses() {
1393 let pool = test_pool();
1396 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1397 let blob_id = cache_ref.next_id();
1398
1399 let page0 = vec![0x11; PAGE_SIZE.get() as usize];
1400 let page2 = vec![0x33; PAGE_SIZE.get() as usize];
1401 {
1402 let mut cache = cache_ref.cache.write();
1403 cache.cache(blob_id, &page0, 0);
1404 cache.cache(blob_id, &page2, 2);
1406 }
1407
1408 let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1409 let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1410 let mut buf2 = vec![0u8; PAGE_SIZE_U64 as usize];
1411 let mut ranges: Vec<(&mut [u8], u64)> = vec![
1412 (&mut buf0, 0),
1413 (&mut buf1, PAGE_SIZE_U64),
1414 (&mut buf2, PAGE_SIZE_U64 * 2),
1415 ];
1416
1417 cache_ref.read_cached_many(blob_id, &mut ranges);
1418
1419 assert_eq!(ranges.len(), 1);
1422 assert_eq!(ranges[0].1, PAGE_SIZE_U64);
1423 drop(ranges);
1424
1425 assert!(buf0 == page0);
1427 assert!(buf2 == page2);
1428 assert!(buf1.iter().all(|b| *b == 0));
1430 }
1431
1432 #[test_traced]
1433 fn test_read_cached_many_stale_hint_after_eviction() {
1434 let pool = test_pool();
1439 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(2));
1440 let blob_id = cache_ref.next_id();
1441 let page_size = PAGE_SIZE.get() as usize;
1442 {
1443 let mut cache = cache_ref.cache.write();
1444 for page in 0u64..3 {
1445 cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1446 }
1447 }
1448
1449 let mut bufs: Vec<Vec<u8>> = (0..3).map(|_| vec![0u8; page_size]).collect();
1450 let mut iter = bufs.iter_mut();
1451 let mut ranges: Vec<(&mut [u8], u64)> = (0..3u64)
1452 .map(|page| (iter.next().unwrap().as_mut_slice(), page * PAGE_SIZE_U64))
1453 .collect();
1454 cache_ref.read_cached_many(blob_id, &mut ranges);
1455
1456 assert_eq!(ranges.len(), 1);
1458 assert_eq!(ranges[0].1, 0);
1459 drop(ranges);
1460 assert!(bufs[0].iter().all(|b| *b == 0));
1461 assert_eq!(bufs[1], vec![2u8; page_size]);
1462 assert_eq!(bufs[2], vec![3u8; page_size]);
1463 }
1464
1465 #[test_traced]
1466 fn test_read_cached_many_cross_blob_hint_collision() {
1467 let pool = test_pool();
1472 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(4));
1473 let blob_a = cache_ref.next_id();
1474 let blob_b = cache_ref.next_id();
1475 let page_size = PAGE_SIZE.get() as usize;
1476 let page_a = 5u64;
1477 let page_b = {
1478 let mut cache = cache_ref.cache.write();
1479
1480 let mask = cache.hints.len() as u64 - 1;
1482 let page_b = page_a
1483 .wrapping_add(blob_a.wrapping_mul(commonware_utils::GOLDEN_RATIO))
1484 .wrapping_sub(blob_b.wrapping_mul(commonware_utils::GOLDEN_RATIO))
1485 & mask;
1486 assert_eq!(
1487 cache.hint_index(blob_a, page_a),
1488 cache.hint_index(blob_b, page_b)
1489 );
1490 cache.cache(blob_a, &vec![0xAA; page_size], page_a);
1491 cache.cache(blob_b, &vec![0xBB; page_size], page_b);
1492 page_b
1493 };
1494
1495 for (blob, page, byte) in [(blob_a, page_a, 0xAAu8), (blob_b, page_b, 0xBB)] {
1496 let mut buf = vec![0u8; page_size];
1497 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page * PAGE_SIZE_U64)];
1498 cache_ref.read_cached_many(blob, &mut ranges);
1499 assert!(
1500 ranges.is_empty(),
1501 "blob {blob} page {page} should be cached"
1502 );
1503 drop(ranges);
1504 assert_eq!(buf, vec![byte; page_size]);
1505 }
1506 }
1507
1508 #[test_traced]
1509 fn test_read_cached_many_sparse_page_number_keeps_hints_fixed() {
1510 let pool = test_pool();
1513 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(2));
1514 let blob_id = cache_ref.next_id();
1515 let page_size = PAGE_SIZE.get() as usize;
1516 let page_num = u64::MAX / PAGE_SIZE_U64 - 1;
1517 {
1518 let mut cache = cache_ref.cache.write();
1519 let hints = cache.hints.len();
1520 cache.cache(blob_id, &vec![0x5A; page_size], page_num);
1521 assert_eq!(cache.hints.len(), hints);
1522 }
1523
1524 let mut buf = vec![0u8; page_size];
1525 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page_num * PAGE_SIZE_U64)];
1526 cache_ref.read_cached_many(blob_id, &mut ranges);
1527 assert!(ranges.is_empty());
1528 drop(ranges);
1529 assert_eq!(buf, vec![0x5A; page_size]);
1530 }
1531
1532 #[test_traced]
1533 fn test_read_cached_many_invalidated_page_is_a_miss() {
1534 let pool = test_pool();
1538 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(4));
1539 let blob_id = cache_ref.next_id();
1540 let page_size = PAGE_SIZE.get() as usize;
1541 {
1542 let mut cache = cache_ref.cache.write();
1543 for page in 0u64..4 {
1544 cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1545 }
1546 }
1547 cache_ref.invalidate_from(blob_id, 2);
1548
1549 let read_page = |page: u64| {
1550 let mut buf = vec![0u8; page_size];
1551 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page * PAGE_SIZE_U64)];
1552 cache_ref.read_cached_many(blob_id, &mut ranges);
1553 let hit = ranges.is_empty();
1554 drop(ranges);
1555 hit.then_some(buf)
1556 };
1557 assert_eq!(read_page(0), Some(vec![1u8; page_size]));
1558 assert_eq!(read_page(1), Some(vec![2u8; page_size]));
1559 assert_eq!(read_page(2), None);
1560 assert_eq!(read_page(3), None);
1561
1562 {
1564 let mut cache = cache_ref.cache.write();
1565 cache.cache(blob_id, &vec![0xCC; page_size], 2);
1566 }
1567 assert_eq!(read_page(2), Some(vec![0xCC; page_size]));
1568 }
1569
1570 #[rstest]
1571 #[case::empty_read(vec![], 0, 0, 0)]
1572 #[case::single_cached_page(vec![0], 3, 5, 5)]
1573 #[case::cached_range_can_cross_pages(vec![0, 1], PAGE_SIZE_U64 - 2, 4, 4)]
1574 #[case::missing_first_page_reads_nothing(vec![1], 0, 4, 0)]
1575 #[case::missing_later_page_truncates_read(vec![0], PAGE_SIZE_U64 - 2, 4, 2)]
1576 fn test_read_cached(
1577 #[case] cached_pages: Vec<u64>,
1578 #[case] logical_offset: u64,
1579 #[case] len: usize,
1580 #[case] expected_count: usize,
1581 ) {
1582 let pool = test_pool();
1583 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1584 let blob_id = cache_ref.next_id();
1585 let sentinel = 0xEE;
1586 let page_size = PAGE_SIZE.get() as usize;
1587
1588 {
1589 let mut cache = cache_ref.cache.write();
1590 for page in cached_pages {
1591 cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1593 }
1594 }
1595
1596 let mut buf = vec![sentinel; len];
1597 let count = cache_ref.read_cached(blob_id, &mut buf, logical_offset);
1598 assert_eq!(count, expected_count);
1599
1600 assert_eq!(buf[..count], expected_cached_bytes(logical_offset, count));
1602 assert!(buf[count..].iter().all(|b| *b == sentinel));
1603 }
1604}