#![cfg(unix)]
use std::{
collections::BTreeMap,
env, fs,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process::Command,
};
use vsdb::{
HnswConfig, InstanceId, Mapx, MetricKind, MptCalc, OpenMode, Orphan, SlotDex,
VecDexDyn, VecDexL2, VerMap, VerMapWithProof, VsdbError, VsdbOptions,
vsdb_configure, vsdb_flush, vsdb_get_base_dir, vsdb_open_mode,
};
const HELPER_BASE: &str = "VSDB_STRATA_READ_ONLY_HELPER_BASE";
struct TaggedValue(u32);
impl vsdb::ValueEnDe for TaggedValue {
fn try_encode(&self) -> vsdb::Result<Vec<u8>> {
use std::sync::atomic::{AtomicU32, Ordering};
static TAG: AtomicU32 = AtomicU32::new(0);
Ok(postcard::to_allocvec(&(
self.0,
TAG.fetch_add(1, Ordering::Relaxed),
))?)
}
fn decode(bytes: &[u8]) -> vsdb::Result<Self> {
let (value, _tag): (u32, u32) = postcard::from_bytes(bytes)?;
Ok(Self(value))
}
}
fn helper_base() -> Option<PathBuf> {
env::var_os(HELPER_BASE).map(PathBuf::from)
}
fn run_helper(name: &str, base: &Path) {
let output = Command::new(env::current_exe().unwrap())
.args(["--ignored", "--exact", name, "--nocapture"])
.env(HELPER_BASE, base)
.output()
.unwrap();
assert!(
output.status.success(),
"helper {name} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn tree_contents(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
fn visit(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
let mut entries: Vec<_> = fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap())
.collect();
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let path = entry.path();
if path.is_dir() {
visit(root, &path, out);
} else {
out.insert(
path.strip_prefix(root).unwrap().to_path_buf(),
fs::read(path).unwrap(),
);
}
}
}
let mut out = BTreeMap::new();
visit(root, root, &mut out);
out
}
fn set_tree_mode(root: &Path, dir_mode: u32, file_mode: u32) {
fn visit(path: &Path, dir_mode: u32, file_mode: u32) {
if path.is_dir() {
for entry in fs::read_dir(path).unwrap() {
visit(&entry.unwrap().path(), dir_mode, file_mode);
}
fs::set_permissions(path, fs::Permissions::from_mode(dir_mode)).unwrap();
} else {
fs::set_permissions(path, fs::Permissions::from_mode(file_mode)).unwrap();
}
}
visit(root, dir_mode, file_mode);
}
#[test]
fn typed_and_versioned_reads_leave_store_unchanged() {
let base = env::temp_dir()
.join(format!("vsdb_strata_read_only_{}", rand::random::<u128>()));
run_helper("read_only_writer_helper", &base);
let before = tree_contents(&base);
set_tree_mode(&base, 0o555, 0o444);
run_helper("read_only_reader_helper", &base);
let after = tree_contents(&base);
set_tree_mode(&base, 0o755, 0o644);
assert_eq!(before, after);
fs::remove_dir_all(base).unwrap();
}
#[test]
#[ignore]
fn read_only_writer_helper() {
let Some(base) = helper_base() else {
return;
};
vsdb_configure(VsdbOptions::new(&base)).unwrap();
let mut map = Mapx::<u64, String>::new();
map.insert(&7, &"typed-value".to_owned());
let map_id = map.save_meta().unwrap();
let orphan_id = Orphan::new(TaggedValue(42)).save_meta().unwrap();
let mut versioned = VerMap::<u64, String>::new();
let main = versioned.main_branch();
versioned
.insert(main, &11, &"versioned-value".to_owned())
.unwrap();
versioned.commit(main).unwrap();
let versioned_id = versioned.save_meta().unwrap();
let expected_root = {
let mut proof = VerMapWithProof::<u64, String, MptCalc>::from_map(versioned);
proof.merkle_root(main).unwrap()
};
let cache_path = vsdb_get_base_dir()
.join("__SYSTEM__")
.join(format!("mpt_cache_{}.bin", versioned_id.map_id));
if cache_path.exists() {
fs::remove_file(cache_path).unwrap();
}
let state = postcard::to_allocvec(&(map_id, orphan_id, versioned_id, expected_root))
.unwrap();
fs::write(base.join("read-only-strata-state"), state).unwrap();
vsdb_flush();
}
#[test]
#[ignore]
fn read_only_reader_helper() {
let Some(base) = helper_base() else {
return;
};
vsdb_configure(VsdbOptions::read_only(&base)).unwrap();
assert_eq!(OpenMode::ReadOnly, vsdb_open_mode());
let state = fs::read(base.join("read-only-strata-state")).unwrap();
let (map_id, orphan_id, versioned_id, expected_root): (
InstanceId,
InstanceId,
InstanceId,
Vec<u8>,
) = postcard::from_bytes(&state).unwrap();
let map = Mapx::<u64, String>::from_meta(map_id).unwrap();
assert!(map.namespace().is_read_only());
assert_eq!(Some("typed-value".to_owned()), map.get(&7));
map.try_sync_wal().unwrap();
map.namespace().try_sync_wal().unwrap();
assert!(matches!(map.save_meta(), Err(VsdbError::ReadOnly { .. })));
let mut orphan = Orphan::<TaggedValue>::from_meta(orphan_id).unwrap();
orphan.try_sync_wal().unwrap();
{
let guard = orphan.get_mut();
assert_eq!(guard.0, 42);
}
let ns = map.namespace();
assert!(matches!(
SlotDex::<u64, u64>::new(64, false),
Err(VsdbError::ReadOnly { .. })
));
assert!(matches!(
SlotDex::<u64, u64>::new_in(&ns, 64, false),
Err(VsdbError::ReadOnly { .. })
));
let config = HnswConfig {
dim: 4,
..Default::default()
};
assert!(matches!(
VecDexL2::<u64>::new(config.clone()),
Err(VsdbError::ReadOnly { .. })
));
assert!(matches!(
VecDexL2::<u64>::new_in(&ns, config.clone()),
Err(VsdbError::ReadOnly { .. })
));
for metric in [MetricKind::L2, MetricKind::Cosine, MetricKind::InnerProduct] {
assert!(matches!(
VecDexDyn::<u64>::new(metric, config.clone()),
Err(VsdbError::ReadOnly { .. })
));
assert!(matches!(
VecDexDyn::<u64>::new_in(&ns, metric, config.clone()),
Err(VsdbError::ReadOnly { .. })
));
}
let mut standalone_trie = MptCalc::new();
assert!(matches!(
standalone_trie.save_cache(&base, 1, 1),
Err(VsdbError::ReadOnly { .. })
));
let mut versioned = VerMap::<u64, String>::from_meta(versioned_id).unwrap();
let main = versioned.branch_id("main").unwrap();
assert_eq!(
Some("versioned-value".to_owned()),
versioned.get(main, &11).unwrap()
);
assert!(matches!(
versioned.insert(main, &12, &"blocked".to_owned()),
Err(VsdbError::ReadOnly { .. })
));
let mut proof = VerMapWithProof::<u64, String, MptCalc>::from_map(versioned);
assert_eq!(expected_root, proof.merkle_root(main).unwrap());
let commit = proof.map().head_commit(main).unwrap().unwrap().id();
assert!(matches!(
proof.save_cache(commit),
Err(VsdbError::ReadOnly { .. })
));
}