1use super::get_page_from_blob;
5use crate::{Blob, BufferPool, BufferPooler, Error, IoBuf, IoBufMut};
6use ahash::AHashMap;
7use commonware_utils::{cache::Clock, sync::RwLock};
8use futures::{future::Shared, FutureExt};
9use std::{
10 collections::hash_map::Entry,
11 future::Future,
12 num::{NonZeroU16, NonZeroUsize},
13 pin::Pin,
14 sync::{
15 atomic::{AtomicU64, Ordering},
16 Arc,
17 },
18};
19use tracing::{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: usize,
124
125 pool: BufferPool,
127
128 page_fetches: AHashMap<(u64, u64), PageFetchEntry>,
131}
132
133#[derive(Clone)]
137pub struct CacheRef {
138 page_size: u64,
145
146 next_id: Arc<AtomicU64>,
148
149 cache: Arc<RwLock<Cache>>,
151
152 pool: BufferPool,
154}
155
156impl CacheRef {
157 pub fn new(pool: BufferPool, page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
162 let page_size_u64 = page_size.get() as u64;
163
164 Self {
165 page_size: page_size_u64,
166 next_id: Arc::new(AtomicU64::new(0)),
167 cache: Arc::new(RwLock::new(Cache::new(pool.clone(), page_size, capacity))),
168 pool,
169 }
170 }
171
172 pub fn from_pooler(
175 pooler: &impl BufferPooler,
176 page_size: NonZeroU16,
177 capacity: NonZeroUsize,
178 ) -> Self {
179 Self::new(pooler.storage_buffer_pool().clone(), page_size, capacity)
180 }
181
182 #[inline]
184 pub const fn page_size(&self) -> u64 {
185 self.page_size
186 }
187
188 #[inline]
190 pub const fn pool(&self) -> &BufferPool {
191 &self.pool
192 }
193
194 pub fn next_id(&self) -> u64 {
196 self.next_id.fetch_add(1, Ordering::Relaxed)
197 }
198
199 pub const fn offset_to_page(&self, offset: u64) -> (u64, u64) {
202 Cache::offset_to_page(self.page_size, offset)
203 }
204
205 pub(super) fn read_cached(
208 &self,
209 blob_id: u64,
210 mut buf: &mut [u8],
211 mut logical_offset: u64,
212 ) -> usize {
213 let original_len = buf.len();
214 let page_cache = self.cache.read();
215 while !buf.is_empty() {
216 let count = page_cache.read_at(blob_id, buf, logical_offset);
217 if count == 0 {
218 break;
220 }
221 logical_offset += count as u64;
222 buf = &mut buf[count..];
223 }
224 original_len - buf.len()
225 }
226
227 pub(super) fn read_cached_many(&self, blob_id: u64, ranges: &mut Vec<(&mut [u8], u64)>) {
234 let page_cache = self.cache.read();
235 let page_size = page_cache.page_size;
236
237 let mut srcs: Vec<Option<&[u8]>> = Vec::with_capacity(ranges.len());
241 for (buf, offset) in ranges.iter() {
242 let (page_num, offset_in_page) = Cache::offset_to_page(page_size as u64, *offset);
243 let offset_in_page = offset_in_page as usize;
244 let seg = std::cmp::min(buf.len(), page_size - offset_in_page);
245 srcs.push(
246 page_cache
247 .get_page(blob_id, page_num)
248 .map(|page| &page.as_ref()[offset_in_page..offset_in_page + seg]),
249 );
250 }
251
252 let mut next = 0;
256 ranges.retain_mut(|(buf, offset)| {
257 let src = srcs[next];
258 next += 1;
259 if buf.is_empty() {
260 return false;
261 }
262 let Some(src) = src else {
263 return true;
264 };
265 buf[..src.len()].copy_from_slice(src);
266 let mut done = src.len();
267 while done < buf.len() {
268 let count = page_cache.read_at(blob_id, &mut buf[done..], *offset + done as u64);
269 if count == 0 {
270 return true;
271 }
272 done += count;
273 }
274 false
275 });
276 }
277
278 pub(super) async fn read<B: Blob>(
281 &self,
282 blob: &B,
283 blob_id: u64,
284 mut buf: &mut [u8],
285 mut offset: u64,
286 ) -> Result<(), Error> {
287 while !buf.is_empty() {
290 {
292 let page_cache = self.cache.read();
293 let count = page_cache.read_at(blob_id, buf, offset);
294 if count != 0 {
295 offset += count as u64;
296 buf = &mut buf[count..];
297 continue;
298 }
299 }
300
301 let count = self
303 .read_after_page_fault(blob, blob_id, buf, offset)
304 .await?;
305 offset += count as u64;
306 buf = &mut buf[count..];
307 }
308
309 Ok(())
310 }
311
312 pub(super) async fn read_after_page_fault<B: Blob>(
316 &self,
317 blob: &B,
318 blob_id: u64,
319 buf: &mut [u8],
320 offset: u64,
321 ) -> Result<usize, Error> {
322 assert!(!buf.is_empty());
323
324 let (page_num, offset_in_page) = Cache::offset_to_page(self.page_size, offset);
325 let offset_in_page = offset_in_page as usize;
326 trace!(page_num, blob_id, "page fault");
327
328 let (fetch_future, mut fetch_guard) = {
332 let mut cache = self.cache.write();
333
334 let count = cache.read_at(blob_id, buf, offset);
337 if count != 0 {
338 return Ok(count);
339 }
340
341 let key = (blob_id, page_num);
342 match cache.page_fetches.entry(key) {
343 Entry::Occupied(o) => {
344 let entry = o.into_mut();
346 entry.waiters += 1;
347 let fetch_future = entry.fetch.as_ref().clone();
348 let fetch = Arc::clone(&entry.fetch);
349 (
350 fetch_future,
351 PageFetchGuard::new(Arc::clone(&self.cache), key, fetch),
352 )
353 }
354 Entry::Vacant(v) => {
355 let blob = blob.clone();
358 let cache = Arc::clone(&self.cache);
359 let page_size = self.page_size;
360 let future = async move {
361 let result = fetch_cacheable_page(&blob, page_num, page_size).await;
362 if let Err(err) = &result {
363 error!(page_num, ?err, "Page fetch failed");
364 }
365
366 let mut cache = cache.write();
372 if let Ok(page) = &result {
373 cache.cache(blob_id, page.as_ref(), page_num);
374 }
375 let _ = cache.page_fetches.remove(&key);
376 result
377 };
378
379 let fetch_future = future.boxed().shared();
381 let fetch = Arc::new(fetch_future.clone());
382 v.insert(PageFetchEntry {
383 fetch: Arc::clone(&fetch),
384 waiters: 1,
385 });
386
387 (
388 fetch_future,
389 PageFetchGuard::new(Arc::clone(&self.cache), key, fetch),
390 )
391 }
392 }
393 };
394
395 let fetch_result = fetch_future.await;
399 fetch_guard.disarm();
400 let page_buf = match fetch_result {
401 Ok(page_buf) => page_buf,
402 Err(_) => return Err(Error::ReadFailed),
403 };
404
405 let bytes_to_copy = std::cmp::min(buf.len(), page_buf.len() - offset_in_page);
407 buf[..bytes_to_copy]
408 .copy_from_slice(&page_buf.as_ref()[offset_in_page..offset_in_page + bytes_to_copy]);
409
410 Ok(bytes_to_copy)
411 }
412
413 pub fn cache(&self, blob_id: u64, mut buf: &[u8], offset: u64) -> usize {
421 let (mut page_num, offset_in_page) = self.offset_to_page(offset);
422 assert_eq!(offset_in_page, 0);
423 {
424 let page_size = self.page_size as usize;
426 let mut page_cache = self.cache.write();
427 while buf.len() >= page_size {
428 page_cache.cache(blob_id, &buf[..page_size], page_num);
429 buf = &buf[page_size..];
430 page_num = match page_num.checked_add(1) {
431 Some(next) => next,
432 None => break,
433 };
434 }
435 }
436
437 buf.len()
438 }
439
440 #[cfg(any(test, feature = "test-utils"))]
444 pub fn clear(&self) {
445 self.cache.write().clear();
446 }
447
448 pub(super) fn invalidate_from(&self, blob_id: u64, start_page: u64) {
452 self.cache.write().invalidate_from(blob_id, start_page);
453 }
454}
455
456impl Cache {
457 pub fn new(pool: BufferPool, page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
460 let page_size = page_size.get() as usize;
461 let mut cache = Clock::new(capacity);
462 cache.prefill(|| pool.alloc_zeroed(page_size));
463 let hints = capacity.get().saturating_mul(2).next_power_of_two();
464 Self {
465 cache,
466 hints: vec![usize::MAX; hints],
467 page_size,
468 pool,
469 page_fetches: AHashMap::new(),
470 }
471 }
472
473 const fn offset_to_page(page_size: u64, offset: u64) -> (u64, u64) {
475 (offset / page_size, offset % page_size)
476 }
477
478 fn read_at(&self, blob_id: u64, buf: &mut [u8], logical_offset: u64) -> usize {
484 let (page_num, offset_in_page) =
485 Self::offset_to_page(self.page_size as u64, logical_offset);
486 let Some(page) = self.get_page(blob_id, page_num) else {
487 return 0;
488 };
489 let page = page.as_ref();
490
491 let offset_in_page = offset_in_page as usize;
492 let bytes_to_copy = std::cmp::min(buf.len(), self.page_size - offset_in_page);
493 buf[..bytes_to_copy].copy_from_slice(&page[offset_in_page..offset_in_page + bytes_to_copy]);
494
495 bytes_to_copy
496 }
497
498 fn cache(&mut self, blob_id: u64, page: &[u8], page_num: u64) {
500 assert_eq!(page.len(), self.page_size);
501 let pool = &self.pool;
502 let page_size = self.page_size;
503 let (slot, buf) = self
504 .cache
505 .get_or_insert_mut((blob_id, page_num), || pool.alloc_zeroed(page_size));
506 buf.as_mut().copy_from_slice(page);
507 let hint = self.hint_index(blob_id, page_num);
508 self.hints[hint] = slot;
509 }
510
511 #[inline]
520 const fn hint_index(&self, blob_id: u64, page_num: u64) -> usize {
521 let salted = page_num.wrapping_add(blob_id.wrapping_mul(commonware_utils::GOLDEN_RATIO));
522 (salted & (self.hints.len() as u64 - 1)) as usize
523 }
524
525 #[inline]
527 fn get_page(&self, blob_id: u64, page_num: u64) -> Option<&IoBufMut> {
528 let key = (blob_id, page_num);
529 let slot = self.hints[self.hint_index(blob_id, page_num)];
530 if let Some(page) = self.cache.get_at(slot, &key) {
531 return Some(page);
532 }
533 self.cache.get(&key)
534 }
535
536 fn invalidate_from(&mut self, blob_id: u64, start_page: u64) {
538 self.cache
539 .retain(|&(bid, page_num), _| bid != blob_id || page_num < start_page);
540 }
541
542 #[cfg(any(test, feature = "test-utils"))]
544 fn clear(&mut self) {
545 self.cache.retain(|_, _| false);
546 self.page_fetches.clear();
547 }
548}
549
550async fn fetch_cacheable_page(
553 blob: &impl Blob,
554 page_num: u64,
555 page_size: u64,
556) -> Result<IoBuf, Arc<Error>> {
557 let page = get_page_from_blob(blob, page_num, page_size)
558 .await
559 .map_err(Arc::new)?;
560
561 let len = page.len();
564 if len != page_size as usize {
565 error!(
566 page_num,
567 expected = page_size,
568 actual = len,
569 "attempted to fetch partial page from blob"
570 );
571 return Err(Arc::new(Error::InvalidChecksum));
572 }
573
574 Ok(page)
575}
576
577#[cfg(test)]
578mod tests {
579 use super::{super::Checksum, *};
580 use crate::{
581 buffer::paged::CHECKSUM_SIZE, deterministic, telemetry::metrics::Registry, Buf, BufferPool,
582 BufferPoolConfig, Clock as _, Handle, IoBufs, IoBufsMut, Runner as _, Spawner as _,
583 Storage as _, Supervisor as _,
584 };
585 use commonware_cryptography::Crc32;
586 use commonware_macros::test_traced;
587 use commonware_utils::{channel::oneshot, sync::Mutex, NZUsize, NZU16};
588 use futures::future::pending;
589 use rstest::rstest;
590 use std::{
591 num::NonZeroU16,
592 sync::{
593 atomic::{AtomicUsize, Ordering},
594 Arc,
595 },
596 time::Duration,
597 };
598
599 fn test_pool() -> BufferPool {
600 let mut registry = Registry::default();
601 BufferPool::new(BufferPoolConfig::for_storage(), &mut registry)
602 }
603
604 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
606 const PAGE_SIZE_U64: u64 = PAGE_SIZE.get() as u64;
607
608 fn expected_cached_bytes(logical_offset: u64, len: usize) -> Vec<u8> {
609 (0..len)
610 .map(|i| {
611 let page = (logical_offset + i as u64) / PAGE_SIZE_U64;
612 page as u8 + 1
613 })
614 .collect()
615 }
616
617 #[derive(Clone)]
619 struct BlockingBlob {
620 started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
621 }
622
623 impl Blob for BlockingBlob {
624 async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
625 self.read_at_buf(offset, len, IoBufsMut::default()).await
626 }
627
628 async fn read_at_buf(
629 &self,
630 _offset: u64,
631 _len: usize,
632 _bufs: impl Into<IoBufsMut> + Send,
633 ) -> Result<IoBufsMut, Error> {
634 let sender = self
635 .started
636 .lock()
637 .take()
638 .expect("blocking blob read started more than once");
639 let _ = sender.send(());
640 pending::<()>().await;
641 unreachable!()
642 }
643
644 async fn write_at(
645 &self,
646 _offset: u64,
647 _bufs: impl Into<crate::IoBufs> + Send,
648 ) -> Result<(), Error> {
649 Ok(())
650 }
651
652 async fn write_at_sync(
653 &self,
654 offset: u64,
655 bufs: impl Into<crate::IoBufs> + Send,
656 ) -> Result<(), Error> {
657 let bufs = bufs.into();
658 if !bufs.has_remaining() {
659 return Ok(());
660 }
661
662 self.write_at(offset, bufs).await?;
663 self.sync().await
664 }
665
666 async fn resize(&self, _len: u64) -> Result<(), Error> {
667 Ok(())
668 }
669
670 async fn sync(&self) -> Result<(), Error> {
671 Ok(())
672 }
673
674 async fn start_sync(&self) -> Handle<()> {
675 Handle::ready(self.sync().await)
676 }
677 }
678
679 #[derive(Clone)]
680 enum ControlledBlobResult {
681 Success(Arc<Vec<u8>>),
682 Error,
683 }
684
685 #[derive(Clone)]
687 struct ControlledBlob {
688 started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
689 release: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
690 reads: Arc<AtomicUsize>,
691 result: ControlledBlobResult,
692 }
693
694 impl Blob for ControlledBlob {
695 async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
696 self.read_at_buf(offset, len, IoBufsMut::default()).await
697 }
698
699 async fn read_at_buf(
700 &self,
701 _offset: u64,
702 _len: usize,
703 _bufs: impl Into<IoBufsMut> + Send,
704 ) -> Result<IoBufsMut, Error> {
705 if self.reads.fetch_add(1, Ordering::Relaxed) == 0 {
706 let sender = self
707 .started
708 .lock()
709 .take()
710 .expect("controlled blob start signal consumed more than once");
711 let _ = sender.send(());
712
713 let release = self
714 .release
715 .lock()
716 .take()
717 .expect("controlled blob release receiver consumed more than once");
718 release.await.expect("release signal dropped");
719 }
720
721 match &self.result {
722 ControlledBlobResult::Success(page) => Ok(IoBufsMut::from(page.as_ref().clone())),
723 ControlledBlobResult::Error => Err(Error::ReadFailed),
724 }
725 }
726
727 async fn write_at(
728 &self,
729 _offset: u64,
730 _bufs: impl Into<crate::IoBufs> + Send,
731 ) -> Result<(), Error> {
732 Ok(())
733 }
734
735 async fn write_at_sync(
736 &self,
737 offset: u64,
738 bufs: impl Into<crate::IoBufs> + Send,
739 ) -> Result<(), Error> {
740 let bufs = bufs.into();
741 if !bufs.has_remaining() {
742 return Ok(());
743 }
744
745 self.write_at(offset, bufs).await?;
746 self.sync().await
747 }
748
749 async fn resize(&self, _len: u64) -> Result<(), Error> {
750 Ok(())
751 }
752
753 async fn sync(&self) -> Result<(), Error> {
754 Ok(())
755 }
756
757 async fn start_sync(&self) -> Handle<()> {
758 Handle::ready(self.sync().await)
759 }
760 }
761
762 #[test_traced]
763 fn test_cache_basic() {
764 let pool = test_pool();
765 let mut cache: Cache = Cache::new(pool, PAGE_SIZE, NZUsize!(10));
766
767 let mut buf = vec![0; PAGE_SIZE.get() as usize];
769 let bytes_read = cache.read_at(0, &mut buf, 0);
770 assert_eq!(bytes_read, 0);
771
772 cache.cache(0, &[1; PAGE_SIZE.get() as usize], 0);
773 let bytes_read = cache.read_at(0, &mut buf, 0);
774 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
775 assert_eq!(buf, [1; PAGE_SIZE.get() as usize]);
776
777 cache.cache(0, &[2; PAGE_SIZE.get() as usize], 0);
779 let bytes_read = cache.read_at(0, &mut buf, 0);
780 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
781 assert_eq!(buf, [2; PAGE_SIZE.get() as usize]);
782
783 for i in 0u64..11 {
785 cache.cache(0, &[i as u8; PAGE_SIZE.get() as usize], i);
786 }
787 let bytes_read = cache.read_at(0, &mut buf, 0);
789 assert_eq!(bytes_read, 0);
790 for i in 1u64..11 {
792 let bytes_read = cache.read_at(0, &mut buf, i * PAGE_SIZE_U64);
793 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
794 assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
795 }
796
797 let mut buf = vec![0; PAGE_SIZE.get() as usize];
800 let bytes_read = cache.read_at(0, &mut buf, PAGE_SIZE_U64 + 2);
801 assert_eq!(bytes_read, PAGE_SIZE.get() as usize - 2);
802 assert_eq!(
803 &buf[..PAGE_SIZE.get() as usize - 2],
804 [1; PAGE_SIZE.get() as usize - 2]
805 );
806 }
807
808 #[test_traced]
809 fn test_invalidate_from_does_not_orphan_re_cached_page() {
810 let mut registry = Registry::default();
814 let pool = BufferPool::new(BufferPoolConfig::for_storage(), &mut registry);
815 let mut cache: Cache = Cache::new(pool, PAGE_SIZE, NZUsize!(2));
816 let blob_id = 0u64;
817 let page_size = PAGE_SIZE.get() as usize;
818
819 cache.cache(blob_id, &vec![0xAA; page_size], 0);
821 cache.cache(blob_id, &vec![0xBB; page_size], 1);
822 cache.invalidate_from(blob_id, 0);
823
824 cache.cache(blob_id, &vec![0xCC; page_size], 1);
826 let mut buf = vec![0u8; page_size];
827 assert_eq!(
828 cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64),
829 page_size,
830 "page 1 should be readable after re-cache"
831 );
832 assert_eq!(buf, vec![0xCC; page_size]);
833
834 cache.cache(blob_id, &vec![0xDD; page_size], 2);
836
837 let mut buf = vec![0u8; page_size];
839 assert_eq!(
840 cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64),
841 page_size,
842 "live page 1 was orphaned by stale-slot eviction"
843 );
844 assert_eq!(buf, vec![0xCC; page_size]);
845
846 let mut buf = vec![0u8; page_size];
848 assert_eq!(
849 cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64 * 2),
850 page_size
851 );
852 assert_eq!(buf, vec![0xDD; page_size]);
853 }
854
855 #[test_traced]
856 fn test_cache_read_with_blob() {
857 let executor = deterministic::Runner::default();
859 executor.start(|context| async move {
861 let physical_page_size = PAGE_SIZE_U64 + CHECKSUM_SIZE;
863
864 let (blob, size) = context
866 .open("test", "blob".as_bytes())
867 .await
868 .expect("Failed to open blob");
869 assert_eq!(size, 0);
870 for i in 0..11 {
871 let logical_data = vec![i as u8; PAGE_SIZE.get() as usize];
873 let crc = Crc32::checksum(&logical_data);
874 let record = Checksum::new(PAGE_SIZE.get(), crc);
875 let mut page_data = logical_data;
876 page_data.extend_from_slice(&record.to_bytes());
877 blob.write_at(i * physical_page_size, page_data)
878 .await
879 .unwrap();
880 }
881
882 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
884 assert_eq!(cache_ref.next_id(), 0);
885 assert_eq!(cache_ref.next_id(), 1);
886 for i in 0..11 {
887 let mut buf = vec![0; PAGE_SIZE.get() as usize];
889 cache_ref
890 .read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
891 .await
892 .unwrap();
893 assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
894 }
895
896 for i in 1..11 {
899 let mut buf = vec![0; PAGE_SIZE.get() as usize];
900 cache_ref
901 .read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
902 .await
903 .unwrap();
904 assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
905 }
906
907 blob.sync().await.unwrap();
909 });
910 }
911
912 #[test_traced]
913 fn test_cache_clear_forces_blob_read() {
914 #[derive(Clone)]
915 struct CountingBlob {
916 reads: Arc<AtomicUsize>,
917 page: Arc<Vec<u8>>,
918 }
919
920 impl Blob for CountingBlob {
921 async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
922 self.read_at_buf(offset, len, IoBufsMut::default()).await
923 }
924
925 async fn read_at_buf(
926 &self,
927 _offset: u64,
928 _len: usize,
929 _bufs: impl Into<IoBufsMut> + Send,
930 ) -> Result<IoBufsMut, Error> {
931 self.reads.fetch_add(1, Ordering::Relaxed);
932 Ok(IoBufsMut::from(self.page.as_ref().clone()))
933 }
934
935 async fn write_at(
936 &self,
937 _offset: u64,
938 _bufs: impl Into<IoBufs> + Send,
939 ) -> Result<(), Error> {
940 Ok(())
941 }
942
943 async fn write_at_sync(
944 &self,
945 offset: u64,
946 bufs: impl Into<IoBufs> + Send,
947 ) -> Result<(), Error> {
948 self.write_at(offset, bufs).await
949 }
950
951 async fn resize(&self, _len: u64) -> Result<(), Error> {
952 Ok(())
953 }
954
955 async fn sync(&self) -> Result<(), Error> {
956 Ok(())
957 }
958
959 async fn start_sync(&self) -> Handle<()> {
960 Handle::ready(self.sync().await)
961 }
962 }
963
964 let executor = deterministic::Runner::default();
965 executor.start(|context| async move {
966 let page = vec![7u8; PAGE_SIZE.get() as usize];
967 let crc = Crc32::checksum(&page);
968 let record = Checksum::new(PAGE_SIZE.get(), crc);
969 let mut physical_page = page.clone();
970 physical_page.extend_from_slice(&record.to_bytes());
971 let blob = CountingBlob {
972 reads: Arc::new(AtomicUsize::new(0)),
973 page: Arc::new(physical_page),
974 };
975 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
976
977 let mut buf = vec![0u8; page.len()];
978 cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
979 assert_eq!(buf, page);
980 assert_eq!(blob.reads.load(Ordering::Relaxed), 1);
981
982 let mut buf = vec![0u8; page.len()];
983 cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
984 assert_eq!(buf, page);
985 assert_eq!(blob.reads.load(Ordering::Relaxed), 1);
986
987 cache_ref.clear();
988
989 let mut buf = vec![0u8; page.len()];
990 cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
991 assert_eq!(buf, page);
992 assert_eq!(blob.reads.load(Ordering::Relaxed), 2);
993 });
994 }
995
996 #[test_traced]
997 fn test_cache_max_page() {
998 let executor = deterministic::Runner::default();
999 executor.start(|context| async move {
1000 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
1001
1002 let aligned_max_offset = u64::MAX - (u64::MAX % PAGE_SIZE_U64);
1004
1005 let logical_data = vec![42u8; PAGE_SIZE.get() as usize];
1007
1008 let remaining = cache_ref.cache(0, logical_data.as_slice(), aligned_max_offset);
1010 assert_eq!(remaining, 0);
1011
1012 let mut buf = vec![0u8; PAGE_SIZE.get() as usize];
1014 let page_cache = cache_ref.cache.read();
1015 let bytes_read = page_cache.read_at(0, &mut buf, aligned_max_offset);
1016 assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
1017 assert!(buf.iter().all(|b| *b == 42));
1018 });
1019 }
1020
1021 #[test_traced]
1022 fn test_cache_at_high_offset() {
1023 let executor = deterministic::Runner::default();
1024 executor.start(|context| async move {
1025 const MIN_PAGE_SIZE: u64 = CHECKSUM_SIZE + 1;
1027 let cache_ref =
1028 CacheRef::from_pooler(&context, NZU16!(MIN_PAGE_SIZE as u16), NZUsize!(2));
1029
1030 let data = vec![1u8; MIN_PAGE_SIZE as usize * 2];
1033
1034 let aligned_max_offset = u64::MAX - (u64::MAX % MIN_PAGE_SIZE);
1037 let high_offset = aligned_max_offset - (MIN_PAGE_SIZE * 2);
1038 let remaining = cache_ref.cache(0, &data, high_offset);
1039 assert_eq!(remaining, 0);
1041
1042 let mut buf = vec![0u8; MIN_PAGE_SIZE as usize];
1044 let page_cache = cache_ref.cache.read();
1045 assert_eq!(
1046 page_cache.read_at(0, &mut buf, high_offset),
1047 MIN_PAGE_SIZE as usize
1048 );
1049 assert!(buf.iter().all(|b| *b == 1));
1050
1051 assert_eq!(
1053 page_cache.read_at(0, &mut buf, high_offset + MIN_PAGE_SIZE),
1054 MIN_PAGE_SIZE as usize
1055 );
1056 assert!(buf.iter().all(|b| *b == 1));
1057 });
1058 }
1059
1060 #[test_traced]
1061 fn test_page_fetches_entry_removed_when_first_fetcher_cancelled() {
1062 let executor = deterministic::Runner::default();
1063 executor.start(|context| async move {
1064 let blob_id = 0;
1066 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1067 let (started_tx, started_rx) = oneshot::channel();
1068 let blob = BlockingBlob {
1069 started: Arc::new(Mutex::new(Some(started_tx))),
1070 };
1071 let mut read_buf = vec![0u8; PAGE_SIZE.get() as usize];
1072
1073 let cache_ref_for_task = cache_ref.clone();
1075 let blob_for_task = blob.clone();
1076 let handle = context.spawn(move |_| async move {
1077 let _ = cache_ref_for_task
1078 .read(&blob_for_task, blob_id, &mut read_buf, 0)
1079 .await;
1080 });
1081
1082 started_rx.await.expect("blocking read never started");
1084 {
1085 let page_cache = cache_ref.cache.read();
1086 assert!(page_cache.page_fetches.contains_key(&(blob_id, 0)));
1087 }
1088
1089 handle.abort();
1091 assert!(matches!(handle.await, Err(Error::Closed)));
1092
1093 let page_cache = cache_ref.cache.read();
1095 assert!(
1096 !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1097 "cancelled first fetcher should not leave stale page_fetches entry"
1098 );
1099 });
1100 }
1101
1102 #[test_traced]
1103 fn test_followers_keep_single_flight_after_first_fetcher_cancellation() {
1104 let executor = deterministic::Runner::default();
1105 executor.start(|context| async move {
1106 let blob_id = 0;
1107 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1108
1109 let logical_page = vec![7u8; PAGE_SIZE.get() as usize];
1111 let crc = Crc32::checksum(&logical_page);
1112 let mut physical_page = logical_page.clone();
1113 physical_page.extend_from_slice(&Checksum::new(PAGE_SIZE.get(), crc).to_bytes());
1114 let (started_tx, started_rx) = oneshot::channel();
1115 let (release_tx, release_rx) = oneshot::channel();
1116 let reads = Arc::new(AtomicUsize::new(0));
1117 let blob = ControlledBlob {
1118 started: Arc::new(Mutex::new(Some(started_tx))),
1119 release: Arc::new(Mutex::new(Some(release_rx))),
1120 reads: reads.clone(),
1121 result: ControlledBlobResult::Success(Arc::new(physical_page)),
1122 };
1123
1124 let mut first_buf = vec![0u8; PAGE_SIZE.get() as usize];
1126 let cache_ref_for_first = cache_ref.clone();
1127 let blob_for_first = blob.clone();
1128 let first = context.child("first").spawn(move |_| async move {
1129 let _ = cache_ref_for_first
1130 .read(&blob_for_first, blob_id, &mut first_buf, 0)
1131 .await;
1132 });
1133 started_rx.await.expect("first read never started");
1134
1135 let mut second_buf = vec![0u8; PAGE_SIZE.get() as usize];
1137 let cache_ref_for_second = cache_ref.clone();
1138 let blob_for_second = blob.clone();
1139 let second = context.child("second").spawn(move |_| async move {
1140 cache_ref_for_second
1141 .read(&blob_for_second, blob_id, &mut second_buf, 0)
1142 .await
1143 .expect("second read failed");
1144 second_buf
1145 });
1146
1147 loop {
1149 let joined = {
1150 let page_cache = cache_ref.cache.read();
1151 page_cache
1152 .page_fetches
1153 .get(&(blob_id, 0))
1154 .map(|fetch| fetch.waiters == 2)
1155 .unwrap_or(false)
1156 };
1157 if joined {
1158 break;
1159 }
1160 context.sleep(Duration::from_millis(1)).await;
1161 }
1162
1163 first.abort();
1165 assert!(matches!(first.await, Err(Error::Closed)));
1166
1167 let mut third_buf = vec![0u8; PAGE_SIZE.get() as usize];
1170 let cache_ref_for_third = cache_ref.clone();
1171 let blob_for_third = blob.clone();
1172 let third = context.child("third").spawn(move |_| async move {
1173 cache_ref_for_third
1174 .read(&blob_for_third, blob_id, &mut third_buf, 0)
1175 .await
1176 .expect("third read failed");
1177 third_buf
1178 });
1179
1180 loop {
1183 let third_entered = {
1184 let page_cache = cache_ref.cache.read();
1185 reads.load(Ordering::Relaxed) > 1
1186 || page_cache
1187 .page_fetches
1188 .get(&(blob_id, 0))
1189 .map(|fetch| fetch.waiters == 2)
1190 .unwrap_or(false)
1191 };
1192 if third_entered {
1193 break;
1194 }
1195 context.sleep(Duration::from_millis(1)).await;
1196 }
1197
1198 let _ = release_tx.send(());
1200 let second_buf = second.await.expect("second task failed");
1201 let third_buf = third.await.expect("third task failed");
1202 assert_eq!(second_buf, logical_page);
1203 assert_eq!(third_buf, logical_page);
1204
1205 assert_eq!(reads.load(Ordering::Relaxed), 1);
1207
1208 let mut cached = vec![0u8; PAGE_SIZE.get() as usize];
1210 assert_eq!(
1211 cache_ref.read_cached(blob_id, &mut cached, 0),
1212 PAGE_SIZE.get() as usize
1213 );
1214 assert_eq!(cached, logical_page);
1215
1216 let mut fourth_buf = vec![0u8; PAGE_SIZE.get() as usize];
1218 cache_ref
1219 .read(&blob, blob_id, &mut fourth_buf, 0)
1220 .await
1221 .unwrap();
1222 assert_eq!(fourth_buf, logical_page);
1223 assert_eq!(reads.load(Ordering::Relaxed), 1);
1224
1225 let page_cache = cache_ref.cache.read();
1226 assert!(
1227 !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1228 "completed fetch should leave no stale page_fetches entry"
1229 );
1230 });
1231 }
1232
1233 #[test_traced]
1234 fn test_page_fetch_error_removes_entry_for_all_waiters() {
1235 let executor = deterministic::Runner::default();
1236 executor.start(|context| async move {
1237 let blob_id = 0;
1238 let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1239
1240 let (started_tx, started_rx) = oneshot::channel();
1242 let (release_tx, release_rx) = oneshot::channel();
1243 let reads = Arc::new(AtomicUsize::new(0));
1244 let blob = ControlledBlob {
1245 started: Arc::new(Mutex::new(Some(started_tx))),
1246 release: Arc::new(Mutex::new(Some(release_rx))),
1247 reads: reads.clone(),
1248 result: ControlledBlobResult::Error,
1249 };
1250
1251 let mut first_buf = vec![0u8; PAGE_SIZE.get() as usize];
1253 let cache_ref_for_first = cache_ref.clone();
1254 let blob_for_first = blob.clone();
1255 let first = context.child("first").spawn(move |_| async move {
1256 cache_ref_for_first
1257 .read(&blob_for_first, blob_id, &mut first_buf, 0)
1258 .await
1259 });
1260 started_rx.await.expect("first erroring read never started");
1261
1262 let mut second_buf = vec![0u8; PAGE_SIZE.get() as usize];
1264 let cache_ref_for_second = cache_ref.clone();
1265 let blob_for_second = blob.clone();
1266 let second = context.child("second").spawn(move |_| async move {
1267 cache_ref_for_second
1268 .read(&blob_for_second, blob_id, &mut second_buf, 0)
1269 .await
1270 });
1271
1272 loop {
1274 let joined = {
1275 let page_cache = cache_ref.cache.read();
1276 page_cache
1277 .page_fetches
1278 .get(&(blob_id, 0))
1279 .map(|fetch| fetch.waiters == 2)
1280 .unwrap_or(false)
1281 };
1282 if joined {
1283 break;
1284 }
1285 context.sleep(Duration::from_millis(1)).await;
1286 }
1287
1288 let _ = release_tx.send(());
1290
1291 assert!(matches!(first.await, Ok(Err(Error::ReadFailed))));
1292 assert!(matches!(second.await, Ok(Err(Error::ReadFailed))));
1293 assert_eq!(reads.load(Ordering::Relaxed), 1);
1295
1296 {
1298 let page_cache = cache_ref.cache.read();
1299 assert!(
1300 !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1301 "erroring fetch should leave no stale page_fetches entry"
1302 );
1303 }
1304 let mut cached = vec![0u8; PAGE_SIZE.get() as usize];
1305 assert_eq!(cache_ref.read_cached(blob_id, &mut cached, 0), 0);
1306
1307 let mut third_buf = vec![0u8; PAGE_SIZE.get() as usize];
1309 assert!(matches!(
1310 cache_ref.read(&blob, blob_id, &mut third_buf, 0).await,
1311 Err(Error::ReadFailed)
1312 ));
1313 assert_eq!(reads.load(Ordering::Relaxed), 2);
1314 });
1315 }
1316
1317 #[test_traced]
1318 fn test_read_cached_many_all_cached() {
1319 let pool = test_pool();
1320 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1321 let blob_id = cache_ref.next_id();
1322 let page0 = vec![0xAA; PAGE_SIZE.get() as usize];
1323 let page1 = vec![0xBB; PAGE_SIZE.get() as usize];
1324
1325 {
1327 let mut cache = cache_ref.cache.write();
1328 cache.cache(blob_id, &page0, 0);
1329 cache.cache(blob_id, &page1, 1);
1330 }
1331
1332 let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1333 let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1334 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf0, 0), (&mut buf1, PAGE_SIZE_U64)];
1335
1336 cache_ref.read_cached_many(blob_id, &mut ranges);
1337
1338 assert!(ranges.is_empty());
1340 drop(ranges);
1341
1342 assert!(buf0 == page0);
1344 assert!(buf1 == page1);
1345 }
1346
1347 #[test_traced]
1348 fn test_read_cached_many_none_cached() {
1349 let pool = test_pool();
1350 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1351 let blob_id = cache_ref.next_id();
1352
1353 let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1354 let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1355 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf0, 0), (&mut buf1, PAGE_SIZE_U64)];
1356
1357 cache_ref.read_cached_many(blob_id, &mut ranges);
1359 assert_eq!(ranges.len(), 2);
1360 assert_eq!(ranges[0].1, 0);
1361 assert_eq!(ranges[1].1, PAGE_SIZE_U64);
1362 }
1363
1364 #[test_traced]
1365 fn test_read_cached_many_scattered_misses() {
1366 let pool = test_pool();
1369 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1370 let blob_id = cache_ref.next_id();
1371
1372 let page0 = vec![0x11; PAGE_SIZE.get() as usize];
1373 let page2 = vec![0x33; PAGE_SIZE.get() as usize];
1374 {
1375 let mut cache = cache_ref.cache.write();
1376 cache.cache(blob_id, &page0, 0);
1377 cache.cache(blob_id, &page2, 2);
1379 }
1380
1381 let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1382 let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1383 let mut buf2 = vec![0u8; PAGE_SIZE_U64 as usize];
1384 let mut ranges: Vec<(&mut [u8], u64)> = vec![
1385 (&mut buf0, 0),
1386 (&mut buf1, PAGE_SIZE_U64),
1387 (&mut buf2, PAGE_SIZE_U64 * 2),
1388 ];
1389
1390 cache_ref.read_cached_many(blob_id, &mut ranges);
1391
1392 assert_eq!(ranges.len(), 1);
1395 assert_eq!(ranges[0].1, PAGE_SIZE_U64);
1396 drop(ranges);
1397
1398 assert!(buf0 == page0);
1400 assert!(buf2 == page2);
1401 assert!(buf1.iter().all(|b| *b == 0));
1403 }
1404
1405 #[test_traced]
1406 fn test_read_cached_many_stale_hint_after_eviction() {
1407 let pool = test_pool();
1412 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(2));
1413 let blob_id = cache_ref.next_id();
1414 let page_size = PAGE_SIZE.get() as usize;
1415 {
1416 let mut cache = cache_ref.cache.write();
1417 for page in 0u64..3 {
1418 cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1419 }
1420 }
1421
1422 let mut bufs: Vec<Vec<u8>> = (0..3).map(|_| vec![0u8; page_size]).collect();
1423 let mut iter = bufs.iter_mut();
1424 let mut ranges: Vec<(&mut [u8], u64)> = (0..3u64)
1425 .map(|page| (iter.next().unwrap().as_mut_slice(), page * PAGE_SIZE_U64))
1426 .collect();
1427 cache_ref.read_cached_many(blob_id, &mut ranges);
1428
1429 assert_eq!(ranges.len(), 1);
1431 assert_eq!(ranges[0].1, 0);
1432 drop(ranges);
1433 assert!(bufs[0].iter().all(|b| *b == 0));
1434 assert_eq!(bufs[1], vec![2u8; page_size]);
1435 assert_eq!(bufs[2], vec![3u8; page_size]);
1436 }
1437
1438 #[test_traced]
1439 fn test_read_cached_many_cross_blob_hint_collision() {
1440 let pool = test_pool();
1445 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(4));
1446 let blob_a = cache_ref.next_id();
1447 let blob_b = cache_ref.next_id();
1448 let page_size = PAGE_SIZE.get() as usize;
1449 let page_a = 5u64;
1450 let page_b = {
1451 let mut cache = cache_ref.cache.write();
1452
1453 let mask = cache.hints.len() as u64 - 1;
1455 let page_b = page_a
1456 .wrapping_add(blob_a.wrapping_mul(commonware_utils::GOLDEN_RATIO))
1457 .wrapping_sub(blob_b.wrapping_mul(commonware_utils::GOLDEN_RATIO))
1458 & mask;
1459 assert_eq!(
1460 cache.hint_index(blob_a, page_a),
1461 cache.hint_index(blob_b, page_b)
1462 );
1463 cache.cache(blob_a, &vec![0xAA; page_size], page_a);
1464 cache.cache(blob_b, &vec![0xBB; page_size], page_b);
1465 page_b
1466 };
1467
1468 for (blob, page, byte) in [(blob_a, page_a, 0xAAu8), (blob_b, page_b, 0xBB)] {
1469 let mut buf = vec![0u8; page_size];
1470 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page * PAGE_SIZE_U64)];
1471 cache_ref.read_cached_many(blob, &mut ranges);
1472 assert!(
1473 ranges.is_empty(),
1474 "blob {blob} page {page} should be cached"
1475 );
1476 drop(ranges);
1477 assert_eq!(buf, vec![byte; page_size]);
1478 }
1479 }
1480
1481 #[test_traced]
1482 fn test_read_cached_many_sparse_page_number_keeps_hints_fixed() {
1483 let pool = test_pool();
1486 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(2));
1487 let blob_id = cache_ref.next_id();
1488 let page_size = PAGE_SIZE.get() as usize;
1489 let page_num = u64::MAX / PAGE_SIZE_U64 - 1;
1490 {
1491 let mut cache = cache_ref.cache.write();
1492 let hints = cache.hints.len();
1493 cache.cache(blob_id, &vec![0x5A; page_size], page_num);
1494 assert_eq!(cache.hints.len(), hints);
1495 }
1496
1497 let mut buf = vec![0u8; page_size];
1498 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page_num * PAGE_SIZE_U64)];
1499 cache_ref.read_cached_many(blob_id, &mut ranges);
1500 assert!(ranges.is_empty());
1501 drop(ranges);
1502 assert_eq!(buf, vec![0x5A; page_size]);
1503 }
1504
1505 #[test_traced]
1506 fn test_read_cached_many_invalidated_page_is_a_miss() {
1507 let pool = test_pool();
1511 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(4));
1512 let blob_id = cache_ref.next_id();
1513 let page_size = PAGE_SIZE.get() as usize;
1514 {
1515 let mut cache = cache_ref.cache.write();
1516 for page in 0u64..4 {
1517 cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1518 }
1519 }
1520 cache_ref.invalidate_from(blob_id, 2);
1521
1522 let read_page = |page: u64| {
1523 let mut buf = vec![0u8; page_size];
1524 let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page * PAGE_SIZE_U64)];
1525 cache_ref.read_cached_many(blob_id, &mut ranges);
1526 let hit = ranges.is_empty();
1527 drop(ranges);
1528 hit.then_some(buf)
1529 };
1530 assert_eq!(read_page(0), Some(vec![1u8; page_size]));
1531 assert_eq!(read_page(1), Some(vec![2u8; page_size]));
1532 assert_eq!(read_page(2), None);
1533 assert_eq!(read_page(3), None);
1534
1535 {
1537 let mut cache = cache_ref.cache.write();
1538 cache.cache(blob_id, &vec![0xCC; page_size], 2);
1539 }
1540 assert_eq!(read_page(2), Some(vec![0xCC; page_size]));
1541 }
1542
1543 #[rstest]
1544 #[case::empty_read(vec![], 0, 0, 0)]
1545 #[case::single_cached_page(vec![0], 3, 5, 5)]
1546 #[case::cached_range_can_cross_pages(vec![0, 1], PAGE_SIZE_U64 - 2, 4, 4)]
1547 #[case::missing_first_page_reads_nothing(vec![1], 0, 4, 0)]
1548 #[case::missing_later_page_truncates_read(vec![0], PAGE_SIZE_U64 - 2, 4, 2)]
1549 fn test_read_cached(
1550 #[case] cached_pages: Vec<u64>,
1551 #[case] logical_offset: u64,
1552 #[case] len: usize,
1553 #[case] expected_count: usize,
1554 ) {
1555 let pool = test_pool();
1556 let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1557 let blob_id = cache_ref.next_id();
1558 let sentinel = 0xEE;
1559 let page_size = PAGE_SIZE.get() as usize;
1560
1561 {
1562 let mut cache = cache_ref.cache.write();
1563 for page in cached_pages {
1564 cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1566 }
1567 }
1568
1569 let mut buf = vec![sentinel; len];
1570 let count = cache_ref.read_cached(blob_id, &mut buf, logical_offset);
1571 assert_eq!(count, expected_count);
1572
1573 assert_eq!(buf[..count], expected_cached_bytes(logical_offset, count));
1575 assert!(buf[count..].iter().all(|b| *b == sentinel));
1576 }
1577}