1use crate::translator::Translator;
171use commonware_runtime::buffer::paged::CacheRef;
172use std::num::{NonZeroU64, NonZeroUsize};
173
174mod storage;
175pub use storage::Archive;
176
177#[derive(Clone)]
179pub struct Config<T: Translator, C> {
180 pub translator: T,
185
186 pub metadata_partition: String,
189
190 pub key_partition: String,
192
193 pub key_page_cache: CacheRef,
195
196 pub value_partition: String,
198
199 pub compression: Option<u8>,
201
202 pub codec_config: C,
204
205 pub items_per_section: NonZeroU64,
207
208 pub key_write_buffer: NonZeroUsize,
211
212 pub value_write_buffer: NonZeroUsize,
215
216 pub replay_buffer: NonZeroUsize,
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::{
224 archive::{Archive as _, Error, Identifier, MultiArchive as _},
225 journal::{Error as JournalError, segmented::glob::corrupt_frame},
226 translator::{FourCap, TwoCap},
227 };
228 use commonware_codec::{DecodeExt, Error as CodecError, FixedSize};
229 use commonware_cryptography::Crc32;
230 use commonware_macros::{test_group, test_traced};
231 use commonware_runtime::{
232 Blob as _, BufferPooler, Error as RError, Metrics as _, ReadOptions, Runner, Spawner as _,
233 Storage as _, Supervisor as _, WriteOptions, deterministic,
234 mocks::{
235 DelayedSyncContext, PendingSyncs, drive_pending_syncs, fail_pending_syncs,
236 release_next_pending_syncs, release_pending_syncs,
237 },
238 telemetry::metrics::has_metric_value,
239 };
240 use commonware_utils::{NZU16, NZU64, NZUsize, sequence::FixedBytes};
241 use rand::RngExt as _;
242 use std::{
243 collections::BTreeMap,
244 num::{NonZeroU16, NonZeroU64},
245 sync::{
246 Arc,
247 atomic::{AtomicUsize, Ordering},
248 },
249 };
250
251 fn test_key(key: &str) -> FixedBytes<64> {
252 let mut buf = [0u8; 64];
253 let key = key.as_bytes();
254 assert!(key.len() <= buf.len());
255 buf[..key.len()].copy_from_slice(key);
256 FixedBytes::decode(buf.as_ref()).unwrap()
257 }
258
259 const DEFAULT_ITEMS_PER_SECTION: u64 = 65536;
260 const DEFAULT_WRITE_BUFFER: usize = 1024;
261 const DEFAULT_REPLAY_BUFFER: usize = 4096;
262 const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
263 const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
264
265 fn test_config<E: BufferPooler>(
266 context: &E,
267 partition_prefix: &str,
268 items_per_section: NonZeroU64,
269 ) -> Config<FourCap, ()> {
270 Config {
271 translator: FourCap,
272 metadata_partition: format!("{partition_prefix}-metadata"),
273 key_partition: format!("{partition_prefix}-index"),
274 key_page_cache: CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE),
275 value_partition: format!("{partition_prefix}-value"),
276 codec_config: (),
277 compression: None,
278 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
279 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
280 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
281 items_per_section,
282 }
283 }
284
285 const I32_VALUE_FRAME_SIZE: u64 =
288 (i32::SIZE + crate::journal::segmented::glob::CHECKSUM_SIZE) as u64;
289
290 #[test_traced]
291 fn test_put_after_start_sync_is_accepted_before_handle_completes() {
292 let executor = deterministic::Runner::default();
293 let (_, checkpoint) = executor.start_and_recover(|context| async move {
294 let pending = PendingSyncs::default();
295 let context = DelayedSyncContext {
296 inner: context,
297 pending: pending.clone(),
298 };
299 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
300 let archive = Archive::init(context.child("storage"), cfg)
301 .await
302 .expect("Failed to initialize archive");
303
304 let (mut archive, handle) = archive
305 .put_start_sync(1, test_key("aaa"), 10)
306 .await
307 .expect("Failed to start sync");
308 let pending_after_start = pending.lock().len();
309 assert!(
310 pending_after_start > 0,
311 "put_start_sync should return while the sync handle is still pending"
312 );
313
314 archive = archive
315 .put(2, test_key("bbb"), 20)
316 .await
317 .expect("archive should remain usable before sync completion");
318 assert_eq!(
319 pending.lock().len(),
320 pending_after_start,
321 "put should not issue a new storage sync while accepting later data"
322 );
323 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
324
325 release_pending_syncs(&pending);
326 handle.await.expect("sync handle should complete");
327
328 let (_archive, follow_up) = archive
329 .start_sync()
330 .await
331 .expect("Failed to start next sync");
332 assert!(
333 !pending.lock().is_empty(),
334 "the later put must remain pending for a future sync"
335 );
336 release_pending_syncs(&pending);
337 follow_up.await.expect("follow-up sync should complete");
338 });
339
340 deterministic::Runner::from(checkpoint).start(|context| async move {
341 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
342 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
343 .await
344 .expect("Failed to reopen archive");
345
346 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
347 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
348 });
349 }
350
351 #[test_traced]
352 fn test_duplicate_put_start_sync_observes_in_flight_sync() {
353 let executor = deterministic::Runner::default();
354 executor.start(|context| async move {
355 let pending = PendingSyncs::default();
356 let context = DelayedSyncContext {
357 inner: context,
358 pending: pending.clone(),
359 };
360 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
361 let archive = Archive::init(context.child("storage"), cfg)
362 .await
363 .expect("Failed to initialize archive");
364
365 let (archive, first) = archive
366 .put_start_sync(1, test_key("aaa"), 10)
367 .await
368 .expect("Failed to start sync");
369 assert_eq!(pending.lock().len(), 2);
370
371 let (archive, second) = archive
372 .put_start_sync(1, test_key("duplicate"), 99)
373 .await
374 .expect("Failed to start duplicate sync");
375 assert_eq!(
376 pending.lock().len(),
377 2,
378 "duplicate put_start_sync must not issue a new storage sync"
379 );
380
381 let started = Arc::new(AtomicUsize::new(0));
382 let completed = Arc::new(AtomicUsize::new(0));
383 let started_clone = started.clone();
384 let completed_clone = completed.clone();
385 let waiter = context.inner.child("duplicate").spawn(|_| async move {
386 started_clone.fetch_add(1, Ordering::Relaxed);
387 second.await.expect("duplicate sync handle should complete");
388 completed_clone.fetch_add(1, Ordering::Relaxed);
389 });
390
391 while started.load(Ordering::Relaxed) == 0 {
392 commonware_runtime::reschedule().await;
393 }
394 commonware_runtime::reschedule().await;
395 assert_eq!(
396 completed.load(Ordering::Relaxed),
397 0,
398 "duplicate put_start_sync must observe the original in-flight sync"
399 );
400
401 release_pending_syncs(&pending);
402 first.await.expect("first sync handle should complete");
403 while completed.load(Ordering::Relaxed) == 0 {
404 commonware_runtime::reschedule().await;
405 }
406 waiter.await.expect("duplicate waiter failed");
407
408 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
409 });
410 }
411
412 #[test_traced]
413 fn test_below_floor_put_start_sync_covers_prior_pending_write() {
414 let executor = deterministic::Runner::default();
415 executor.start(|context| async move {
416 let pending = PendingSyncs::default();
417 let context = DelayedSyncContext {
418 inner: context,
419 pending: pending.clone(),
420 };
421 let cfg = test_config(&context, "test", NZU64!(1));
422 let archive = Archive::init(context.child("storage"), cfg)
423 .await
424 .expect("Failed to initialize archive");
425
426 let archive = archive.prune(1).await.expect("Failed to set prune floor");
429 let archive = archive
430 .put(2, test_key("pending"), 20)
431 .await
432 .expect("Failed to buffer retained write");
433
434 assert!(pending.lock().is_empty());
438 let (archive, handle) = archive
439 .put_start_sync(0, test_key("pruned"), 0)
440 .await
441 .expect("Failed to request sync through below-floor put");
442 assert_eq!(
443 pending.lock().len(),
444 2,
445 "the sync combinator must cover writes accepted before its below-floor put"
446 );
447
448 release_pending_syncs(&pending);
451 handle.await.expect("covering sync should complete");
452 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
453 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), None);
454 });
455 }
456
457 #[test_traced]
458 fn test_below_floor_put_multi_sync_covers_prior_pending_write() {
459 let executor = deterministic::Runner::default();
460 executor.start(|context| async move {
461 let pending = PendingSyncs::default();
462 let context = DelayedSyncContext {
463 inner: context,
464 pending: pending.clone(),
465 };
466 let cfg = test_config(&context, "test", NZU64!(1));
467 let archive = Archive::init(context.child("storage"), cfg)
468 .await
469 .expect("Failed to initialize archive");
470
471 let archive = archive.prune(1).await.expect("Failed to set prune floor");
474 let archive = archive
475 .put_multi(2, test_key("pending"), 20)
476 .await
477 .expect("Failed to buffer retained write");
478
479 pending.arm();
482 let completed = Arc::new(AtomicUsize::new(0));
483 let completed_clone = completed.clone();
484 let task = context.inner.child("put_multi_sync").spawn(|_| async move {
485 let result = archive.put_multi_sync(0, test_key("pruned"), 0).await;
486 completed_clone.store(1, Ordering::Relaxed);
487 result
488 });
489 while pending.calls() == 0 && completed.load(Ordering::Relaxed) == 0 {
490 commonware_runtime::reschedule().await;
491 }
492
493 assert_eq!(
496 completed.load(Ordering::Relaxed),
497 0,
498 "put_multi_sync must wait for writes accepted before its below-floor put"
499 );
500 assert!(pending.calls() > 0);
501 release_pending_syncs(&pending);
502 let archive = task
503 .await
504 .expect("put_multi_sync task failed")
505 .expect("put_multi_sync failed");
506
507 assert_eq!(archive.get_all(2).await.unwrap(), Some(vec![20]));
509 assert_eq!(archive.get_all(0).await.unwrap(), None);
510 });
511 }
512
513 #[test_traced]
514 fn test_overlapping_put_start_sync_waits_for_in_flight_sync() {
515 let executor = deterministic::Runner::default();
516 executor.start(|context| async move {
517 let pending = PendingSyncs::default();
518 let context = DelayedSyncContext {
519 inner: context,
520 pending: pending.clone(),
521 };
522 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
523 let archive = Archive::init(context.child("storage"), cfg)
524 .await
525 .expect("Failed to initialize archive");
526
527 let (archive, first) = archive
528 .put_start_sync(1, test_key("aaa"), 10)
529 .await
530 .expect("Failed to start sync");
531 let pending_after_first = pending.lock().len();
532 assert!(pending_after_first > 0);
533
534 let started = Arc::new(AtomicUsize::new(0));
535 let completed = Arc::new(AtomicUsize::new(0));
536 let started_clone = started.clone();
537 let completed_clone = completed.clone();
538 let waiter = context.inner.child("second").spawn(|_| async move {
539 started_clone.fetch_add(1, Ordering::Relaxed);
540 let (archive, second) = archive
541 .put_start_sync(2, test_key("bbb"), 20)
542 .await
543 .expect("Failed to start second sync");
544 completed_clone.fetch_add(1, Ordering::Relaxed);
545 (archive, second)
546 });
547
548 while started.load(Ordering::Relaxed) == 0 {
549 commonware_runtime::reschedule().await;
550 }
551 commonware_runtime::reschedule().await;
552 assert_eq!(completed.load(Ordering::Relaxed), 0);
553 assert_eq!(
554 pending.lock().len(),
555 pending_after_first,
556 "second put_start_sync must not start new syncs before the first completes"
557 );
558
559 release_pending_syncs(&pending);
560 first.await.expect("first sync handle should complete");
561 while completed.load(Ordering::Relaxed) == 0 {
562 commonware_runtime::reschedule().await;
563 }
564 let (archive, second) = waiter.await.expect("second put task failed");
565 assert!(!pending.lock().is_empty());
566 release_pending_syncs(&pending);
567 second.await.expect("second sync handle should complete");
568
569 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
570 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
571 });
572 }
573
574 #[test_traced]
575 fn test_sync_after_put_start_sync_waits_for_in_flight_sync() {
576 let executor = deterministic::Runner::default();
577 executor.start(|context| async move {
578 let pending = PendingSyncs::default();
579 let context = DelayedSyncContext {
580 inner: context,
581 pending: pending.clone(),
582 };
583 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
584 let archive = Archive::init(context.child("storage"), cfg)
585 .await
586 .expect("Failed to initialize archive");
587
588 let (archive, first) = archive
589 .put_start_sync(1, test_key("aaa"), 10)
590 .await
591 .expect("Failed to start sync");
592 assert!(!pending.lock().is_empty());
593
594 let started = Arc::new(AtomicUsize::new(0));
595 let completed = Arc::new(AtomicUsize::new(0));
596 let started_clone = started.clone();
597 let completed_clone = completed.clone();
598 let waiter = context.inner.child("sync").spawn(|_| async move {
599 started_clone.fetch_add(1, Ordering::Relaxed);
600 let archive = archive.sync().await.expect("sync should complete");
601 completed_clone.fetch_add(1, Ordering::Relaxed);
602 archive
603 });
604
605 while started.load(Ordering::Relaxed) == 0 {
606 commonware_runtime::reschedule().await;
607 }
608 commonware_runtime::reschedule().await;
609 assert_eq!(
610 completed.load(Ordering::Relaxed),
611 0,
612 "shutdown sync must wait for the in-flight put_start_sync handle"
613 );
614
615 release_pending_syncs(&pending);
616 first.await.expect("first sync handle should complete");
617 while completed.load(Ordering::Relaxed) == 0 {
618 commonware_runtime::reschedule().await;
619 }
620 let archive = waiter.await.expect("sync task failed");
621 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
622 });
623 }
624
625 #[test_traced]
626 fn test_destroy_after_put_start_sync_waits_for_in_flight_sync() {
627 let executor = deterministic::Runner::default();
628 executor.start(|context| async move {
629 let pending = PendingSyncs::default();
630 let context = DelayedSyncContext {
631 inner: context,
632 pending: pending.clone(),
633 };
634 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
635 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
636 .await
637 .expect("Failed to initialize archive");
638
639 let (archive, first) = archive
640 .put_start_sync(1, test_key("aaa"), 10)
641 .await
642 .expect("Failed to start sync");
643 assert!(!pending.lock().is_empty());
644
645 let started = Arc::new(AtomicUsize::new(0));
646 let completed = Arc::new(AtomicUsize::new(0));
647 let started_clone = started.clone();
648 let completed_clone = completed.clone();
649 let waiter = context.inner.child("destroy").spawn(|_| async move {
650 started_clone.fetch_add(1, Ordering::Relaxed);
651 archive.destroy().await.expect("destroy should complete");
652 completed_clone.fetch_add(1, Ordering::Relaxed);
653 });
654
655 while started.load(Ordering::Relaxed) == 0 {
656 commonware_runtime::reschedule().await;
657 }
658 commonware_runtime::reschedule().await;
659 assert_eq!(
660 completed.load(Ordering::Relaxed),
661 0,
662 "destroy must wait for the in-flight put_start_sync handle"
663 );
664
665 release_pending_syncs(&pending);
666 first.await.expect("first sync handle should complete");
667 while completed.load(Ordering::Relaxed) == 0 {
668 commonware_runtime::reschedule().await;
669 }
670 waiter.await.expect("destroy task failed");
671 });
672 }
673
674 #[test_traced]
675 fn test_prune_after_put_start_sync_waits_for_in_flight_sync() {
676 let executor = deterministic::Runner::default();
677 executor.start(|context| async move {
678 let pending = PendingSyncs::default();
679 let context = DelayedSyncContext {
680 inner: context,
681 pending: pending.clone(),
682 };
683 let cfg = test_config(&context, "test", NZU64!(1));
684 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
685 .await
686 .expect("Failed to initialize archive");
687
688 let (archive, first) = archive
689 .put_start_sync(1, test_key("aaa"), 10)
690 .await
691 .expect("Failed to start sync");
692 assert!(!pending.lock().is_empty());
693
694 let started = Arc::new(AtomicUsize::new(0));
695 let completed = Arc::new(AtomicUsize::new(0));
696 let started_clone = started.clone();
697 let completed_clone = completed.clone();
698 let waiter = context.inner.child("prune").spawn(|_| async move {
699 started_clone.fetch_add(1, Ordering::Relaxed);
700 let archive = archive.prune(2).await.expect("prune should complete");
701 completed_clone.fetch_add(1, Ordering::Relaxed);
702 archive
703 });
704
705 while started.load(Ordering::Relaxed) == 0 {
706 commonware_runtime::reschedule().await;
707 }
708 commonware_runtime::reschedule().await;
709 assert_eq!(
710 completed.load(Ordering::Relaxed),
711 0,
712 "prune must wait for in-flight syncs on pruned sections"
713 );
714
715 release_pending_syncs(&pending);
716 first
717 .await
718 .expect("sync handle should complete despite pruning");
719 while completed.load(Ordering::Relaxed) == 0 {
720 commonware_runtime::reschedule().await;
721 }
722 let archive = waiter.await.expect("prune task failed");
723 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
724 });
725 }
726
727 #[test_traced]
728 fn test_prune_surfaces_failed_in_flight_sync() {
729 let executor = deterministic::Runner::default();
730 executor.start(|context| async move {
731 let pending = PendingSyncs::default();
732 let context = DelayedSyncContext {
733 inner: context,
734 pending: pending.clone(),
735 };
736 let cfg = test_config(&context, "test", NZU64!(1));
737 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
738 .await
739 .expect("Failed to initialize archive");
740
741 let (archive, first) = archive
742 .put_start_sync(1, test_key("aaa"), 10)
743 .await
744 .expect("Failed to start sync");
745 fail_pending_syncs(&pending);
746
747 let err = archive
748 .prune(2)
749 .await
750 .expect_err("prune must surface a failed in-flight sync");
751 assert!(matches!(
752 err,
753 Error::Journal(JournalError::Runtime(RError::Io(_)))
754 ));
755
756 let err = first.await.expect_err("first sync handle should fail");
757 assert!(matches!(err, RError::Io(_)));
758 });
759 }
760
761 #[test_traced]
762 fn test_put_start_sync_after_prune_drops_pruned_sync_requests() {
763 let executor = deterministic::Runner::default();
764 executor.start(|context| async move {
765 let pending = PendingSyncs::default();
766 let context = DelayedSyncContext {
767 inner: context,
768 pending: pending.clone(),
769 };
770 let cfg = test_config(&context, "test", NZU64!(1));
771 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
772 .await
773 .expect("Failed to initialize archive");
774
775 let (archive, first) = archive
776 .put_start_sync(1, test_key("aaa"), 10)
777 .await
778 .expect("Failed to start sync");
779 release_pending_syncs(&pending);
780 first.await.expect("first sync handle should complete");
781
782 let archive = archive.prune(2).await.expect("Failed to prune");
783
784 let (archive, second) = archive
787 .put_start_sync(2, test_key("bbb"), 20)
788 .await
789 .expect("put_start_sync after prune should succeed");
790 release_pending_syncs(&pending);
791 second.await.expect("second sync handle should complete");
792 let archive = drive_pending_syncs(&pending, archive.sync())
793 .await
794 .expect("sync after prune should succeed");
795
796 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
797 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
798 });
799 }
800
801 #[test_traced]
802 fn test_overlapping_put_start_sync_restarts_after_all_handles_complete() {
803 let executor = deterministic::Runner::default();
804 let (_, checkpoint) = executor.start_and_recover(|context| async move {
805 let pending = PendingSyncs::default();
806 let context = DelayedSyncContext {
807 inner: context,
808 pending: pending.clone(),
809 };
810 let cfg = test_config(&context, "test", NZU64!(1));
811 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
812 .await
813 .expect("Failed to initialize archive");
814
815 let (archive, first) = archive
816 .put_start_sync(1, test_key("aaa"), 10)
817 .await
818 .expect("Failed to start first sync");
819 assert_eq!(pending.lock().len(), 2);
820
821 let (_archive, second) = archive
822 .put_start_sync(2, test_key("bbb"), 20)
823 .await
824 .expect("Failed to start second sync");
825 assert_eq!(
826 pending.lock().len(),
827 4,
828 "different sections should be able to have independent in-flight syncs"
829 );
830
831 release_pending_syncs(&pending);
832 first.await.expect("first sync handle should complete");
833 second.await.expect("second sync handle should complete");
834 });
835
836 deterministic::Runner::from(checkpoint).start(|context| async move {
837 let cfg = test_config(&context, "test", NZU64!(1));
838 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
839 .await
840 .expect("Failed to reopen archive");
841
842 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
843 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
844 });
845 }
846
847 #[test_traced]
848 fn test_overlapping_put_start_sync_restarts_only_completed_handles() {
849 let executor = deterministic::Runner::default();
850 let (_, checkpoint) = executor.start_and_recover(|context| async move {
851 let pending = PendingSyncs::default();
852 let context = DelayedSyncContext {
853 inner: context,
854 pending: pending.clone(),
855 };
856 let cfg = test_config(&context, "test", NZU64!(1));
857 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
858 .await
859 .expect("Failed to initialize archive");
860
861 let (archive, first) = archive
862 .put_start_sync(1, test_key("aaa"), 10)
863 .await
864 .expect("Failed to start first sync");
865 let (archive, second) = archive
866 .put_start_sync(2, test_key("bbb"), 20)
867 .await
868 .expect("Failed to start second sync");
869 assert_eq!(pending.lock().len(), 4);
870
871 release_next_pending_syncs(&pending, 2);
872 first.await.expect("first sync handle should complete");
873
874 drop(second);
875 drop(archive);
876 });
877
878 deterministic::Runner::from(checkpoint).start(|context| async move {
879 let cfg = test_config(&context, "test", NZU64!(1));
880 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
881 .await
882 .expect("Failed to reopen archive");
883
884 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
885 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), None);
886 });
887 }
888
889 #[test_traced]
890 fn test_failed_start_sync_is_returned_by_next_start_sync_handle() {
891 let executor = deterministic::Runner::default();
892 executor.start(|context| async move {
893 let pending = PendingSyncs::default();
894 let context = DelayedSyncContext {
895 inner: context,
896 pending: pending.clone(),
897 };
898 let cfg = test_config(&context, "test", NZU64!(DEFAULT_ITEMS_PER_SECTION));
899 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
900 .await
901 .expect("Failed to initialize archive");
902
903 let (archive, first) = archive
904 .put_start_sync(1, test_key("aaa"), 10)
905 .await
906 .expect("Failed to start sync");
907 assert_eq!(pending.lock().len(), 2);
908 fail_pending_syncs(&pending);
909
910 let archive = archive
911 .put(2, test_key("bbb"), 20)
912 .await
913 .expect("write should be accepted before observing the failed sync");
914
915 let (_archive, second) = archive
916 .start_sync()
917 .await
918 .expect("start_sync should return a handle for the failed sync");
919 let err = second
920 .await
921 .expect_err("next start_sync handle should observe failed in-flight sync");
922 assert!(matches!(err, RError::Io(_)));
923
924 let err = first.await.expect_err("first sync handle should fail");
925 assert!(matches!(err, RError::Io(_)));
926 });
927 }
928
929 #[test_traced]
930 fn test_archive_truncates_at_first_invalid_value() {
931 deterministic::Runner::default().start(|context| async move {
932 for (name, bad_position, retained) in [("first", 0, 0), ("middle", 1, 1)] {
933 let cfg = test_config(&context, &format!("invalid-{name}"), NZU64!(4));
934
935 let mut archive = Archive::init(context.child(name), cfg.clone())
939 .await
940 .unwrap();
941 for (index, value) in [10, 20, 30].into_iter().enumerate() {
942 archive = archive
943 .put(index as u64, test_key(&format!("key-{index}")), value)
944 .await
945 .unwrap();
946 }
947 archive = archive.sync().await.unwrap();
948 drop(archive);
949
950 corrupt_frame(
951 &context,
952 &cfg.value_partition,
953 &0u64.to_be_bytes(),
954 bad_position,
955 I32_VALUE_FRAME_SIZE,
956 )
957 .await;
958
959 let archive =
965 Archive::<_, _, FixedBytes<64>, i32>::init(context.child(name), cfg.clone())
966 .await
967 .unwrap();
968 assert_eq!(
969 archive.ranges().collect::<Vec<_>>(),
970 if retained == 0 {
971 Vec::new()
972 } else {
973 vec![(0, retained - 1)]
974 }
975 );
976 for (index, value) in [10, 20, 30].into_iter().enumerate() {
977 let expected = (index < retained as usize).then_some(value);
978 assert_eq!(
979 archive.get(Identifier::Index(index as u64)).await.unwrap(),
980 expected
981 );
982 }
983 drop(archive);
984
985 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child(name), cfg)
988 .await
989 .unwrap();
990 assert_eq!(archive.last_index(), retained.checked_sub(1));
991 archive.destroy().await.unwrap();
992 }
993 });
994 }
995
996 #[test_traced]
997 fn test_archive_completes_interrupted_rewind_to_empty_section() {
998 deterministic::Runner::default().start(|context| async move {
999 let cfg = test_config(&context, "empty-rewind", NZU64!(4));
1000
1001 let archive = Archive::init(context.child("seed"), cfg.clone())
1004 .await
1005 .unwrap();
1006 let archive = archive.put(0, test_key("zero"), 10).await.unwrap();
1007 let archive = archive.sync().await.unwrap();
1008 drop(archive);
1009 context.remove(&cfg.metadata_partition, None).await.unwrap();
1010
1011 let (index, _) = context
1018 .open(&cfg.key_partition, &0u64.to_be_bytes())
1019 .await
1020 .unwrap();
1021 index.resize(0).await.unwrap();
1022 index.sync().await.unwrap();
1023 drop(index);
1024 let (_, value_size) = context
1025 .open(&cfg.value_partition, &0u64.to_be_bytes())
1026 .await
1027 .unwrap();
1028 assert_eq!(value_size, I32_VALUE_FRAME_SIZE);
1029
1030 let archive =
1031 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("repair"), cfg.clone())
1032 .await
1033 .unwrap();
1034 assert_eq!(archive.last_index(), None);
1035 drop(archive);
1036 let (_, value_size) = context
1037 .open(&cfg.value_partition, &0u64.to_be_bytes())
1038 .await
1039 .unwrap();
1040 assert_eq!(value_size, 0, "startup must finish the value truncation");
1041 context.remove(&cfg.metadata_partition, None).await.unwrap();
1042
1043 let pending = PendingSyncs::default();
1051 let delayed = DelayedSyncContext {
1052 inner: context.child("clean_restart"),
1053 pending: pending.clone(),
1054 };
1055 pending.arm();
1056 let completed = Arc::new(AtomicUsize::new(0));
1057 let completed_clone = completed.clone();
1058 let cfg_clone = cfg.clone();
1059 let task = context.child("clean_restart_task").spawn(|_| async move {
1060 let result = Archive::init(delayed.child("archive"), cfg_clone).await;
1061 completed_clone.store(1, Ordering::Relaxed);
1062 result
1063 });
1064 while pending.calls() == 0 && completed.load(Ordering::Relaxed) == 0 {
1065 commonware_runtime::reschedule().await;
1066 }
1067 if pending.calls() != 0 {
1068 pending.unblock();
1069 let _ = task.await;
1070 panic!("clean empty-section restart must not issue durability operations");
1071 }
1072 pending.unblock();
1073 let archive = task.await.unwrap().unwrap();
1074
1075 let archive = archive.put(0, test_key("new"), 20).await.unwrap();
1078 let archive = archive.sync().await.unwrap();
1079 drop(archive);
1080 let (_, value_size) = context
1081 .open(&cfg.value_partition, &0u64.to_be_bytes())
1082 .await
1083 .unwrap();
1084 assert_eq!(value_size, I32_VALUE_FRAME_SIZE);
1085
1086 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
1087 .await
1088 .unwrap();
1089 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(20));
1090 archive.destroy().await.unwrap();
1091 });
1092 }
1093
1094 #[test_traced]
1095 fn test_validation_marker_skips_previously_validated_values() {
1096 let executor = deterministic::Runner::default();
1097 executor.start(|context| async move {
1098 let cfg = test_config(&context, "marker-skip", NZU64!(4));
1099
1100 let mut archive = Archive::init(context.child("seed"), cfg.clone())
1102 .await
1103 .unwrap();
1104 archive = archive.put(0, test_key("zero"), 10).await.unwrap();
1105 archive = archive.put(1, test_key("one"), 20).await.unwrap();
1106 archive = archive.sync().await.unwrap();
1107 drop(archive);
1108
1109 context.remove(&cfg.metadata_partition, None).await.unwrap();
1115 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1116 context.child("first_open"),
1117 cfg.clone(),
1118 )
1119 .await
1120 .unwrap();
1121 assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 1)]);
1122 drop(archive);
1123
1124 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1125 context.child("second_open"),
1126 cfg.clone(),
1127 )
1128 .await
1129 .unwrap();
1130 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1131 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1132
1133 let archive = archive.put(2, test_key("two"), 30).await.unwrap();
1136 let archive = archive.sync().await.unwrap();
1137 drop(archive);
1138
1139 let archive =
1140 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("third_open"), cfg)
1141 .await
1142 .unwrap();
1143 assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 2)]);
1144 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1145 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1146 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(30));
1147 });
1148 }
1149
1150 #[test_traced]
1151 fn test_validation_marker_skips_covered_interior_values() {
1152 deterministic::Runner::default().start(|context| async move {
1153 let cfg = test_config(&context, "marker-covered-interior", NZU64!(4));
1154
1155 let archive = Archive::init(context.child("seed"), cfg.clone())
1158 .await
1159 .unwrap();
1160 let archive = archive.put(0, test_key("zero"), 10).await.unwrap();
1161 let archive = archive.put(1, test_key("one"), 20).await.unwrap();
1162
1163 let archive = archive.sync().await.unwrap();
1165 let archive = archive.sync().await.unwrap();
1166 drop(archive);
1167
1168 corrupt_frame(
1171 &context,
1172 &cfg.value_partition,
1173 &0u64.to_be_bytes(),
1174 0,
1175 I32_VALUE_FRAME_SIZE,
1176 )
1177 .await;
1178 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
1179 .await
1180 .unwrap();
1181 assert!(archive.get(Identifier::Index(0)).await.is_err());
1182 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1183 });
1184 }
1185
1186 #[test_traced]
1187 fn test_validation_marker_damage_never_mutates() {
1188 #[derive(Clone, Copy)]
1189 enum Damage {
1190 MissingIndex,
1191 MissingValues,
1192 TruncatedIndex,
1193 TruncatedValues,
1194 CorruptIndex,
1195 CorruptValues,
1196 }
1197
1198 deterministic::Runner::default().start(|context| async move {
1199 for (name, damage) in [
1203 ("missing_index", Damage::MissingIndex),
1204 ("missing_values", Damage::MissingValues),
1205 ("truncated_index", Damage::TruncatedIndex),
1206 ("truncated_values", Damage::TruncatedValues),
1207 ("corrupt_index", Damage::CorruptIndex),
1208 ("corrupt_values", Damage::CorruptValues),
1209 ] {
1210 let case = context.child(name);
1211 let cfg = test_config(&case, name, NZU64!(4));
1212
1213 let archive = Archive::init(case.child("seed"), cfg.clone())
1216 .await
1217 .unwrap();
1218 let archive = archive.put_sync(0, test_key("zero"), 10).await.unwrap();
1219 let archive = archive.sync().await.unwrap();
1220 drop(archive);
1221
1222 let (_, index_size) = context
1231 .open(&cfg.key_partition, &0u64.to_be_bytes())
1232 .await
1233 .unwrap();
1234 let (_, value_size) = context
1235 .open(&cfg.value_partition, &0u64.to_be_bytes())
1236 .await
1237 .unwrap();
1238 let damage_index = matches!(
1239 damage,
1240 Damage::MissingIndex | Damage::TruncatedIndex | Damage::CorruptIndex
1241 );
1242 let damaged_partition = if damage_index {
1243 &cfg.key_partition
1244 } else {
1245 &cfg.value_partition
1246 };
1247 let damaged_size = match damage {
1248 Damage::MissingIndex | Damage::MissingValues => {
1249 context
1250 .remove(damaged_partition, Some(&0u64.to_be_bytes()))
1251 .await
1252 .unwrap();
1253 None
1254 }
1255 Damage::TruncatedIndex | Damage::TruncatedValues => {
1256 let (blob, size) = context
1257 .open(damaged_partition, &0u64.to_be_bytes())
1258 .await
1259 .unwrap();
1260 let size = if damage_index { size - 1 } else { 0 };
1261 blob.resize(size).await.unwrap();
1262 blob.sync().await.unwrap();
1263 Some(size)
1264 }
1265 Damage::CorruptIndex | Damage::CorruptValues => {
1266 let (blob, size) = context
1267 .open(damaged_partition, &0u64.to_be_bytes())
1268 .await
1269 .unwrap();
1270 let byte = blob
1271 .read_at(0, 1, ReadOptions::default())
1272 .await
1273 .unwrap()
1274 .coalesce();
1275 let byte = byte.as_ref()[0];
1276 blob.write_at(0, vec![byte ^ 0xFF], WriteOptions::SYNC)
1277 .await
1278 .unwrap();
1279 Some(size)
1280 }
1281 };
1282
1283 if matches!(damage, Damage::CorruptValues) {
1286 for child in ["first", "second"] {
1287 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1288 case.child(child),
1289 cfg.clone(),
1290 )
1291 .await
1292 .expect("marked value damage must not fail startup");
1293 assert!(archive.get(Identifier::Index(0)).await.is_err());
1294 drop(archive);
1295 let (_, size) = context
1296 .open(&cfg.value_partition, &0u64.to_be_bytes())
1297 .await
1298 .unwrap();
1299 assert_eq!(size, value_size, "adoption must preserve the damaged frame");
1300 }
1301 continue;
1302 }
1303
1304 for child in ["first", "second"] {
1307 let result =
1308 Archive::<_, _, FixedBytes<64>, i32>::init(case.child(child), cfg.clone())
1309 .await;
1310 assert!(
1311 matches!(result, Err(Error::Journal(JournalError::Corruption(_)))),
1312 "damaged marked section must remain visible as corruption"
1313 );
1314
1315 let (surviving_partition, surviving_size) = if damage_index {
1316 (&cfg.value_partition, value_size)
1317 } else {
1318 (&cfg.key_partition, index_size)
1319 };
1320 let (_, size) = context
1321 .open(surviving_partition, &0u64.to_be_bytes())
1322 .await
1323 .unwrap();
1324 assert_eq!(
1325 size, surviving_size,
1326 "failed startup must preserve the surviving journal section"
1327 );
1328 if let Some(damaged_size) = damaged_size {
1329 let (_, size) = context
1330 .open(damaged_partition, &0u64.to_be_bytes())
1331 .await
1332 .unwrap();
1333 assert_eq!(
1334 size, damaged_size,
1335 "failed startup must not normalize the damaged journal section"
1336 );
1337 }
1338 }
1339 }
1340 });
1341 }
1342
1343 #[test_traced]
1344 fn test_validation_floor_rejection_precedes_index_suffix_repair() {
1345 deterministic::Runner::default().start(|context| async move {
1346 let cfg = test_config(&context, "floor-order", NZU64!(4));
1347
1348 let archive =
1351 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("seed"), cfg.clone())
1352 .await
1353 .unwrap();
1354 let archive = archive.put_sync(0, test_key("zero"), 10).await.unwrap();
1355 let archive = archive.sync().await.unwrap();
1356 drop(archive);
1357
1358 let (index, index_size) = context
1361 .open(&cfg.key_partition, &0u64.to_be_bytes())
1362 .await
1363 .unwrap();
1364 index
1365 .write_at(index_size, vec![0xA5; 7], WriteOptions::SYNC)
1366 .await
1367 .unwrap();
1368 let expected_size = index_size + 7;
1369
1370 let byte = index
1373 .read_at(0, 1, ReadOptions::default())
1374 .await
1375 .unwrap()
1376 .coalesce();
1377 index
1378 .write_at(0, vec![byte.as_ref()[0] ^ 0xFF], WriteOptions::SYNC)
1379 .await
1380 .unwrap();
1381 let expected = index
1382 .read_at(0, expected_size as usize, ReadOptions::default())
1383 .await
1384 .unwrap()
1385 .coalesce();
1386 drop(index);
1387
1388 for child in ["first", "second"] {
1391 let result =
1392 Archive::<_, _, FixedBytes<64>, i32>::init(context.child(child), cfg.clone())
1393 .await;
1394 assert!(matches!(
1395 result,
1396 Err(Error::Journal(JournalError::Corruption(_)))
1397 ));
1398
1399 let (index, actual_size) = context
1400 .open(&cfg.key_partition, &0u64.to_be_bytes())
1401 .await
1402 .unwrap();
1403 assert_eq!(actual_size, expected_size);
1404 let actual = index
1405 .read_at(0, actual_size as usize, ReadOptions::default())
1406 .await
1407 .unwrap()
1408 .coalesce();
1409 assert_eq!(actual.as_ref(), expected.as_ref());
1410 }
1411 });
1412 }
1413
1414 #[test_traced]
1415 fn test_validation_marker_survives_torn_index_tail_rewrite() {
1416 let executor = deterministic::Runner::default();
1417 let (_, checkpoint) = executor.start_and_recover(|context| async move {
1418 let cfg = test_config(&context, "marker-torn-tail", NZU64!(4));
1419 let archive = Archive::init(context.child("seed"), cfg.clone())
1420 .await
1421 .unwrap();
1422
1423 let archive = archive.put_sync(0, test_key("zero"), 10).await.unwrap();
1425 let archive = archive.sync().await.unwrap();
1426
1427 let page_size = usize::from(PAGE_SIZE.get());
1435 let physical_page_size = page_size + 12;
1436 let record_size = u64::SIZE + FixedBytes::<64>::SIZE + u64::SIZE + u32::SIZE;
1437 assert!(record_size < page_size);
1438 let (index, size) = context
1439 .open(&cfg.key_partition, &0u64.to_be_bytes())
1440 .await
1441 .unwrap();
1442 assert_eq!(size, physical_page_size as u64);
1443 let old_page = index
1444 .read_at(0, physical_page_size, ReadOptions::default())
1445 .await
1446 .unwrap()
1447 .coalesce();
1448 let old_page = old_page.as_ref().to_vec();
1449 drop(index);
1450 let old_len =
1451 u16::from_be_bytes(old_page[page_size..page_size + 2].try_into().unwrap()) as usize;
1452 let old_crc =
1453 u32::from_be_bytes(old_page[page_size + 2..page_size + 6].try_into().unwrap());
1454 assert_eq!(old_len, record_size);
1455 assert_eq!(old_crc, Crc32::checksum(&old_page[..old_len]));
1456
1457 let archive = archive.put_sync(1, test_key("one"), 20).await.unwrap();
1461 drop(archive);
1462 let (index, size) = context
1463 .open(&cfg.key_partition, &0u64.to_be_bytes())
1464 .await
1465 .unwrap();
1466 assert_eq!(size, physical_page_size as u64);
1467 let new_page = index
1468 .read_at(0, physical_page_size, ReadOptions::default())
1469 .await
1470 .unwrap()
1471 .coalesce();
1472 let new_page = new_page.as_ref().to_vec();
1473 assert_eq!(
1474 &new_page[page_size..page_size + 6],
1475 &old_page[page_size..page_size + 6],
1476 );
1477 let new_len =
1478 u16::from_be_bytes(new_page[page_size + 6..page_size + 8].try_into().unwrap())
1479 as usize;
1480 let new_crc =
1481 u32::from_be_bytes(new_page[page_size + 8..page_size + 12].try_into().unwrap());
1482 assert_eq!(new_len, 2 * record_size);
1483 assert_eq!(new_crc, Crc32::checksum(&new_page[..new_len]));
1484
1485 index
1492 .write_at(0, old_page.clone(), WriteOptions::SYNC)
1493 .await
1494 .unwrap();
1495 let torn_prefix = page_size + 6 + 2;
1496 index
1497 .write_at(0, new_page[..torn_prefix].to_vec(), WriteOptions::SYNC)
1498 .await
1499 .unwrap();
1500 let torn_page = index
1501 .read_at(0, physical_page_size, ReadOptions::default())
1502 .await
1503 .unwrap()
1504 .coalesce();
1505 let torn_page = torn_page.as_ref();
1506 assert_eq!(
1507 &torn_page[page_size..page_size + 6],
1508 &old_page[page_size..page_size + 6],
1509 );
1510 assert_eq!(
1511 u16::from_be_bytes(torn_page[page_size + 6..page_size + 8].try_into().unwrap(),)
1512 as usize,
1513 new_len,
1514 );
1515 let torn_crc =
1516 u32::from_be_bytes(torn_page[page_size + 8..page_size + 12].try_into().unwrap());
1517 assert_ne!(torn_crc, Crc32::checksum(&torn_page[..new_len]));
1518 drop(index);
1519
1520 let (_, value_size) = context
1523 .open(&cfg.value_partition, &0u64.to_be_bytes())
1524 .await
1525 .unwrap();
1526 assert_eq!(value_size, 2 * I32_VALUE_FRAME_SIZE);
1527 });
1528
1529 deterministic::Runner::from(checkpoint).start(|context| async move {
1530 let pending = PendingSyncs::default();
1531 let delayed = DelayedSyncContext {
1532 inner: context.child("delayed"),
1533 pending: pending.clone(),
1534 };
1535 pending.arm();
1536 let cfg = test_config(&delayed, "marker-torn-tail", NZU64!(4));
1537
1538 let archive = drive_pending_syncs(
1541 &pending,
1542 Archive::<_, _, FixedBytes<64>, i32>::init(delayed.child("reopen"), cfg.clone()),
1543 )
1544 .await
1545 .unwrap();
1546 assert_eq!(pending.calls(), 1);
1547 assert_eq!(archive.last_index(), Some(0));
1548 assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 0)]);
1549 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1550 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
1551 drop(archive);
1552
1553 let (_, value_size) = context
1554 .open(&cfg.value_partition, &0u64.to_be_bytes())
1555 .await
1556 .unwrap();
1557 assert_eq!(value_size, I32_VALUE_FRAME_SIZE);
1558 });
1559 }
1560
1561 #[test_traced]
1562 fn test_startup_publishes_validated_marker_without_data_resync() {
1563 deterministic::Runner::default().start(|context| async move {
1564 let cfg = test_config(&context, "startup-order", NZU64!(4));
1565
1566 let archive = Archive::init(context.child("seed"), cfg.clone())
1569 .await
1570 .unwrap();
1571 let archive = archive.put(0, test_key("zero"), 10).await.unwrap();
1572 let archive = archive.sync().await.unwrap();
1573 drop(archive);
1574
1575 let pending = PendingSyncs::default();
1578 let delayed = DelayedSyncContext {
1579 inner: context.child("delayed"),
1580 pending: pending.clone(),
1581 };
1582 pending.arm();
1583 let completed = Arc::new(AtomicUsize::new(0));
1584 let completed_clone = completed.clone();
1585 let reopen_cfg = cfg.clone();
1586 let task = context.child("startup").spawn(|_| async move {
1587 let result =
1588 Archive::<_, _, FixedBytes<64>, i32>::init(delayed.child("reopen"), reopen_cfg)
1589 .await;
1590 completed_clone.store(1, Ordering::Relaxed);
1591 result
1592 });
1593
1594 while pending.calls() == 0 && completed.load(Ordering::Relaxed) == 0 {
1595 commonware_runtime::reschedule().await;
1596 }
1597 commonware_runtime::reschedule().await;
1598
1599 let calls = pending.calls();
1602 let finished = completed.load(Ordering::Relaxed);
1603 if calls != 1 || finished != 1 {
1604 pending.unblock();
1605 let _ = task.await;
1606 panic!(
1607 "clean startup must return after starting one marker sync, calls={calls}, \
1608 finished={finished}"
1609 );
1610 }
1611
1612 pending.unblock();
1613 let archive = task.await.unwrap().unwrap().sync().await.unwrap();
1614 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1615 drop(archive);
1616
1617 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("marker_reopen"), cfg)
1619 .await
1620 .unwrap()
1621 .destroy()
1622 .await
1623 .unwrap();
1624 });
1625 }
1626
1627 #[test_traced]
1628 fn test_startup_marker_failure_fails_next_sync() {
1629 deterministic::Runner::default().start(|context| async move {
1630 let cfg = test_config(&context, "startup-marker-failure", NZU64!(4));
1631
1632 let archive = Archive::init(context.child("seed"), cfg.clone())
1634 .await
1635 .unwrap();
1636 let archive = archive.put(0, test_key("zero"), 10).await.unwrap();
1637 let archive = archive.sync().await.unwrap();
1638 drop(archive);
1639
1640 let pending = PendingSyncs::default();
1643 let delayed = DelayedSyncContext {
1644 inner: context.child("delayed"),
1645 pending: pending.clone(),
1646 };
1647 pending.arm();
1648 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(delayed.child("reopen"), cfg)
1649 .await
1650 .unwrap();
1651 assert_eq!(pending.lock().len(), 1);
1652
1653 fail_pending_syncs(&pending);
1656 assert!(matches!(archive.sync().await, Err(Error::Metadata(_))));
1657 });
1658 }
1659
1660 #[test_traced]
1661 fn test_start_sync_publishes_closed_section_boundary() {
1662 let executor = deterministic::Runner::default();
1663 executor.start(|context| async move {
1664 let cfg = test_config(&context, "marker-lag", NZU64!(4));
1665
1666 let pending = PendingSyncs::default();
1667 let delayed = DelayedSyncContext {
1668 inner: context.child("delayed"),
1669 pending: pending.clone(),
1670 };
1671 let archive = Archive::init(delayed.child("archive"), cfg.clone())
1672 .await
1673 .unwrap();
1674
1675 let (archive, first) = archive
1682 .put_start_sync(0, test_key("zero"), 10)
1683 .await
1684 .unwrap();
1685 assert_eq!(pending.lock().len(), 2);
1686 release_pending_syncs(&pending);
1687 first.await.unwrap();
1688
1689 let archive = archive.put(4, test_key("four"), 40).await.unwrap();
1690 let (archive, second) = archive.start_sync().await.unwrap();
1691 assert_eq!(pending.lock().len(), 3);
1692 release_pending_syncs(&pending);
1693 second.await.unwrap();
1694 drop(archive);
1695
1696 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1699 context.child("first_reopen"),
1700 cfg.clone(),
1701 )
1702 .await
1703 .unwrap();
1704 assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 0), (4, 4)]);
1705 drop(archive);
1706
1707 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("second_reopen"), cfg)
1708 .await
1709 .unwrap()
1710 .destroy()
1711 .await
1712 .unwrap();
1713 });
1714 }
1715
1716 #[test_traced]
1717 fn test_sync_publishes_closed_section_boundary() {
1718 let executor = deterministic::Runner::default();
1719 executor.start(|context| async move {
1720 let cfg = test_config(&context, "blocking-marker-lag", NZU64!(4));
1721
1722 let pending = PendingSyncs::default();
1723 let delayed = DelayedSyncContext {
1724 inner: context.child("delayed"),
1725 pending: pending.clone(),
1726 };
1727 let archive = Archive::init(delayed.child("archive"), cfg.clone())
1728 .await
1729 .unwrap();
1730
1731 let (archive, first) = archive
1733 .put_start_sync(0, test_key("zero"), 10)
1734 .await
1735 .unwrap();
1736 assert_eq!(pending.lock().len(), 2);
1737 release_pending_syncs(&pending);
1738 first.await.unwrap();
1739
1740 let archive = archive.put(4, test_key("four"), 40).await.unwrap();
1742 pending.arm();
1743 let completed = Arc::new(AtomicUsize::new(0));
1744 let completed_clone = completed.clone();
1745 let task = delayed.inner.child("sync").spawn(|_| async move {
1746 let archive = archive.sync().await.unwrap();
1747 completed_clone.store(1, Ordering::Relaxed);
1748 archive
1749 });
1750
1751 while pending.calls() < 2 {
1752 commonware_runtime::reschedule().await;
1753 }
1754 commonware_runtime::reschedule().await;
1755
1756 let parked_syncs = pending.lock().len();
1762 if parked_syncs != 3 {
1763 pending.unblock();
1766 let _ = task.await;
1767 panic!(
1768 "blocking sync must not serialize the derived marker behind data: \
1769 parked {parked_syncs} durability operations"
1770 );
1771 }
1772
1773 let metadata = pending.lock().remove(2);
1778 metadata.release.send(Ok(())).unwrap();
1779 commonware_runtime::reschedule().await;
1780 assert_eq!(
1781 completed.load(Ordering::Relaxed),
1782 0,
1783 "publishing the previous marker must not complete the current data sync"
1784 );
1785
1786 release_pending_syncs(&pending);
1787 let archive = task.await.unwrap();
1788 drop(archive);
1789
1790 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1793 delayed.inner.child("first_reopen"),
1794 cfg.clone(),
1795 )
1796 .await
1797 .unwrap();
1798 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1799 assert_eq!(archive.get(Identifier::Index(4)).await.unwrap(), Some(40));
1800 drop(archive);
1801
1802 Archive::<_, _, FixedBytes<64>, i32>::init(delayed.inner.child("second_reopen"), cfg)
1803 .await
1804 .unwrap()
1805 .destroy()
1806 .await
1807 .unwrap();
1808 });
1809 }
1810
1811 #[test_traced]
1812 fn test_sync_delays_immediately_ready_durable_boundary() {
1813 let executor = deterministic::Runner::default();
1814 executor.start(|context| async move {
1815 let pending = PendingSyncs::default();
1817 pending.unblock();
1818 let immediate = DelayedSyncContext {
1819 inner: context,
1820 pending: pending.clone(),
1821 };
1822 let cfg = test_config(&immediate, "ready-marker-lag", NZU64!(4));
1823
1824 let archive = Archive::init(immediate.child("archive"), cfg)
1825 .await
1826 .unwrap();
1827 let initial_starts = pending.starts();
1828 let archive = archive.put_sync(0, test_key("zero"), 10).await.unwrap();
1829
1830 assert_eq!(pending.starts() - initial_starts, 2);
1838
1839 let archive = archive.sync().await.unwrap();
1840 assert_eq!(pending.starts() - initial_starts, 3);
1841 archive.destroy().await.unwrap();
1842 });
1843 }
1844
1845 #[test_traced]
1846 fn test_sync_batches_markers_by_active_section() {
1847 let executor = deterministic::Runner::default();
1848 executor.start(|context| async move {
1849 let pending = PendingSyncs::default();
1851 pending.unblock();
1852 let immediate = DelayedSyncContext {
1853 inner: context,
1854 pending: pending.clone(),
1855 };
1856 let cfg = test_config(&immediate, "section-marker-batch", NZU64!(4));
1857 let archive = Archive::init(immediate.child("archive"), cfg)
1858 .await
1859 .unwrap();
1860 let initial_starts = pending.starts();
1861
1862 let archive = archive.put_sync(0, test_key("zero"), 10).await.unwrap();
1865 let archive = archive.put_sync(1, test_key("one"), 20).await.unwrap();
1866 assert_eq!(pending.starts() - initial_starts, 4);
1867
1868 let archive = archive.put_sync(4, test_key("four"), 40).await.unwrap();
1872 assert_eq!(pending.starts() - initial_starts, 7);
1873 let archive = archive.sync().await.unwrap();
1874 assert_eq!(pending.starts() - initial_starts, 8);
1875 let archive = archive.sync().await.unwrap();
1876 assert_eq!(pending.starts() - initial_starts, 8);
1877 archive.destroy().await.unwrap();
1878 });
1879 }
1880
1881 #[test_traced]
1882 fn test_start_sync_withholds_marker_for_unproven_boundary() {
1883 let executor = deterministic::Runner::default();
1884 executor.start(|context| async move {
1885 let cfg = test_config(&context, "marker-unproven-boundary", NZU64!(4));
1886
1887 let pending = PendingSyncs::default();
1888 let delayed = DelayedSyncContext {
1889 inner: context.child("delayed"),
1890 pending: pending.clone(),
1891 };
1892 let archive = Archive::init(delayed.child("archive"), cfg.clone())
1893 .await
1894 .unwrap();
1895
1896 let (archive, first) = archive
1898 .put_start_sync(0, test_key("zero"), 10)
1899 .await
1900 .unwrap();
1901 release_pending_syncs(&pending);
1902 first.await.unwrap();
1903
1904 let archive = archive.put(4, test_key("four"), 40).await.unwrap();
1906 let (archive, second) = archive.start_sync().await.unwrap();
1907 assert_eq!(pending.lock().len(), 3);
1908
1909 let marker = pending.lock().pop().expect("marker sync parked");
1913 marker
1914 .release
1915 .send(Ok(()))
1916 .expect("marker sync receiver dropped");
1917
1918 let archive = archive.put(8, test_key("eight"), 80).await.unwrap();
1924 let before = pending.starts();
1925 let (archive, third) = archive.start_sync().await.unwrap();
1926 assert_eq!(pending.starts() - before, 2);
1927
1928 release_pending_syncs(&pending);
1929 second.await.unwrap();
1930 third.await.unwrap();
1931
1932 let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
1935 drop(archive);
1936 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
1937 .await
1938 .unwrap();
1939 assert_eq!(
1940 archive.ranges().collect::<Vec<_>>(),
1941 vec![(0, 0), (4, 4), (8, 8)]
1942 );
1943 archive.destroy().await.unwrap();
1944 });
1945 }
1946
1947 #[test_traced]
1948 fn test_sync_publishes_previous_durable_sections_across_section_changes() {
1949 let executor = deterministic::Runner::default();
1950 executor.start(|context| async move {
1951 let cfg = test_config(&context, "cross-section-marker-lag", NZU64!(1));
1952
1953 let pending = PendingSyncs::default();
1954 let delayed = DelayedSyncContext {
1955 inner: context.child("delayed"),
1956 pending: pending.clone(),
1957 };
1958 let archive = Archive::init(delayed.child("archive"), cfg.clone())
1959 .await
1960 .unwrap();
1961
1962 let archive = drive_pending_syncs(&pending, archive.put_sync(0, test_key("zero"), 10))
1965 .await
1966 .unwrap();
1967 let archive = drive_pending_syncs(&pending, archive.put_sync(1, test_key("one"), 20))
1968 .await
1969 .unwrap();
1970 let archive = drive_pending_syncs(&pending, archive.put_sync(2, test_key("two"), 30))
1971 .await
1972 .unwrap();
1973 drop(archive);
1974
1975 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1985 context.child("first_reopen"),
1986 cfg.clone(),
1987 )
1988 .await
1989 .unwrap();
1990 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1991 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1992 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(30));
1993 drop(archive);
1994
1995 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("second_reopen"), cfg)
1996 .await
1997 .unwrap()
1998 .destroy()
1999 .await
2000 .unwrap();
2001 });
2002 }
2003
2004 #[test_traced]
2005 fn test_empty_sync_publishes_final_durable_boundary() {
2006 let executor = deterministic::Runner::default();
2007 executor.start(|context| async move {
2008 let cfg = test_config(&context, "empty-sync-marker", NZU64!(1));
2009
2010 let pending = PendingSyncs::default();
2011 let delayed = DelayedSyncContext {
2012 inner: context.child("delayed"),
2013 pending: pending.clone(),
2014 };
2015 let archive = Archive::init(delayed.child("archive"), cfg.clone())
2016 .await
2017 .unwrap();
2018
2019 let archive = drive_pending_syncs(&pending, archive.put_sync(0, test_key("zero"), 10))
2021 .await
2022 .unwrap();
2023 assert_eq!(pending.starts(), 2);
2024
2025 let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2031 assert_eq!(pending.starts(), 3);
2032
2033 let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2035 assert_eq!(pending.starts(), 3);
2036 drop(archive);
2037
2038 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2039 .await
2040 .unwrap();
2041 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
2042 });
2043 }
2044
2045 #[test_traced]
2046 fn test_sync_recreates_settled_section_barrier() {
2047 let executor = deterministic::Runner::default();
2048 executor.start(|context| async move {
2049 let cfg = test_config(&context, "marker-recreate", NZU64!(2));
2050
2051 let pending = PendingSyncs::default();
2052 let delayed = DelayedSyncContext {
2053 inner: context.child("delayed"),
2054 pending: pending.clone(),
2055 };
2056 let archive = Archive::init(delayed.child("archive"), cfg.clone())
2057 .await
2058 .unwrap();
2059
2060 let archive = drive_pending_syncs(&pending, archive.put_sync(0, test_key("zero"), 10))
2064 .await
2065 .unwrap();
2066 assert_eq!(pending.starts(), 2);
2067 let archive = drive_pending_syncs(&pending, archive.put_sync(2, test_key("two"), 30))
2068 .await
2069 .unwrap();
2070 assert_eq!(pending.starts(), 5);
2071 let archive = drive_pending_syncs(&pending, archive.put_sync(1, test_key("one"), 20))
2072 .await
2073 .unwrap();
2074 assert_eq!(pending.starts(), 8);
2075
2076 let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2078 assert_eq!(pending.starts(), 9);
2079 let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2080 assert_eq!(pending.starts(), 9);
2081 drop(archive);
2082
2083 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2086 .await
2087 .unwrap();
2088 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
2089 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
2090 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(30));
2091 });
2092 }
2093
2094 #[test_traced]
2095 fn test_prune_clears_validation_marker_before_section_reuse() {
2096 let executor = deterministic::Runner::default();
2097 executor.start(|context| async move {
2098 let cfg = test_config(&context, "marker-reuse", NZU64!(2));
2099
2100 let archive = Archive::init(context.child("seed"), cfg.clone())
2105 .await
2106 .unwrap();
2107 let archive = archive.put_sync(0, test_key("old"), 10).await.unwrap();
2108 let archive = archive.sync().await.unwrap();
2109 let archive = archive.prune(2).await.unwrap();
2110 drop(archive);
2111
2112 let pending = PendingSyncs::default();
2121 let delayed = DelayedSyncContext {
2122 inner: context.child("delayed"),
2123 pending: pending.clone(),
2124 };
2125 let archive = Archive::init(delayed.child("reuse"), cfg.clone())
2126 .await
2127 .unwrap();
2128 let archive = archive.put(0, test_key("new"), 20).await.unwrap();
2129 let (archive, handle) = archive.start_sync().await.unwrap();
2130 assert_eq!(pending.lock().len(), 2);
2131 release_pending_syncs(&pending);
2132 handle.await.unwrap();
2133 drop(archive);
2134
2135 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2136 .await
2137 .unwrap();
2138 assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(20));
2139 });
2140 }
2141
2142 #[test_traced]
2143 fn test_archive_compression_then_none() {
2144 let executor = deterministic::Runner::default();
2146 executor.start(|context| async move {
2147 let cfg = Config {
2149 translator: FourCap,
2150 metadata_partition: "test-metadata".into(),
2151 key_partition: "test-index".into(),
2152 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2153 value_partition: "test-value".into(),
2154 codec_config: (),
2155 compression: Some(3),
2156 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2157 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2158 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2159 items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2160 };
2161 let mut archive = Archive::init(context.child("first"), cfg.clone())
2162 .await
2163 .expect("Failed to initialize archive");
2164
2165 let index = 1u64;
2167 let key = test_key("testkey");
2168 let data = 1;
2169 archive = archive
2170 .put(index, key.clone(), data)
2171 .await
2172 .expect("Failed to put data");
2173
2174 let archive = archive.sync().await.expect("Failed to sync archive");
2176 drop(archive);
2177
2178 let cfg = Config {
2181 translator: FourCap,
2182 metadata_partition: "test-metadata".into(),
2183 key_partition: "test-index".into(),
2184 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2185 value_partition: "test-value".into(),
2186 codec_config: (),
2187 compression: None,
2188 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2189 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2190 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2191 items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2192 };
2193 let archive =
2194 Archive::<_, _, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone())
2195 .await
2196 .unwrap();
2197
2198 let result: Result<Option<i32>, _> = archive.get(Identifier::Index(index)).await;
2202 assert!(matches!(
2203 result,
2204 Err(Error::Journal(JournalError::Codec(CodecError::ExtraData(
2205 _
2206 ))))
2207 ));
2208 });
2209 }
2210
2211 #[test_traced]
2212 fn test_archive_overlapping_key_basic() {
2213 let executor = deterministic::Runner::default();
2215 executor.start(|context| async move {
2216 let cfg = Config {
2218 translator: FourCap,
2219 metadata_partition: "test-metadata".into(),
2220 key_partition: "test-index".into(),
2221 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2222 value_partition: "test-value".into(),
2223 codec_config: (),
2224 compression: None,
2225 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2226 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2227 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2228 items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2229 };
2230 let mut archive = Archive::init(context.child("storage"), cfg.clone())
2231 .await
2232 .expect("Failed to initialize archive");
2233
2234 let index1 = 1u64;
2235 let key1 = test_key("keys1");
2236 let data1 = 1;
2237 let index2 = 2u64;
2238 let key2 = test_key("keys2");
2239 let data2 = 2;
2240
2241 archive = archive
2243 .put(index1, key1.clone(), data1)
2244 .await
2245 .expect("Failed to put data");
2246
2247 archive = archive
2249 .put(index2, key2.clone(), data2)
2250 .await
2251 .expect("Failed to put data");
2252
2253 let retrieved = archive
2255 .get(Identifier::Key(&key1))
2256 .await
2257 .expect("Failed to get data")
2258 .expect("Data not found");
2259 assert_eq!(retrieved, data1);
2260
2261 let retrieved = archive
2263 .get(Identifier::Key(&key2))
2264 .await
2265 .expect("Failed to get data")
2266 .expect("Data not found");
2267 assert_eq!(retrieved, data2);
2268
2269 let buffer = context.encode();
2271 assert!(has_metric_value(&buffer, "items_tracked", 2));
2272 assert!(buffer.contains("unnecessary_reads_total 1"));
2273 assert!(buffer.contains("gets_total 2"));
2274 });
2275 }
2276
2277 #[test_traced]
2278 fn test_archive_overlapping_key_multiple_sections() {
2279 let executor = deterministic::Runner::default();
2281 executor.start(|context| async move {
2282 let cfg = Config {
2284 translator: FourCap,
2285 metadata_partition: "test-metadata".into(),
2286 key_partition: "test-index".into(),
2287 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2288 value_partition: "test-value".into(),
2289 codec_config: (),
2290 compression: None,
2291 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2292 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2293 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2294 items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2295 };
2296 let mut archive = Archive::init(context.child("storage"), cfg.clone())
2297 .await
2298 .expect("Failed to initialize archive");
2299
2300 let index1 = 1u64;
2301 let key1 = test_key("keys1");
2302 let data1 = 1;
2303 let index2 = 2_000_000u64;
2304 let key2 = test_key("keys2");
2305 let data2 = 2;
2306
2307 archive = archive
2309 .put(index1, key1.clone(), data1)
2310 .await
2311 .expect("Failed to put data");
2312
2313 archive = archive
2315 .put(index2, key2.clone(), data2)
2316 .await
2317 .expect("Failed to put data");
2318
2319 let retrieved = archive
2321 .get(Identifier::Key(&key1))
2322 .await
2323 .expect("Failed to get data")
2324 .expect("Data not found");
2325 assert_eq!(retrieved, data1);
2326
2327 let retrieved = archive
2329 .get(Identifier::Key(&key2))
2330 .await
2331 .expect("Failed to get data")
2332 .expect("Data not found");
2333 assert_eq!(retrieved, data2);
2334 });
2335 }
2336
2337 #[test_traced]
2338 fn test_archive_prune_keys() {
2339 let executor = deterministic::Runner::default();
2341 executor.start(|context| async move {
2342 let cfg = Config {
2344 translator: FourCap,
2345 metadata_partition: "test-metadata".into(),
2346 key_partition: "test-index".into(),
2347 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2348 value_partition: "test-value".into(),
2349 codec_config: (),
2350 compression: None,
2351 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2352 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2353 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2354 items_per_section: NZU64!(1), };
2356 let mut archive = Archive::init(context.child("storage"), cfg.clone())
2357 .await
2358 .expect("Failed to initialize archive");
2359
2360 let keys = vec![
2362 (1u64, test_key("key1-blah"), 1),
2363 (2u64, test_key("key2-blah"), 2),
2364 (3u64, test_key("key3-blah"), 3),
2365 (4u64, test_key("key3-bleh"), 3),
2366 (5u64, test_key("key4-blah"), 4),
2367 ];
2368
2369 for (index, key, data) in &keys {
2370 archive = archive
2371 .put(*index, key.clone(), *data)
2372 .await
2373 .expect("Failed to put data");
2374 }
2375
2376 let buffer = context.encode();
2378 assert!(has_metric_value(&buffer, "items_tracked", 5));
2379
2380 archive = archive.prune(3).await.expect("Failed to prune");
2382
2383 for (index, key, data) in keys {
2385 let retrieved = archive
2386 .get(Identifier::Key(&key))
2387 .await
2388 .expect("Failed to get data");
2389 if index < 3 {
2390 assert!(retrieved.is_none());
2391 } else {
2392 assert_eq!(retrieved.expect("Data not found"), data);
2393 }
2394 }
2395
2396 let buffer = context.encode();
2398 assert!(has_metric_value(&buffer, "items_tracked", 3));
2399 assert!(has_metric_value(&buffer, "indices_pruned_total", 2));
2400 assert!(has_metric_value(&buffer, "pruned_total", 0)); archive = archive.prune(2).await.expect("Failed to prune");
2404
2405 archive = archive.prune(3).await.expect("Failed to prune");
2407
2408 archive = archive
2410 .put(6, test_key("key2-blfh"), 5)
2411 .await
2412 .expect("Failed to put data");
2413
2414 let buffer = context.encode();
2416 assert!(has_metric_value(&buffer, "items_tracked", 4)); assert!(has_metric_value(&buffer, "indices_pruned_total", 2));
2418 assert!(has_metric_value(&buffer, "pruned_total", 1));
2419
2420 let archive = archive
2422 .put(1, test_key("key1-blah"), 1)
2423 .await
2424 .expect("Failed to put below floor");
2425 assert_eq!(
2426 archive
2427 .get(Identifier::Key(&test_key("key1-blah")))
2428 .await
2429 .expect("Failed to get data"),
2430 None
2431 );
2432
2433 let (archive, handle) = archive
2436 .put_start_sync(1, test_key("key1-blah"), 1)
2437 .await
2438 .expect("Failed to put_start_sync below floor");
2439 handle.await.expect("handle must resolve");
2440 let archive = archive
2441 .put_sync(2, test_key("key2-blfh"), 2)
2442 .await
2443 .expect("Failed to put_sync below floor");
2444 assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
2445 assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), None);
2446 });
2447 }
2448
2449 fn test_archive_keys_and_restart(num_keys: usize) -> String {
2450 let executor = deterministic::Runner::default();
2452 executor.start(|mut context| async move {
2453 let items_per_section = 256u64;
2455 let cfg = Config {
2456 translator: TwoCap,
2457 metadata_partition: "test-metadata".into(),
2458 key_partition: "test-index".into(),
2459 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2460 value_partition: "test-value".into(),
2461 codec_config: (),
2462 compression: None,
2463 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2464 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2465 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2466 items_per_section: NZU64!(items_per_section),
2467 };
2468 let mut archive = Archive::init(
2469 context.child("init").with_attribute("index", 1),
2470 cfg.clone(),
2471 )
2472 .await
2473 .expect("Failed to initialize archive");
2474
2475 let mut keys = BTreeMap::new();
2477 while keys.len() < num_keys {
2478 let index = keys.len() as u64;
2479 let mut key = [0u8; 64];
2480 context.fill(&mut key);
2481 let key = FixedBytes::<64>::decode(key.as_ref()).unwrap();
2482 let mut data = [0u8; 1024];
2483 context.fill(&mut data);
2484 let data = FixedBytes::<1024>::decode(data.as_ref()).unwrap();
2485
2486 archive = archive
2487 .put(index, key.clone(), data.clone())
2488 .await
2489 .expect("Failed to put data");
2490 keys.insert(key, (index, data));
2491 }
2492
2493 for (key, (index, data)) in &keys {
2495 let retrieved = archive
2496 .get(Identifier::Index(*index))
2497 .await
2498 .expect("Failed to get data")
2499 .expect("Data not found");
2500 assert_eq!(&retrieved, data);
2501 let retrieved = archive
2502 .get(Identifier::Key(key))
2503 .await
2504 .expect("Failed to get data")
2505 .expect("Data not found");
2506 assert_eq!(&retrieved, data);
2507 }
2508
2509 let buffer = context.encode();
2511 assert!(has_metric_value(&buffer, "items_tracked", num_keys));
2512 assert!(has_metric_value(&buffer, "pruned_total", 0));
2513
2514 let archive = archive.sync().await.expect("Failed to sync archive");
2516 drop(archive);
2517
2518 let cfg = Config {
2520 translator: TwoCap,
2521 metadata_partition: "test-metadata".into(),
2522 key_partition: "test-index".into(),
2523 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2524 value_partition: "test-value".into(),
2525 codec_config: (),
2526 compression: None,
2527 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2528 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2529 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2530 items_per_section: NZU64!(items_per_section),
2531 };
2532 let mut archive = Archive::<_, _, _, FixedBytes<1024>>::init(
2533 context.child("init").with_attribute("index", 2),
2534 cfg.clone(),
2535 )
2536 .await
2537 .expect("Failed to initialize archive");
2538
2539 for (key, (index, data)) in &keys {
2541 let retrieved = archive
2542 .get(Identifier::Index(*index))
2543 .await
2544 .expect("Failed to get data")
2545 .expect("Data not found");
2546 assert_eq!(&retrieved, data);
2547 let retrieved = archive
2548 .get(Identifier::Key(key))
2549 .await
2550 .expect("Failed to get data")
2551 .expect("Data not found");
2552 assert_eq!(&retrieved, data);
2553 }
2554
2555 let min = (keys.len() / 2) as u64;
2557 archive = archive.prune(min).await.expect("Failed to prune");
2558
2559 let min = (min / items_per_section) * items_per_section;
2561 let mut removed = 0;
2562 for (key, (index, data)) in keys {
2563 if index >= min {
2564 let retrieved = archive
2565 .get(Identifier::Key(&key))
2566 .await
2567 .expect("Failed to get data")
2568 .expect("Data not found");
2569 assert_eq!(retrieved, data);
2570
2571 let (current_end, start_next) = archive.next_gap(index);
2573 assert_eq!(current_end.unwrap(), num_keys as u64 - 1);
2574 assert!(start_next.is_none());
2575 } else {
2576 let retrieved = archive
2577 .get(Identifier::Key(&key))
2578 .await
2579 .expect("Failed to get data");
2580 assert!(retrieved.is_none());
2581 removed += 1;
2582
2583 let (current_end, start_next) = archive.next_gap(index);
2585 assert!(current_end.is_none());
2586 assert_eq!(start_next.unwrap(), min);
2587 }
2588 }
2589
2590 let buffer = context.encode();
2592 assert!(has_metric_value(
2593 &buffer,
2594 "items_tracked",
2595 num_keys - removed
2596 ));
2597 assert!(has_metric_value(&buffer, "indices_pruned_total", removed));
2598 assert!(has_metric_value(&buffer, "pruned_total", 0)); context.auditor().state()
2601 })
2602 }
2603
2604 #[test_group("slow")]
2605 #[test_traced]
2606 fn test_archive_many_keys_and_restart() {
2607 test_archive_keys_and_restart(100_000);
2608 }
2609
2610 #[test_group("slow")]
2611 #[test_traced]
2612 fn test_determinism() {
2613 let state1 = test_archive_keys_and_restart(5_000);
2614 let state2 = test_archive_keys_and_restart(5_000);
2615 assert_eq!(state1, state2);
2616 }
2617
2618 #[test_traced]
2625 fn test_archive_key_lookup_skips_pruned_duplicates() {
2626 let executor = deterministic::Runner::default();
2627 executor.start(|context| async move {
2628 let cfg = Config {
2629 translator: FourCap,
2630 metadata_partition: "test-metadata".into(),
2631 key_partition: "test-index".into(),
2632 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2633 value_partition: "test-value".into(),
2634 codec_config: (),
2635 compression: None,
2636 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2637 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2638 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2639 items_per_section: NZU64!(1),
2640 };
2641 let mut archive = Archive::init(context.child("storage"), cfg)
2642 .await
2643 .expect("Failed to initialize archive");
2644
2645 let key = test_key("dupe-key");
2649 archive = archive.put(2, key.clone(), 20).await.unwrap();
2650 archive = archive.put(5, key.clone(), 50).await.unwrap();
2651
2652 assert!(archive.get(Identifier::Key(&key)).await.unwrap().is_some());
2656 assert!(archive.has(Identifier::Key(&key)).await.unwrap());
2657
2658 archive = archive.prune(3).await.unwrap();
2661 let got = archive.get(Identifier::Key(&key)).await.unwrap();
2662 assert_eq!(
2663 got,
2664 Some(50),
2665 "key lookup must skip the pruned entry and return the surviving one"
2666 );
2667 assert!(archive.has(Identifier::Key(&key)).await.unwrap());
2668
2669 let archive = archive.prune(6).await.unwrap();
2671 assert_eq!(archive.get(Identifier::Key(&key)).await.unwrap(), None);
2672 assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
2673 });
2674 }
2675
2676 #[test_traced]
2677 fn test_get_all_after_prune() {
2678 let executor = deterministic::Runner::default();
2679 executor.start(|context| async move {
2680 let cfg = Config {
2681 translator: FourCap,
2682 metadata_partition: "test-metadata".into(),
2683 key_partition: "test-index".into(),
2684 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2685 value_partition: "test-value".into(),
2686 codec_config: (),
2687 compression: None,
2688 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2689 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2690 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2691 items_per_section: NZU64!(1),
2692 };
2693 let mut archive = Archive::init(context.child("storage"), cfg)
2694 .await
2695 .expect("Failed to initialize archive");
2696
2697 archive = archive.put_multi(1, test_key("aaa"), 10).await.unwrap();
2698 archive = archive.put_multi(1, test_key("bbb"), 20).await.unwrap();
2699 archive = archive.put_multi(3, test_key("ccc"), 30).await.unwrap();
2700
2701 let archive = archive.prune(3).await.unwrap();
2703
2704 let all = archive.get_all(1).await.unwrap();
2706 assert_eq!(all, None);
2707
2708 let all = archive.get_all(3).await.unwrap();
2710 assert_eq!(all, Some(vec![30]));
2711 });
2712 }
2713
2714 #[test_traced]
2715 fn test_has_at() {
2716 let executor = deterministic::Runner::default();
2717 let (_, checkpoint) = executor.start_and_recover(|context| async move {
2718 let cfg = test_config(&context, "test", NZU64!(2));
2719 let mut archive = Archive::init(context.child("storage"), cfg)
2720 .await
2721 .expect("Failed to initialize archive");
2722
2723 assert!(!archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2725
2726 archive = archive.put_multi(1, test_key("aaaa1"), 10).await.unwrap();
2728 assert!(archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2729
2730 assert!(!archive.has_at(2, &test_key("aaaa1")).await.unwrap());
2732
2733 assert!(!archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2736
2737 archive = archive.put_multi(1, test_key("aaaa2"), 20).await.unwrap();
2739 assert!(archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2740 assert!(archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2741
2742 assert!(!archive.has_at(1, &test_key("bbbb")).await.unwrap());
2744
2745 archive = archive.put_multi(3, test_key("cccc"), 30).await.unwrap();
2746 archive.sync().await.unwrap();
2747 });
2748
2749 deterministic::Runner::from(checkpoint).start(|context| async move {
2750 let cfg = test_config(&context, "test", NZU64!(2));
2751 let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2752 .await
2753 .expect("Failed to reopen archive");
2754
2755 assert!(archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2757 assert!(archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2758 assert!(!archive.has_at(1, &test_key("bbbb")).await.unwrap());
2759
2760 let archive = archive.prune(2).await.unwrap();
2762 assert!(!archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2763 assert!(!archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2764 assert!(archive.has_at(3, &test_key("cccc")).await.unwrap());
2765
2766 archive.destroy().await.unwrap();
2767 });
2768 }
2769
2770 #[test_traced]
2771 fn test_has_key() {
2772 let executor = deterministic::Runner::default();
2773 executor.start(|context| async move {
2774 let cfg = test_config(&context, "test", NZU64!(2));
2775 let mut archive = Archive::init(context.child("storage"), cfg)
2776 .await
2777 .expect("Failed to initialize archive");
2778
2779 let key = test_key("aaaa1");
2781 assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
2782
2783 archive = archive.put(1, key.clone(), 10).await.unwrap();
2785 assert!(archive.has(Identifier::Key(&key)).await.unwrap());
2786
2787 let collision = test_key("aaaa2");
2790 assert!(!archive.has(Identifier::Key(&collision)).await.unwrap());
2791 archive = archive.put(2, collision.clone(), 20).await.unwrap();
2792 assert!(archive.has(Identifier::Key(&collision)).await.unwrap());
2793
2794 archive = archive.put(4, test_key("cccc"), 30).await.unwrap();
2798 let archive = archive.prune(4).await.unwrap();
2799 assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
2800 assert!(!archive.has(Identifier::Key(&collision)).await.unwrap());
2801 assert!(
2802 archive
2803 .has(Identifier::Key(&test_key("cccc")))
2804 .await
2805 .unwrap()
2806 );
2807
2808 archive.destroy().await.unwrap();
2809 });
2810 }
2811
2812 #[test_traced]
2813 fn test_put_multi_prune() {
2814 let executor = deterministic::Runner::default();
2815 executor.start(|context| async move {
2816 let cfg = Config {
2817 translator: FourCap,
2818 metadata_partition: "test-metadata".into(),
2819 key_partition: "test-index".into(),
2820 key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2821 value_partition: "test-value".into(),
2822 codec_config: (),
2823 compression: None,
2824 key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2825 value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2826 replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2827 items_per_section: NZU64!(1),
2828 };
2829 let mut archive = Archive::init(context.child("storage"), cfg)
2830 .await
2831 .expect("Failed to initialize archive");
2832
2833 archive = archive.put_multi(1, test_key("aaa"), 10).await.unwrap();
2835 archive = archive.put_multi(1, test_key("bbb"), 20).await.unwrap();
2836 archive = archive.put_multi(3, test_key("ccc"), 30).await.unwrap();
2837
2838 let buffer = context.encode();
2839 assert!(has_metric_value(&buffer, "items_tracked", 2));
2840
2841 let archive = archive.prune(3).await.unwrap();
2843
2844 assert_eq!(
2846 archive
2847 .get(Identifier::Key(&test_key("aaa")))
2848 .await
2849 .unwrap(),
2850 None
2851 );
2852 assert_eq!(
2853 archive
2854 .get(Identifier::Key(&test_key("bbb")))
2855 .await
2856 .unwrap(),
2857 None
2858 );
2859
2860 assert_eq!(
2862 archive
2863 .get(Identifier::Key(&test_key("ccc")))
2864 .await
2865 .unwrap(),
2866 Some(30)
2867 );
2868
2869 let buffer = context.encode();
2870 assert!(has_metric_value(&buffer, "items_tracked", 1));
2871 assert!(has_metric_value(&buffer, "indices_pruned_total", 1));
2872
2873 let archive = archive
2875 .put_multi(2, test_key("ddd"), 40)
2876 .await
2877 .expect("Failed to put below floor");
2878 assert_eq!(
2879 archive
2880 .get(Identifier::Key(&test_key("ddd")))
2881 .await
2882 .expect("Failed to get data"),
2883 None
2884 );
2885
2886 let (archive, handle) = archive
2889 .put_multi_start_sync(2, test_key("ddd"), 41)
2890 .await
2891 .expect("Failed to put_multi_start_sync below floor");
2892 handle.await.expect("handle must resolve");
2893 assert_eq!(archive.get_all(2).await.expect("Failed to get data"), None);
2894
2895 let archive = archive
2897 .put_multi_sync(2, test_key("ddd"), 42)
2898 .await
2899 .expect("Failed to put_multi_sync below floor");
2900 assert_eq!(archive.get_all(2).await.expect("Failed to get data"), None);
2901 });
2902 }
2903}