mod storage;
use commonware_runtime::buffer::paged::CacheRef;
use std::num::{NonZeroU64, NonZeroUsize};
pub use storage::Archive;
#[derive(Clone)]
pub struct Config<C> {
pub metadata_partition: String,
pub freezer_table_partition: String,
pub freezer_table_initial_size: u32,
pub freezer_table_resize_frequency: u8,
pub freezer_table_resize_chunk_size: u32,
pub freezer_key_partition: String,
pub freezer_key_page_cache: CacheRef,
pub freezer_value_partition: String,
pub freezer_value_target_size: u64,
pub freezer_value_compression: Option<u8>,
pub ordinal_partition: String,
pub items_per_section: NonZeroU64,
pub freezer_key_write_buffer: NonZeroUsize,
pub freezer_value_write_buffer: NonZeroUsize,
pub ordinal_write_buffer: NonZeroUsize,
pub replay_buffer: NonZeroUsize,
pub codec_config: C,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive::{Archive as ArchiveTrait, Error, Identifier};
use commonware_cryptography::{Hasher, Sha256, sha256::Digest};
use commonware_runtime::{
Runner, Supervisor as _,
buffer::paged::CacheRef,
deterministic::{self, FaultConfig, PartialWriteMode, WriteConfig},
};
use commonware_utils::{NZU16, NZU64, NZUsize, Widen, probability};
use std::num::{NonZeroU16, NonZeroU64};
const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
fn config(items_per_section: u64) -> Config<()> {
Config {
metadata_partition: "destroy-metadata".into(),
freezer_table_partition: "destroy-freezer-table".into(),
freezer_table_initial_size: 4,
freezer_table_resize_frequency: 2,
freezer_table_resize_chunk_size: 2,
freezer_key_partition: "destroy-freezer-key".into(),
freezer_key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
freezer_value_partition: "destroy-freezer-value".into(),
freezer_value_target_size: 64,
freezer_value_compression: None,
ordinal_partition: "destroy-ordinal".into(),
items_per_section: NonZeroU64::new(items_per_section).unwrap(),
freezer_key_write_buffer: NZUsize!(1024),
freezer_value_write_buffer: NZUsize!(1024),
ordinal_write_buffer: NZUsize!(1024),
replay_buffer: NZUsize!(1024),
codec_config: (),
}
}
#[test]
fn test_init_rejects_changed_items_per_section() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let key = |index: u64| Sha256::hash(&[&index.to_be_bytes()]);
let archive = Archive::init(context.child("sparse"), config(4))
.await
.unwrap();
let archive = archive.put(0, key(0), &0).await.unwrap();
let archive = archive.put_sync(5, key(5), &50).await.unwrap();
drop(archive);
let result =
Archive::<_, Digest, i32>::init(context.child("sparse_grown"), config(8)).await;
assert!(matches!(result, Err(Error::RecordCorrupted)), "{result:?}");
let result =
Archive::<_, Digest, i32>::init(context.child("sparse_shrunk"), config(2)).await;
assert!(matches!(result, Err(Error::RecordCorrupted)), "{result:?}");
Archive::<_, Digest, i32>::init(context.child("reset"), config(4))
.await
.unwrap()
.destroy()
.await
.unwrap();
let mut archive = Archive::init(context.child("full"), config(8))
.await
.unwrap();
for index in 0..8u64 {
archive = archive
.put(index, key(index), &(index as i32))
.await
.unwrap();
}
let archive = archive.sync().await.unwrap();
drop(archive);
let result =
Archive::<_, Digest, i32>::init(context.child("full_shrunk"), config(4)).await;
assert!(matches!(result, Err(Error::Ordinal(_))), "{result:?}");
let result =
Archive::<_, Digest, i32>::init(context.child("full_grown"), config(16)).await;
assert!(matches!(result, Err(Error::Ordinal(_))), "{result:?}");
let archive = Archive::<_, Digest, i32>::init(context.child("full_same"), config(8))
.await
.unwrap();
for index in 0..8u64 {
assert_eq!(
archive.get(Identifier::Index(index)).await.unwrap(),
Some(index as i32)
);
}
});
}
#[test]
fn test_interrupted_destroy_reopens() {
let entries = || -> Vec<(u64, Digest, i32)> {
(0..11u64)
.map(|index| {
(
index,
Sha256::hash(&[&index.to_be_bytes()]),
index as i32 * 7,
)
})
.collect()
};
let mut interrupted = 0;
for seed in 0..32u64 {
let faults = FaultConfig::default().write(WriteConfig {
failure_rate: probability!(0.0),
retention_rate: probability!(1.0),
mode: PartialWriteMode::Prefix,
});
let runner = deterministic::Runner::new(
deterministic::Config::default()
.with_seed(seed)
.with_storage_fault_config(faults),
);
let (stopped, checkpoint) = runner.start_and_recover(|context| async move {
let cfg = config(4);
let entries = entries();
let mut archive = Archive::init(context.child("first"), cfg).await.unwrap();
for (index, key, value) in &entries[..5] {
archive = archive.put(*index, *key, value).await.unwrap();
}
archive = archive.sync().await.unwrap();
for (index, key, value) in &entries[5..10] {
archive = archive.put(*index, *key, value).await.unwrap();
}
archive = archive.sync().await.unwrap();
let (index, key, value) = &entries[10];
let archive = archive.put(*index, *key, value).await.unwrap();
context.storage_fault_config().write().remove_rate = Some(probability!(0.1));
let result = archive.destroy().await;
context.storage_fault_config().write().remove_rate = None;
result.is_err()
});
if !stopped {
continue;
}
interrupted += 1;
deterministic::Runner::from(checkpoint).start(|context| async move {
let cfg = config(4);
let entries = entries();
let archive = Archive::<_, Digest, i32>::init(context.child("second"), cfg.clone())
.await
.unwrap();
let mut present = Vec::new();
for (index, key, value) in &entries {
let by_index = archive.get(Identifier::Index(*index)).await.unwrap();
assert!(by_index.is_none_or(|stored| stored == *value));
assert_eq!(
archive.get(Identifier::Key(key)).await.unwrap(),
by_index,
"seed {seed} index {index}"
);
assert_eq!(
archive.has(Identifier::Index(*index)).await.unwrap(),
by_index.is_some()
);
assert_eq!(
archive.has(Identifier::Key(key)).await.unwrap(),
by_index.is_some()
);
if by_index.is_some() {
present.push(*index);
}
}
assert!(
[0, 10].contains(&present.len())
&& present.iter().copied().eq(0..Widen::widen(present.len())),
"seed {seed} reopened with {present:?}"
);
archive.destroy().await.unwrap();
let archive = Archive::<_, Digest, i32>::init(context.child("third"), cfg)
.await
.unwrap();
assert_eq!(archive.first_index(), None);
});
}
assert!(interrupted > 0, "no seed interrupted destroy");
}
#[test]
fn test_unclean_shutdown() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = Config {
metadata_partition: "test-metadata2".into(),
freezer_table_partition: "test-freezer-table2".into(),
freezer_table_initial_size: 8192, freezer_table_resize_frequency: 4,
freezer_table_resize_chunk_size: 8192,
freezer_key_partition: "test-freezer-key2".into(),
freezer_key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
freezer_value_partition: "test-freezer-value2".into(),
freezer_value_target_size: 1024 * 1024,
freezer_value_compression: Some(3),
ordinal_partition: "test-ordinal2".into(),
items_per_section: NZU64!(512),
freezer_key_write_buffer: NZUsize!(1024),
freezer_value_write_buffer: NZUsize!(1024),
ordinal_write_buffer: NZUsize!(1024),
replay_buffer: NZUsize!(1024),
codec_config: (),
};
let archive: Archive<_, Digest, i32> =
Archive::init(context.child("first"), cfg.clone())
.await
.unwrap();
drop(archive);
let mut archive = Archive::init(context.child("second"), cfg.clone())
.await
.unwrap();
let key1 = Sha256::hash(&[b"key1"]);
let key2 = Sha256::hash(&[b"key2"]);
archive = archive.put(1, key1, &2000).await.unwrap();
archive = archive.put(2, key2, &2001).await.unwrap();
let archive = archive.sync().await.unwrap();
drop(archive);
let archive = Archive::init(context.child("third"), cfg).await.unwrap();
assert_eq!(
archive
.get(crate::archive::Identifier::Key(&key1))
.await
.unwrap(),
Some(2000)
);
assert_eq!(
archive
.get(crate::archive::Identifier::Key(&key2))
.await
.unwrap(),
Some(2001)
);
});
}
#[test]
fn test_sync_empty_archive_then_restart() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = Config {
metadata_partition: "empty-metadata".into(),
freezer_table_partition: "empty-freezer-table".into(),
freezer_table_initial_size: 8192,
freezer_table_resize_frequency: 4,
freezer_table_resize_chunk_size: 8192,
freezer_key_partition: "empty-freezer-key".into(),
freezer_key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
freezer_value_partition: "empty-freezer-value".into(),
freezer_value_target_size: 1024 * 1024,
freezer_value_compression: Some(3),
ordinal_partition: "empty-ordinal".into(),
items_per_section: NZU64!(512),
freezer_key_write_buffer: NZUsize!(1024),
freezer_value_write_buffer: NZUsize!(1024),
ordinal_write_buffer: NZUsize!(1024),
replay_buffer: NZUsize!(1024),
codec_config: (),
};
let archive: Archive<_, Digest, i32> =
Archive::init(context.child("first"), cfg.clone())
.await
.unwrap();
let archive = archive.sync().await.unwrap();
drop(archive);
let archive: Archive<_, Digest, i32> =
Archive::init(context.child("second"), cfg.clone())
.await
.unwrap();
let key = Sha256::hash(&[b"after-restart"]);
let archive = archive.put_sync(0, key, &42).await.unwrap();
drop(archive);
let archive: Archive<_, Digest, i32> =
Archive::init(context.child("third"), cfg).await.unwrap();
assert_eq!(
archive
.get(crate::archive::Identifier::Key(&key))
.await
.unwrap(),
Some(42)
);
});
}
}