1use commonware_runtime::buffer::paged::CacheRef;
80use std::num::{NonZeroU64, NonZeroUsize};
81use thiserror::Error;
82
83#[cfg(all(test, feature = "arbitrary"))]
84mod conformance;
85mod storage;
86pub use storage::Cache;
87
88#[derive(Debug, Error)]
90pub enum Error {
91 #[error("journal error: {0}")]
92 Journal(#[from] crate::journal::Error),
93 #[error("record corrupted")]
94 RecordCorrupted,
95 #[error("already pruned to: {0}")]
96 AlreadyPrunedTo(u64),
97 #[error("record too large")]
98 RecordTooLarge,
99}
100
101#[derive(Clone)]
103pub struct Config<C> {
104 pub partition: String,
106
107 pub compression: Option<u8>,
109
110 pub codec_config: C,
112
113 pub items_per_blob: NonZeroU64,
115
116 pub write_buffer: NonZeroUsize,
119
120 pub replay_buffer: NonZeroUsize,
122
123 pub page_cache: CacheRef,
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::journal::Error as JournalError;
131 use commonware_macros::{test_group, test_traced};
132 use commonware_runtime::{
133 deterministic, telemetry::metrics::has_metric_value, Metrics as _, Runner, Supervisor as _,
134 };
135 use commonware_utils::{NZUsize, NZU16, NZU64};
136 use rand::RngExt as _;
137 use std::{collections::BTreeMap, num::NonZeroU16};
138
139 const DEFAULT_ITEMS_PER_BLOB: u64 = 65536;
140 const DEFAULT_WRITE_BUFFER: usize = 1024;
141 const DEFAULT_REPLAY_BUFFER: usize = 4096;
142 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
143 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
144
145 #[test_traced]
146 fn test_cache_compression_then_none() {
147 let executor = deterministic::Runner::default();
149 executor.start(|context| async move {
150 let cfg = Config {
152 partition: "test-partition".into(),
153 codec_config: (),
154 compression: Some(3),
155 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
156 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
157 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
158 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
159 };
160 let mut cache = Cache::init(context.child("first"), cfg.clone())
161 .await
162 .expect("Failed to initialize cache");
163
164 let index = 1u64;
166 let data = 1;
167 cache.put(index, data).await.expect("Failed to put data");
168
169 cache.sync().await.expect("Failed to sync cache");
171 drop(cache);
172
173 let cfg = Config {
175 partition: "test-partition".into(),
176 codec_config: (),
177 compression: None,
178 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
179 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
180 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
181 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
182 };
183 let result = Cache::<_, i32>::init(context.child("second"), cfg.clone()).await;
184 assert!(matches!(
185 result,
186 Err(Error::Journal(JournalError::Codec(_)))
187 ));
188 });
189 }
190
191 #[test_traced]
192 fn test_cache_prune() {
193 let executor = deterministic::Runner::default();
195 executor.start(|context| async move {
196 let cfg = Config {
198 partition: "test-partition".into(),
199 codec_config: (),
200 compression: None,
201 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
202 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
203 items_per_blob: NZU64!(1), page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
205 };
206 let mut cache = Cache::init(context.child("storage"), cfg.clone())
207 .await
208 .expect("Failed to initialize cache");
209
210 let items = vec![(1u64, 1), (2u64, 2), (3u64, 3), (4u64, 4), (5u64, 5)];
212 for (index, data) in &items {
213 cache.put(*index, *data).await.expect("Failed to put data");
214 }
215 assert_eq!(cache.first(), Some(1));
216
217 let buffer = context.encode();
219 assert!(has_metric_value(&buffer, "items_tracked", 5));
220
221 cache.prune(3).await.expect("Failed to prune");
223
224 for (index, data) in items {
226 let retrieved = cache.get(index).await.expect("Failed to get data");
227 if index < 3 {
228 assert!(retrieved.is_none());
229 } else {
230 assert_eq!(retrieved.expect("Data not found"), data);
231 }
232 }
233 assert_eq!(cache.first(), Some(3));
234
235 let buffer = context.encode();
237 assert!(has_metric_value(&buffer, "items_tracked", 3));
238
239 cache.prune(2).await.expect("Failed to prune");
241 assert_eq!(cache.first(), Some(3));
242
243 cache.prune(3).await.expect("Failed to prune");
245 assert_eq!(cache.first(), Some(3));
246
247 let result = cache.put(1, 1).await;
249 assert!(matches!(result, Err(Error::AlreadyPrunedTo(3))));
250 });
251 }
252
253 fn test_cache_restart(num_items: usize) -> String {
254 let executor = deterministic::Runner::default();
256 executor.start(|mut context| async move {
257 let items_per_blob = 256u64;
259 let cfg = Config {
260 partition: "test-partition".into(),
261 codec_config: (),
262 compression: None,
263 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
264 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
265 items_per_blob: NZU64!(items_per_blob),
266 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
267 };
268 let mut cache = Cache::init(
269 context.child("init").with_attribute("index", 1),
270 cfg.clone(),
271 )
272 .await
273 .expect("Failed to initialize cache");
274
275 let mut items = BTreeMap::new();
277 while items.len() < num_items {
278 let index = items.len() as u64;
279 let mut data = [0u8; 1024];
280 context.fill(&mut data);
281 items.insert(index, data);
282
283 cache.put(index, data).await.expect("Failed to put data");
284 }
285
286 for (index, data) in &items {
288 let retrieved = cache
289 .get(*index)
290 .await
291 .expect("Failed to get data")
292 .expect("Data not found");
293 assert_eq!(retrieved, *data);
294 }
295
296 let buffer = context.encode();
298 assert!(has_metric_value(&buffer, "items_tracked", num_items));
299
300 cache.sync().await.expect("Failed to sync cache");
302 drop(cache);
303
304 let cfg = Config {
306 partition: "test-partition".into(),
307 codec_config: (),
308 compression: None,
309 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
310 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
311 items_per_blob: NZU64!(items_per_blob),
312 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
313 };
314 let mut cache = Cache::<_, [u8; 1024]>::init(
315 context.child("init").with_attribute("index", 2),
316 cfg.clone(),
317 )
318 .await
319 .expect("Failed to initialize cache");
320
321 for (index, data) in &items {
323 let retrieved = cache
324 .get(*index)
325 .await
326 .expect("Failed to get data")
327 .expect("Data not found");
328 assert_eq!(&retrieved, data);
329 }
330
331 let min = (items.len() / 2) as u64;
333 cache.prune(min).await.expect("Failed to prune");
334
335 let min = (min / items_per_blob) * items_per_blob;
337 let mut removed = 0;
338 for (index, data) in items {
339 if index >= min {
340 let retrieved = cache
341 .get(index)
342 .await
343 .expect("Failed to get data")
344 .expect("Data not found");
345 assert_eq!(retrieved, data);
346 } else {
347 let retrieved = cache.get(index).await.expect("Failed to get data");
348 assert!(retrieved.is_none());
349 removed += 1;
350 }
351 }
352
353 let buffer = context.encode();
355 assert!(has_metric_value(
356 &buffer,
357 "items_tracked",
358 num_items - removed
359 ));
360
361 context.auditor().state()
362 })
363 }
364
365 #[test_group("slow")]
366 #[test_traced]
367 fn test_cache_many_items_and_restart() {
368 test_cache_restart(100_000);
369 }
370
371 #[test_group("slow")]
372 #[test_traced]
373 fn test_determinism() {
374 let state1 = test_cache_restart(5_000);
375 let state2 = test_cache_restart(5_000);
376 assert_eq!(state1, state2);
377 }
378
379 #[test_traced]
380 fn test_cache_next_gap() {
381 let executor = deterministic::Runner::default();
382 executor.start(|context| async move {
383 let cfg = Config {
384 partition: "test-partition".into(),
385 codec_config: (),
386 compression: None,
387 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
388 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
389 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
390 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
391 };
392 let mut cache = Cache::init(context.child("storage"), cfg.clone())
393 .await
394 .expect("Failed to initialize cache");
395
396 assert_eq!(cache.first(), None);
398
399 cache.put(1, 1).await.unwrap();
401 cache.put(10, 10).await.unwrap();
402 cache.put(11, 11).await.unwrap();
403 cache.put(14, 14).await.unwrap();
404
405 let (current_end, start_next) = cache.next_gap(0);
407 assert!(current_end.is_none());
408 assert_eq!(start_next, Some(1));
409 assert_eq!(cache.first(), Some(1));
410
411 let (current_end, start_next) = cache.next_gap(1);
412 assert_eq!(current_end, Some(1));
413 assert_eq!(start_next, Some(10));
414
415 let (current_end, start_next) = cache.next_gap(10);
416 assert_eq!(current_end, Some(11));
417 assert_eq!(start_next, Some(14));
418
419 let (current_end, start_next) = cache.next_gap(11);
420 assert_eq!(current_end, Some(11));
421 assert_eq!(start_next, Some(14));
422
423 let (current_end, start_next) = cache.next_gap(12);
424 assert!(current_end.is_none());
425 assert_eq!(start_next, Some(14));
426
427 let (current_end, start_next) = cache.next_gap(14);
428 assert_eq!(current_end, Some(14));
429 assert!(start_next.is_none());
430 });
431 }
432
433 #[test_traced]
434 fn test_cache_missing_items() {
435 let executor = deterministic::Runner::default();
436 executor.start(|context| async move {
437 let cfg = Config {
438 partition: "test-partition".into(),
439 codec_config: (),
440 compression: None,
441 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
442 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
443 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
444 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
445 };
446 let mut cache = Cache::init(context.child("storage"), cfg.clone())
447 .await
448 .expect("Failed to initialize cache");
449
450 assert_eq!(cache.first(), None);
452 assert_eq!(cache.missing_items(0, 5), Vec::<u64>::new());
453 assert_eq!(cache.missing_items(100, 10), Vec::<u64>::new());
454
455 cache.put(1, 1).await.unwrap();
457 cache.put(2, 2).await.unwrap();
458 cache.put(5, 5).await.unwrap();
459 cache.put(6, 6).await.unwrap();
460 cache.put(10, 10).await.unwrap();
461
462 assert_eq!(cache.missing_items(0, 5), vec![0, 3, 4, 7, 8]);
464 assert_eq!(cache.missing_items(0, 6), vec![0, 3, 4, 7, 8, 9]);
465 assert_eq!(cache.missing_items(0, 7), vec![0, 3, 4, 7, 8, 9]);
466
467 assert_eq!(cache.missing_items(3, 3), vec![3, 4, 7]);
469 assert_eq!(cache.missing_items(4, 2), vec![4, 7]);
470
471 assert_eq!(cache.missing_items(1, 3), vec![3, 4, 7]);
473 assert_eq!(cache.missing_items(2, 4), vec![3, 4, 7, 8]);
474 assert_eq!(cache.missing_items(5, 2), vec![7, 8]);
475
476 assert_eq!(cache.missing_items(11, 5), Vec::<u64>::new());
478 assert_eq!(cache.missing_items(100, 10), Vec::<u64>::new());
479
480 cache.put(1000, 1000).await.unwrap();
482
483 let items = cache.missing_items(11, 10);
485 assert_eq!(items, vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
486
487 let items = cache.missing_items(990, 15);
489 assert_eq!(
490 items,
491 vec![990, 991, 992, 993, 994, 995, 996, 997, 998, 999]
492 );
493
494 cache.sync().await.unwrap();
496 assert_eq!(cache.missing_items(0, 5), vec![0, 3, 4, 7, 8]);
497 assert_eq!(cache.missing_items(3, 3), vec![3, 4, 7]);
498
499 cache.put(DEFAULT_ITEMS_PER_BLOB - 1, 99).await.unwrap();
501 cache.put(DEFAULT_ITEMS_PER_BLOB + 1, 101).await.unwrap();
502
503 let items = cache.missing_items(DEFAULT_ITEMS_PER_BLOB - 2, 5);
505 assert_eq!(
506 items,
507 vec![DEFAULT_ITEMS_PER_BLOB - 2, DEFAULT_ITEMS_PER_BLOB]
508 );
509 });
510 }
511
512 #[test_traced]
513 fn test_cache_intervals_after_restart() {
514 let executor = deterministic::Runner::default();
515 executor.start(|context| async move {
516 let cfg = Config {
517 partition: "test-partition".into(),
518 codec_config: (),
519 compression: None,
520 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
521 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
522 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
523 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
524 };
525
526 {
528 let mut cache = Cache::init(context.child("first"), cfg.clone())
529 .await
530 .expect("Failed to initialize cache");
531
532 cache.put(0, 0).await.expect("Failed to put data");
533 cache.put(100, 100).await.expect("Failed to put data");
534 cache.put(1000, 1000).await.expect("Failed to put data");
535
536 cache.sync().await.expect("Failed to sync cache");
537 }
538
539 {
541 let cache = Cache::<_, i32>::init(context.child("second"), cfg.clone())
542 .await
543 .expect("Failed to initialize cache");
544
545 let (current_end, start_next) = cache.next_gap(0);
547 assert_eq!(current_end, Some(0));
548 assert_eq!(start_next, Some(100));
549
550 let (current_end, start_next) = cache.next_gap(100);
551 assert_eq!(current_end, Some(100));
552 assert_eq!(start_next, Some(1000));
553
554 let items = cache.missing_items(1, 5);
556 assert_eq!(items, vec![1, 2, 3, 4, 5]);
557 }
558 });
559 }
560
561 #[test_traced]
562 fn test_cache_intervals_with_pruning() {
563 let executor = deterministic::Runner::default();
564 executor.start(|context| async move {
565 let cfg = Config {
566 partition: "test-partition".into(),
567 codec_config: (),
568 compression: None,
569 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
570 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
571 items_per_blob: NZU64!(100), page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
573 };
574 let mut cache = Cache::init(context.child("storage"), cfg.clone())
575 .await
576 .expect("Failed to initialize cache");
577
578 cache.put(50, 50).await.unwrap();
580 cache.put(150, 150).await.unwrap();
581 cache.put(250, 250).await.unwrap();
582 cache.put(350, 350).await.unwrap();
583
584 let (current_end, start_next) = cache.next_gap(0);
586 assert!(current_end.is_none());
587 assert_eq!(start_next, Some(50));
588
589 cache.prune(200).await.expect("Failed to prune");
591
592 assert!(!cache.has(50));
594 assert!(!cache.has(150));
595
596 let (current_end, start_next) = cache.next_gap(200);
598 assert!(current_end.is_none());
599 assert_eq!(start_next, Some(250));
600
601 let items = cache.missing_items(200, 5);
603 assert_eq!(items, vec![200, 201, 202, 203, 204]);
604
605 assert!(cache.has(250));
607 assert!(cache.has(350));
608 assert_eq!(cache.get(250).await.unwrap(), Some(250));
609 assert_eq!(cache.get(350).await.unwrap(), Some(350));
610 });
611 }
612
613 #[test_traced]
614 fn test_cache_sparse_indices() {
615 let executor = deterministic::Runner::default();
616 executor.start(|context| async move {
617 let cfg = Config {
618 partition: "test-partition".into(),
619 codec_config: (),
620 compression: None,
621 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
622 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
623 items_per_blob: NZU64!(100), page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
625 };
626 let mut cache = Cache::init(context.child("storage"), cfg.clone())
627 .await
628 .expect("Failed to initialize cache");
629
630 let indices = vec![
632 (0u64, 0),
633 (99u64, 99), (100u64, 100), (500u64, 500), ];
637
638 for (index, value) in &indices {
639 cache.put(*index, *value).await.expect("Failed to put data");
640 }
641
642 assert!(!cache.has(1));
644 assert!(!cache.has(50));
645 assert!(!cache.has(101));
646 assert!(!cache.has(499));
647
648 let (current_end, start_next) = cache.next_gap(50);
650 assert!(current_end.is_none());
651 assert_eq!(start_next, Some(99));
652
653 let (current_end, start_next) = cache.next_gap(99);
654 assert_eq!(current_end, Some(100));
655 assert_eq!(start_next, Some(500));
656
657 cache.sync().await.expect("Failed to sync");
659
660 for (index, value) in &indices {
661 let retrieved = cache
662 .get(*index)
663 .await
664 .expect("Failed to get data")
665 .expect("Data not found");
666 assert_eq!(retrieved, *value);
667 }
668 });
669 }
670
671 #[test_traced]
672 fn test_cache_intervals_edge_cases() {
673 let executor = deterministic::Runner::default();
674 executor.start(|context| async move {
675 let cfg = Config {
676 partition: "test-partition".into(),
677 codec_config: (),
678 compression: None,
679 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
680 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
681 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
682 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
683 };
684 let mut cache = Cache::init(context.child("storage"), cfg.clone())
685 .await
686 .expect("Failed to initialize cache");
687
688 cache.put(42, 42).await.unwrap();
690
691 let (current_end, start_next) = cache.next_gap(42);
692 assert_eq!(current_end, Some(42));
693 assert!(start_next.is_none());
694
695 let (current_end, start_next) = cache.next_gap(41);
696 assert!(current_end.is_none());
697 assert_eq!(start_next, Some(42));
698
699 let (current_end, start_next) = cache.next_gap(43);
700 assert!(current_end.is_none());
701 assert!(start_next.is_none());
702
703 cache.put(43, 43).await.unwrap();
705 cache.put(44, 44).await.unwrap();
706
707 let (current_end, start_next) = cache.next_gap(42);
708 assert_eq!(current_end, Some(44));
709 assert!(start_next.is_none());
710
711 cache.put(u64::MAX - 1, 999).await.unwrap();
713
714 let (current_end, start_next) = cache.next_gap(u64::MAX - 2);
715 assert!(current_end.is_none());
716 assert_eq!(start_next, Some(u64::MAX - 1));
717
718 let (current_end, start_next) = cache.next_gap(u64::MAX - 1);
719 assert_eq!(current_end, Some(u64::MAX - 1));
720 assert!(start_next.is_none());
721 });
722 }
723
724 #[test_traced]
725 fn test_cache_intervals_duplicate_inserts() {
726 let executor = deterministic::Runner::default();
727 executor.start(|context| async move {
728 let cfg = Config {
729 partition: "test-partition".into(),
730 codec_config: (),
731 compression: None,
732 write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
733 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
734 items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
735 page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
736 };
737 let mut cache = Cache::init(context.child("storage"), cfg.clone())
738 .await
739 .expect("Failed to initialize cache");
740
741 cache.put(10, 10).await.unwrap();
743 assert!(cache.has(10));
744 assert_eq!(cache.get(10).await.unwrap(), Some(10));
745
746 cache.put(10, 20).await.unwrap();
748 assert!(cache.has(10));
749 assert_eq!(cache.get(10).await.unwrap(), Some(10)); let (current_end, start_next) = cache.next_gap(10);
753 assert_eq!(current_end, Some(10));
754 assert!(start_next.is_none());
755
756 cache.put(9, 9).await.unwrap();
758 cache.put(11, 11).await.unwrap();
759
760 let (current_end, start_next) = cache.next_gap(9);
762 assert_eq!(current_end, Some(11));
763 assert!(start_next.is_none());
764 });
765 }
766}