1use commonware_runtime::buffer::paged::CacheRef;
81use std::num::{NonZeroU64, NonZeroUsize};
82
83#[cfg(all(test, feature = "arbitrary"))]
84mod conformance;
85mod storage;
86pub use storage::Cache;
87
88#[derive(Clone)]
90pub struct Config<C> {
91 pub partition: String,
93
94 pub compression: Option<u8>,
96
97 pub codec_config: C,
99
100 pub items_per_blob: NonZeroU64,
102
103 pub write_buffer: NonZeroUsize,
106
107 pub replay_buffer: NonZeroUsize,
109
110 pub page_cache: CacheRef,
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::journal::Error as JournalError;
118 use commonware_macros::{test_group, test_traced};
119 use commonware_runtime::{
120 Metrics as _, Runner, Supervisor as _, deterministic, mocks::RecordingContext,
121 telemetry::metrics::has_metric_value,
122 };
123 use commonware_utils::{NZU16, NZU64, NZUsize};
124 use rand::RngExt as _;
125 use std::{collections::BTreeMap, num::NonZeroU16};
126
127 const DEFAULT_ITEMS_PER_BLOB: u64 = 65536;
128 const DEFAULT_WRITE_BUFFER: usize = 1024;
129 const DEFAULT_REPLAY_BUFFER: usize = 4096;
130 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
131 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
132
133 #[test_traced]
134 fn test_cache_compression_then_none() {
135 let executor = deterministic::Runner::default();
137 executor.start(|context| async move {
138 let cfg = Config {
140 partition: "test-partition".into(),
141 codec_config: (),
142 compression: Some(3),
143 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
144 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
145 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
146 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
147 };
148 let mut cache = Cache::init(context.child("first"), cfg.clone())
149 .await
150 .expect("Failed to initialize cache");
151
152 let index = 1u64;
154 let data = 1;
155 cache = cache.put(index, data).await.expect("Failed to put data");
156
157 cache.sync().await.expect("Failed to sync cache");
159
160 let cfg = Config {
162 partition: "test-partition".into(),
163 codec_config: (),
164 compression: None,
165 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
166 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
167 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
168 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
169 };
170 let result = Cache::<_, i32>::init(context.child("second"), cfg.clone()).await;
171 assert!(matches!(result, Err(JournalError::Codec(_))));
172 });
173 }
174
175 #[test_traced]
176 fn test_cache_prune() {
177 let executor = deterministic::Runner::default();
179 executor.start(|context| async move {
180 let cfg = Config {
182 partition: "test-partition".into(),
183 codec_config: (),
184 compression: None,
185 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
186 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
187 items_per_blob: NZU64!(1), page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
189 };
190 let mut cache = Cache::init(context.child("storage"), cfg.clone())
191 .await
192 .expect("Failed to initialize cache");
193
194 let items = vec![(1u64, 1), (2u64, 2), (3u64, 3), (4u64, 4), (5u64, 5)];
196 for (index, data) in &items {
197 cache = cache.put(*index, *data).await.expect("Failed to put data");
198 }
199 assert_eq!(cache.first(), Some(1));
200
201 let buffer = context.encode();
203 assert!(has_metric_value(&buffer, "items_tracked", 5));
204
205 cache = cache.prune(3).await.expect("Failed to prune");
207
208 for (index, data) in items {
210 let retrieved = cache.get(index).await.expect("Failed to get data");
211 if index < 3 {
212 assert!(retrieved.is_none());
213 } else {
214 assert_eq!(retrieved.expect("Data not found"), data);
215 }
216 }
217 assert_eq!(cache.first(), Some(3));
218
219 let buffer = context.encode();
221 assert!(has_metric_value(&buffer, "items_tracked", 3));
222
223 cache = cache.prune(2).await.expect("Failed to prune");
225 assert_eq!(cache.first(), Some(3));
226
227 cache = cache.prune(3).await.expect("Failed to prune");
229 assert_eq!(cache.first(), Some(3));
230
231 let cache = cache.put(1, 1).await.expect("Failed to put below floor");
233 assert_eq!(cache.get(1).await.expect("Failed to get data"), None);
234 assert!(!cache.has(1));
235
236 let cache = cache
238 .put_sync(1, 1)
239 .await
240 .expect("Failed to put_sync below floor");
241 assert_eq!(cache.get(1).await.expect("Failed to get data"), None);
242 });
243 }
244
245 fn test_cache_restart(num_items: usize) -> String {
246 let executor = deterministic::Runner::default();
248 executor.start(|mut context| async move {
249 let items_per_blob = 256u64;
251 let cfg = Config {
252 partition: "test-partition".into(),
253 codec_config: (),
254 compression: None,
255 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
256 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
257 items_per_blob: NZU64!(items_per_blob),
258 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
259 };
260 let mut cache = Cache::init(
261 context.child("init").with_attribute("index", 1),
262 cfg.clone(),
263 )
264 .await
265 .expect("Failed to initialize cache");
266
267 let mut items = BTreeMap::new();
269 while items.len() < num_items {
270 let index = items.len() as u64;
271 let mut data = [0u8; 1024];
272 context.fill(&mut data);
273 items.insert(index, data);
274
275 cache = cache.put(index, data).await.expect("Failed to put data");
276 }
277
278 for (index, data) in &items {
280 let retrieved = cache
281 .get(*index)
282 .await
283 .expect("Failed to get data")
284 .expect("Data not found");
285 assert_eq!(retrieved, *data);
286 }
287
288 let buffer = context.encode();
290 assert!(has_metric_value(&buffer, "items_tracked", num_items));
291
292 cache.sync().await.expect("Failed to sync cache");
294
295 let cfg = Config {
297 partition: "test-partition".into(),
298 codec_config: (),
299 compression: None,
300 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
301 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
302 items_per_blob: NZU64!(items_per_blob),
303 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
304 };
305 let mut cache = Cache::<_, [u8; 1024]>::init(
306 context.child("init").with_attribute("index", 2),
307 cfg.clone(),
308 )
309 .await
310 .expect("Failed to initialize cache");
311
312 for (index, data) in &items {
314 let retrieved = cache
315 .get(*index)
316 .await
317 .expect("Failed to get data")
318 .expect("Data not found");
319 assert_eq!(&retrieved, data);
320 }
321
322 let min = (items.len() / 2) as u64;
324 cache = cache.prune(min).await.expect("Failed to prune");
325
326 let min = (min / items_per_blob) * items_per_blob;
328 let mut removed = 0;
329 for (index, data) in items {
330 if index >= min {
331 let retrieved = cache
332 .get(index)
333 .await
334 .expect("Failed to get data")
335 .expect("Data not found");
336 assert_eq!(retrieved, data);
337 } else {
338 let retrieved = cache.get(index).await.expect("Failed to get data");
339 assert!(retrieved.is_none());
340 removed += 1;
341 }
342 }
343
344 let buffer = context.encode();
346 assert!(has_metric_value(
347 &buffer,
348 "items_tracked",
349 num_items - removed
350 ));
351
352 context.auditor().state()
353 })
354 }
355
356 #[test_traced]
357 fn test_cache_clean_restart_reads_journal_once() {
358 deterministic::Runner::default().start(|context| async move {
359 let (context, recordings) = RecordingContext::new(context);
360 let config = |context: &RecordingContext<_>| Config {
361 partition: "clean-restart-single-pass".into(),
362 codec_config: (),
363 compression: None,
364 write_buffer: NZUsize!(256),
365 replay_buffer: NZUsize!(1024),
366 items_per_blob: NZU64!(64),
367 page_cache: CacheRef::from_pooler(context, NZU16!(64), NZUsize!(10)),
368 };
369
370 let mut cache = Cache::<_, u64>::init(context.child("seed"), config(&context))
371 .await
372 .expect("failed to initialize cache");
373 for index in 0..15 {
374 cache = cache.put(index, index).await.expect("failed to put");
375 }
376 cache = cache.sync().await.expect("failed to sync");
377 drop(cache);
378
379 recordings.clear();
380 let cache = Cache::<_, u64>::init(context.child("reopen"), config(&context))
381 .await
382 .expect("failed to reopen cache");
383 for index in 0..15 {
384 assert!(cache.has(index));
385 }
386
387 assert_eq!(recordings.snapshot().reads.len(), 2);
390 });
391 }
392
393 #[test_group("slow")]
394 #[test_traced]
395 fn test_cache_many_items_and_restart() {
396 test_cache_restart(100_000);
397 }
398
399 #[test_group("slow")]
400 #[test_traced]
401 fn test_determinism() {
402 let state1 = test_cache_restart(5_000);
403 let state2 = test_cache_restart(5_000);
404 assert_eq!(state1, state2);
405 }
406
407 #[test_traced]
408 fn test_cache_next_gap() {
409 let executor = deterministic::Runner::default();
410 executor.start(|context| async move {
411 let cfg = Config {
412 partition: "test-partition".into(),
413 codec_config: (),
414 compression: None,
415 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
416 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
417 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
418 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
419 };
420 let mut cache = Cache::init(context.child("storage"), cfg.clone())
421 .await
422 .expect("Failed to initialize cache");
423
424 assert_eq!(cache.first(), None);
426
427 cache = cache.put(1, 1).await.unwrap();
429 cache = cache.put(10, 10).await.unwrap();
430 cache = cache.put(11, 11).await.unwrap();
431 cache = cache.put(14, 14).await.unwrap();
432
433 let (current_end, start_next) = cache.next_gap(0);
435 assert!(current_end.is_none());
436 assert_eq!(start_next, Some(1));
437 assert_eq!(cache.first(), Some(1));
438
439 let (current_end, start_next) = cache.next_gap(1);
440 assert_eq!(current_end, Some(1));
441 assert_eq!(start_next, Some(10));
442
443 let (current_end, start_next) = cache.next_gap(10);
444 assert_eq!(current_end, Some(11));
445 assert_eq!(start_next, Some(14));
446
447 let (current_end, start_next) = cache.next_gap(11);
448 assert_eq!(current_end, Some(11));
449 assert_eq!(start_next, Some(14));
450
451 let (current_end, start_next) = cache.next_gap(12);
452 assert!(current_end.is_none());
453 assert_eq!(start_next, Some(14));
454
455 let (current_end, start_next) = cache.next_gap(14);
456 assert_eq!(current_end, Some(14));
457 assert!(start_next.is_none());
458 });
459 }
460
461 #[test_traced]
462 fn test_cache_missing_items() {
463 let executor = deterministic::Runner::default();
464 executor.start(|context| async move {
465 let cfg = Config {
466 partition: "test-partition".into(),
467 codec_config: (),
468 compression: None,
469 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
470 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
471 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
472 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
473 };
474 let mut cache = Cache::init(context.child("storage"), cfg.clone())
475 .await
476 .expect("Failed to initialize cache");
477
478 assert_eq!(cache.first(), None);
480 assert_eq!(cache.missing_items(0, 5), Vec::<u64>::new());
481 assert_eq!(cache.missing_items(100, 10), Vec::<u64>::new());
482
483 cache = cache.put(1, 1).await.unwrap();
485 cache = cache.put(2, 2).await.unwrap();
486 cache = cache.put(5, 5).await.unwrap();
487 cache = cache.put(6, 6).await.unwrap();
488 cache = cache.put(10, 10).await.unwrap();
489
490 assert_eq!(cache.missing_items(0, 5), vec![0, 3, 4, 7, 8]);
492 assert_eq!(cache.missing_items(0, 6), vec![0, 3, 4, 7, 8, 9]);
493 assert_eq!(cache.missing_items(0, 7), vec![0, 3, 4, 7, 8, 9]);
494
495 assert_eq!(cache.missing_items(3, 3), vec![3, 4, 7]);
497 assert_eq!(cache.missing_items(4, 2), vec![4, 7]);
498
499 assert_eq!(cache.missing_items(1, 3), vec![3, 4, 7]);
501 assert_eq!(cache.missing_items(2, 4), vec![3, 4, 7, 8]);
502 assert_eq!(cache.missing_items(5, 2), vec![7, 8]);
503
504 assert_eq!(cache.missing_items(11, 5), Vec::<u64>::new());
506 assert_eq!(cache.missing_items(100, 10), Vec::<u64>::new());
507
508 cache = cache.put(1000, 1000).await.unwrap();
510
511 let items = cache.missing_items(11, 10);
513 assert_eq!(items, vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
514
515 let items = cache.missing_items(990, 15);
517 assert_eq!(
518 items,
519 vec![990, 991, 992, 993, 994, 995, 996, 997, 998, 999]
520 );
521
522 cache = cache.sync().await.unwrap();
524 assert_eq!(cache.missing_items(0, 5), vec![0, 3, 4, 7, 8]);
525 assert_eq!(cache.missing_items(3, 3), vec![3, 4, 7]);
526
527 cache = cache.put(DEFAULT_ITEMS_PER_BLOB - 1, 99).await.unwrap();
529 cache = cache.put(DEFAULT_ITEMS_PER_BLOB + 1, 101).await.unwrap();
530
531 let items = cache.missing_items(DEFAULT_ITEMS_PER_BLOB - 2, 5);
533 assert_eq!(
534 items,
535 vec![DEFAULT_ITEMS_PER_BLOB - 2, DEFAULT_ITEMS_PER_BLOB]
536 );
537 });
538 }
539
540 #[test_traced]
541 fn test_cache_intervals_after_restart() {
542 let executor = deterministic::Runner::default();
543 executor.start(|context| async move {
544 let cfg = Config {
545 partition: "test-partition".into(),
546 codec_config: (),
547 compression: None,
548 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
549 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
550 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
551 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
552 };
553
554 {
556 let mut cache = Cache::init(context.child("first"), cfg.clone())
557 .await
558 .expect("Failed to initialize cache");
559
560 cache = cache.put(0, 0).await.expect("Failed to put data");
561 cache = cache.put(100, 100).await.expect("Failed to put data");
562 cache = cache.put(1000, 1000).await.expect("Failed to put data");
563
564 cache.sync().await.expect("Failed to sync cache");
565 }
566
567 {
569 let cache = Cache::<_, i32>::init(context.child("second"), cfg.clone())
570 .await
571 .expect("Failed to initialize cache");
572
573 let (current_end, start_next) = cache.next_gap(0);
575 assert_eq!(current_end, Some(0));
576 assert_eq!(start_next, Some(100));
577
578 let (current_end, start_next) = cache.next_gap(100);
579 assert_eq!(current_end, Some(100));
580 assert_eq!(start_next, Some(1000));
581
582 let items = cache.missing_items(1, 5);
584 assert_eq!(items, vec![1, 2, 3, 4, 5]);
585 }
586 });
587 }
588
589 #[test_traced]
590 fn test_cache_intervals_with_pruning() {
591 let executor = deterministic::Runner::default();
592 executor.start(|context| async move {
593 let cfg = Config {
594 partition: "test-partition".into(),
595 codec_config: (),
596 compression: None,
597 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
598 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
599 items_per_blob: NZU64!(100), page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
601 };
602 let mut cache = Cache::init(context.child("storage"), cfg.clone())
603 .await
604 .expect("Failed to initialize cache");
605
606 cache = cache.put(50, 50).await.unwrap();
608 cache = cache.put(150, 150).await.unwrap();
609 cache = cache.put(250, 250).await.unwrap();
610 cache = cache.put(350, 350).await.unwrap();
611
612 let (current_end, start_next) = cache.next_gap(0);
614 assert!(current_end.is_none());
615 assert_eq!(start_next, Some(50));
616
617 cache = cache.prune(200).await.expect("Failed to prune");
619
620 assert!(!cache.has(50));
622 assert!(!cache.has(150));
623
624 let (current_end, start_next) = cache.next_gap(200);
626 assert!(current_end.is_none());
627 assert_eq!(start_next, Some(250));
628
629 let items = cache.missing_items(200, 5);
631 assert_eq!(items, vec![200, 201, 202, 203, 204]);
632
633 assert!(cache.has(250));
635 assert!(cache.has(350));
636 assert_eq!(cache.get(250).await.unwrap(), Some(250));
637 assert_eq!(cache.get(350).await.unwrap(), Some(350));
638 });
639 }
640
641 #[test_traced]
642 fn test_cache_sparse_indices() {
643 let executor = deterministic::Runner::default();
644 executor.start(|context| async move {
645 let cfg = Config {
646 partition: "test-partition".into(),
647 codec_config: (),
648 compression: None,
649 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
650 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
651 items_per_blob: NZU64!(100), page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
653 };
654 let mut cache = Cache::init(context.child("storage"), cfg.clone())
655 .await
656 .expect("Failed to initialize cache");
657
658 let indices = vec![
660 (0u64, 0),
661 (99u64, 99), (100u64, 100), (500u64, 500), ];
665
666 for (index, value) in &indices {
667 cache = cache.put(*index, *value).await.expect("Failed to put data");
668 }
669
670 assert!(!cache.has(1));
672 assert!(!cache.has(50));
673 assert!(!cache.has(101));
674 assert!(!cache.has(499));
675
676 let (current_end, start_next) = cache.next_gap(50);
678 assert!(current_end.is_none());
679 assert_eq!(start_next, Some(99));
680
681 let (current_end, start_next) = cache.next_gap(99);
682 assert_eq!(current_end, Some(100));
683 assert_eq!(start_next, Some(500));
684
685 cache = cache.sync().await.expect("Failed to sync");
687
688 for (index, value) in &indices {
689 let retrieved = cache
690 .get(*index)
691 .await
692 .expect("Failed to get data")
693 .expect("Data not found");
694 assert_eq!(retrieved, *value);
695 }
696 });
697 }
698
699 #[test_traced]
700 fn test_cache_intervals_edge_cases() {
701 let executor = deterministic::Runner::default();
702 executor.start(|context| async move {
703 let cfg = Config {
704 partition: "test-partition".into(),
705 codec_config: (),
706 compression: None,
707 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
708 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
709 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
710 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
711 };
712 let mut cache = Cache::init(context.child("storage"), cfg.clone())
713 .await
714 .expect("Failed to initialize cache");
715
716 cache = cache.put(42, 42).await.unwrap();
718
719 let (current_end, start_next) = cache.next_gap(42);
720 assert_eq!(current_end, Some(42));
721 assert!(start_next.is_none());
722
723 let (current_end, start_next) = cache.next_gap(41);
724 assert!(current_end.is_none());
725 assert_eq!(start_next, Some(42));
726
727 let (current_end, start_next) = cache.next_gap(43);
728 assert!(current_end.is_none());
729 assert!(start_next.is_none());
730
731 cache = cache.put(43, 43).await.unwrap();
733 cache = cache.put(44, 44).await.unwrap();
734
735 let (current_end, start_next) = cache.next_gap(42);
736 assert_eq!(current_end, Some(44));
737 assert!(start_next.is_none());
738
739 cache = cache.put(u64::MAX - 1, 999).await.unwrap();
741
742 let (current_end, start_next) = cache.next_gap(u64::MAX - 2);
743 assert!(current_end.is_none());
744 assert_eq!(start_next, Some(u64::MAX - 1));
745
746 let (current_end, start_next) = cache.next_gap(u64::MAX - 1);
747 assert_eq!(current_end, Some(u64::MAX - 1));
748 assert!(start_next.is_none());
749 });
750 }
751
752 #[test_traced]
753 fn test_cache_intervals_duplicate_inserts() {
754 let executor = deterministic::Runner::default();
755 executor.start(|context| async move {
756 let cfg = Config {
757 partition: "test-partition".into(),
758 codec_config: (),
759 compression: None,
760 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
761 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
762 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
763 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
764 };
765 let mut cache = Cache::init(context.child("storage"), cfg.clone())
766 .await
767 .expect("Failed to initialize cache");
768
769 cache = cache.put(10, 10).await.unwrap();
771 assert!(cache.has(10));
772 assert_eq!(cache.get(10).await.unwrap(), Some(10));
773
774 cache = cache.put(10, 20).await.unwrap();
776 assert!(cache.has(10));
777 assert_eq!(cache.get(10).await.unwrap(), Some(10)); let (current_end, start_next) = cache.next_gap(10);
781 assert_eq!(current_end, Some(10));
782 assert!(start_next.is_none());
783
784 cache = cache.put(9, 9).await.unwrap();
786 cache = cache.put(11, 11).await.unwrap();
787
788 let (current_end, start_next) = cache.next_gap(9);
790 assert_eq!(current_end, Some(11));
791 assert!(start_next.is_none());
792 });
793 }
794}