#![cfg(all(
feature = "armour",
feature = "typed-tree",
feature = "rapira-codec",
feature = "var-collections"
))]
use std::path::Path;
use armdb::armour::Db;
use armdb::{Config, ConstTree};
fn open_tree(dir: &Path, name: &str) -> ConstTree<[u8; 8], 4> {
let mut cfg = Config::test();
cfg.shard_count = 4;
ConstTree::<[u8; 8], 4>::open(dir.join(name), cfg).expect("open const tree")
}
#[test]
fn atomic2_writes_both_collections() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_tree(dir.path(), "a");
let b = open_tree(dir.path(), "b");
let ka = [1u8; 8];
let kb = [2u8; 8];
db.atomic2(&a, &[ka], &b, &[kb], |ta, tb| {
ta.put(&ka, &[11u8; 4])?;
tb.put(&kb, &[22u8; 4])?;
Ok(())
})
.unwrap();
assert_eq!(a.get(&ka), Some([11u8; 4]));
assert_eq!(b.get(&kb), Some([22u8; 4]));
}
#[test]
fn atomic2_rejects_same_collection_twice() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_tree(dir.path(), "a");
let ka = [1u8; 8];
let err = db.atomic2(&a, &[ka], &a, &[ka], |_, _| Ok(()));
assert!(matches!(err, Err(armdb::DbError::DuplicateCollectionInTx)));
}
#[test]
fn atomic3_and_atomic4_write_all() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_tree(dir.path(), "a");
let b = open_tree(dir.path(), "b");
let c = open_tree(dir.path(), "c");
let d = open_tree(dir.path(), "d");
let (ka, kb, kc, kd) = ([1u8; 8], [2u8; 8], [3u8; 8], [4u8; 8]);
db.atomic3(&a, &[ka], &b, &[kb], &c, &[kc], |ta, tb, tc| {
ta.put(&ka, &[1u8; 4])?;
tb.put(&kb, &[2u8; 4])?;
tc.put(&kc, &[3u8; 4])?;
Ok(())
})
.unwrap();
db.atomic4(
&a,
&[ka],
&b,
&[kb],
&c,
&[kc],
&d,
&[kd],
|ta, tb, tc, td| {
ta.delete(&ka)?;
tb.put(&kb, &[9u8; 4])?;
tc.put(&kc, &[9u8; 4])?;
td.put(&kd, &[4u8; 4])?;
Ok(())
},
)
.unwrap();
assert_eq!(a.get(&ka), None);
assert_eq!(b.get(&kb), Some([9u8; 4]));
assert_eq!(d.get(&kd), Some([4u8; 4]));
}
use armdb::ConstMap;
fn open_map(dir: &Path, name: &str) -> ConstMap<[u8; 8], 4> {
let mut cfg = Config::test();
cfg.shard_count = 4;
ConstMap::<[u8; 8], 4>::open(dir.join(name), cfg).expect("open const map")
}
#[test]
fn atomic2_tree_plus_map_heterogeneous() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let t = open_tree(dir.path(), "t");
let m = open_map(dir.path(), "m");
let kt = [1u8; 8];
let km = [2u8; 8];
db.atomic2(&t, &[kt], &m, &[km], |tt, tm| {
tt.put(&kt, &[7u8; 4])?;
tm.put(&km, &[8u8; 4])?;
Ok(())
})
.unwrap();
assert_eq!(t.get(&kt), Some([7u8; 4]));
assert_eq!(m.get(&km), Some([8u8; 4]));
}
use armdb::ZeroTree;
fn open_zero(dir: &Path, name: &str) -> ZeroTree<[u8; 8], 4, u32> {
let mut cfg = Config::test();
cfg.shard_count = 4;
ZeroTree::<[u8; 8], 4, u32>::open(dir.join(name), cfg).expect("open zero tree")
}
#[test]
fn atomic2_zero_trees_typed_values() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let z1 = open_zero(dir.path(), "z1");
let z2 = open_zero(dir.path(), "z2");
let k1 = [1u8; 8];
let k2 = [2u8; 8];
db.atomic2(&z1, &[k1], &z2, &[k2], |t1, t2| {
t1.put(&k1, &111u32)?;
t2.put(&k2, &222u32)?;
assert_eq!(t1.try_get(&k1)?, Some(111u32));
Ok(())
})
.unwrap();
assert_eq!(z1.get(&k1), Some(111u32));
assert_eq!(z2.get(&k2), Some(222u32));
}
use armdb::{CollectionMeta, NoHook, RapiraCodec};
use armour_core::GetType;
use rapira::Rapira;
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct TItemA {
value: u64,
}
impl CollectionMeta for TItemA {
type SelfId = [u8; 8];
const NAME: &'static str = "typed_a";
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct TItemB {
value: u64,
}
impl CollectionMeta for TItemB {
type SelfId = [u8; 8];
const NAME: &'static str = "typed_b";
}
#[test]
fn atomic2_typed_trees() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = db
.open_typed_tree::<TItemA, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let b = db
.open_typed_tree::<TItemB, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let ka = [1u8; 8];
let kb = [2u8; 8];
db.atomic2(a.as_ref(), &[ka], b.as_ref(), &[kb], |ta, tb| {
ta.put(&ka, TItemA { value: 10 })?;
tb.put(&kb, TItemB { value: 20 })?;
assert_eq!(ta.try_get(&ka)?, Some(&TItemA { value: 10 }));
Ok(())
})
.unwrap();
assert_eq!(a.get(&ka).unwrap().value, 10);
assert_eq!(b.get(&kb).unwrap().value, 20);
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct TMapItem {
value: u64,
}
impl CollectionMeta for TMapItem {
type SelfId = [u8; 8];
const NAME: &'static str = "typed_map_item";
}
#[test]
fn atomic2_typed_map_plus_tree() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let m = db
.open_typed_map::<TMapItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let t = db
.open_typed_tree::<TItemA, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let km = [3u8; 8];
let kt = [4u8; 8];
db.atomic2(m.as_ref(), &[km], t.as_ref(), &[kt], |tm, tt| {
tm.put(&km, TMapItem { value: 30 })?;
tt.put(&kt, TItemA { value: 40 })?;
assert_eq!(tm.try_get(&km)?, Some(&TMapItem { value: 30 }));
Ok(())
})
.unwrap();
assert_eq!(m.get(&km).unwrap().value, 30);
assert_eq!(t.get(&kt).unwrap().value, 40);
}
use armdb::VarTree;
fn open_var(dir: &Path, name: &str) -> VarTree<[u8; 8]> {
let mut cfg = Config::test();
cfg.shard_count = 4;
VarTree::<[u8; 8]>::open(dir.join(name), cfg).expect("open var tree")
}
#[test]
fn atomic2_var_trees() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_var(dir.path(), "va");
let b = open_var(dir.path(), "vb");
let ka = [1u8; 8];
let kb = [2u8; 8];
db.atomic2(&a, &[ka], &b, &[kb], |ta, tb| {
ta.put(&ka, b"hello")?;
tb.put(&kb, b"world")?;
assert_eq!(ta.try_get(&ka)?.as_deref(), Some(&b"hello"[..]));
Ok(())
})
.unwrap();
assert_eq!(a.get(&ka).as_deref(), Some(&b"hello"[..]));
assert_eq!(b.get(&kb).as_deref(), Some(&b"world"[..]));
}
use armdb::VarMap;
fn open_var_map(dir: &Path, name: &str) -> VarMap<[u8; 8]> {
let mut cfg = Config::test();
cfg.shard_count = 4;
VarMap::<[u8; 8]>::open(dir.join(name), cfg).expect("open var map")
}
#[test]
fn atomic2_var_map_plus_var_tree() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let m = open_var_map(dir.path(), "vmap");
let t = open_var(dir.path(), "vtree2");
let km = [5u8; 8];
let kt = [6u8; 8];
db.atomic2(&m, &[km], &t, &[kt], |tm, tt| {
tm.put(&km, b"mapval")?;
tt.put(&kt, b"treeval")?;
assert_eq!(tm.try_get(&km)?.as_deref(), Some(&b"mapval"[..]));
Ok(())
})
.unwrap();
assert_eq!(m.get(&km).as_deref(), Some(&b"mapval"[..]));
assert_eq!(t.get(&kt).as_deref(), Some(&b"treeval"[..]));
}
use armdb::VarTypedTree;
#[derive(Clone, Debug, PartialEq, Rapira)]
struct VTItem {
label: String,
n: u64,
}
fn open_var_typed(dir: &Path, name: &str) -> VarTypedTree<[u8; 8], VTItem, RapiraCodec> {
let mut cfg = Config::test();
cfg.shard_count = 4;
VarTypedTree::<[u8; 8], VTItem, RapiraCodec>::open(dir.join(name), cfg, RapiraCodec)
.expect("open var typed tree")
}
#[test]
fn atomic2_var_typed_trees() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_var_typed(dir.path(), "vt_a");
let b = open_var_typed(dir.path(), "vt_b");
let ka = [1u8; 8];
let kb = [2u8; 8];
db.atomic2(&a, &[ka], &b, &[kb], |ta, tb| {
ta.put(
&ka,
&VTItem {
label: "x".into(),
n: 1,
},
)?;
tb.put(
&kb,
&VTItem {
label: "y".into(),
n: 2,
},
)?;
assert_eq!(
ta.try_get(&ka)?,
Some(VTItem {
label: "x".into(),
n: 1
})
);
Ok(())
})
.unwrap();
assert_eq!(
a.get(&ka),
Some(VTItem {
label: "x".into(),
n: 1
})
);
assert_eq!(
b.get(&kb),
Some(VTItem {
label: "y".into(),
n: 2
})
);
}
#[test]
fn out_of_scope_key_errors_on_all_accessors() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_tree(dir.path(), "a");
let b = open_tree(dir.path(), "b");
let ka = [1u8; 8];
let kb = [2u8; 8];
let mut other = [9u8; 8];
for i in 0..=255u8 {
other[0] = i;
if a.shard_for(&other) != a.shard_for(&ka) {
break;
}
}
db.atomic2(&a, &[ka], &b, &[kb], |ta, _tb| {
if a.shard_for(&other) != a.shard_for(&ka) {
assert!(matches!(
ta.try_get(&other),
Err(armdb::DbError::ShardMismatch)
));
assert!(matches!(
ta.try_contains(&other),
Err(armdb::DbError::ShardMismatch)
));
assert!(matches!(
ta.put(&other, &[0u8; 4]),
Err(armdb::DbError::ShardMismatch)
));
assert_eq!(ta.try_get(&ka).unwrap(), None);
assert!(!ta.try_contains(&ka).unwrap());
}
Ok(())
})
.unwrap();
}
#[test]
fn err_from_closure_keeps_writes_and_replays_hooks() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let a = open_tree(dir.path(), "a");
let b = open_tree(dir.path(), "b");
let ka = [1u8; 8];
let kb = [2u8; 8];
let r: Result<(), _> = db.atomic2(&a, &[ka], &b, &[kb], |ta, _tb| {
ta.put(&ka, &[5u8; 4])?;
Err(armdb::DbError::TxConflict)
});
assert!(matches!(r, Err(armdb::DbError::TxConflict)));
assert_eq!(a.get(&ka), Some([5u8; 4])); }
#[test]
fn atomic2_mixed_backend_bitcask_tree_plus_fixed_map() {
use armdb::{FixedConfig, FixedMap};
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let t = open_tree(dir.path(), "bt"); let mut fcfg = FixedConfig::test();
fcfg.shard_count = 4;
let m: FixedMap<[u8; 8], 4> =
FixedMap::<[u8; 8], 4>::open(dir.path().join("fm"), fcfg).expect("open fixed map");
let kt = [1u8; 8];
let km = [2u8; 8];
db.atomic2(&t, &[kt], &m, &[km], |tt, tm| {
tt.put(&kt, &[3u8; 4])?;
tm.put(&km, &[4u8; 4])?;
Ok(())
})
.unwrap();
assert_eq!(t.get(&kt), Some([3u8; 4]));
assert_eq!(m.get(&km), Some([4u8; 4]));
}
#[test]
fn stress_atomic2_preserves_balance_sum() {
use std::sync::Arc;
use std::thread;
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let acc_a = Arc::new(open_tree(dir.path(), "acc_a")); let acc_b = Arc::new(open_tree(dir.path(), "acc_b"));
let k = [1u8; 8];
acc_a.put(&k, &100u32.to_le_bytes()).unwrap();
acc_b.put(&k, &100u32.to_le_bytes()).unwrap();
let mut handles = Vec::new();
for t in 0..8 {
let (db, acc_a, acc_b) = (db.clone(), acc_a.clone(), acc_b.clone());
handles.push(thread::spawn(move || {
for _ in 0..200 {
let _ = db.atomic2(acc_a.as_ref(), &[k], acc_b.as_ref(), &[k], |ta, tb| {
let va = u32::from_le_bytes(ta.get_or_err(&k)?);
let vb = u32::from_le_bytes(tb.get_or_err(&k)?);
if t % 2 == 0 {
if va > 0 {
ta.put(&k, &(va - 1).to_le_bytes())?;
tb.put(&k, &(vb + 1).to_le_bytes())?;
}
} else if vb > 0 {
tb.put(&k, &(vb - 1).to_le_bytes())?;
ta.put(&k, &(va + 1).to_le_bytes())?;
}
Ok(())
});
}
}));
}
for h in handles {
h.join().unwrap();
}
let s1 = u32::from_le_bytes(acc_a.get(&k).unwrap());
let s2 = u32::from_le_bytes(acc_b.get(&k).unwrap());
assert_eq!(s1 + s2, 200); }
#[test]
fn reader_sees_no_torn_values_during_atomic2() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let a = Arc::new(open_tree(dir.path(), "ra"));
let b = Arc::new(open_tree(dir.path(), "rb"));
let k = [1u8; 8];
a.put(&k, &[0u8; 4]).unwrap();
b.put(&k, &[0u8; 4]).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let writer = {
let (db, a, b, stop) = (db.clone(), a.clone(), b.clone(), stop.clone());
thread::spawn(move || {
for i in 0u32..2000 {
let v = (i % 250) as u8;
db.atomic2(a.as_ref(), &[k], b.as_ref(), &[k], |ta, tb| {
ta.put(&k, &[v; 4])?;
tb.put(&k, &[v; 4])?;
Ok(())
})
.unwrap();
}
stop.store(true, Ordering::Relaxed);
})
};
let reader = {
let (a, b, stop) = (a.clone(), b.clone(), stop.clone());
thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
if let Some(va) = a.get(&k) {
assert!(va.iter().all(|&x| x == va[0]));
}
if let Some(vb) = b.get(&k) {
assert!(vb.iter().all(|&x| x == vb[0]));
}
}
})
};
writer.join().unwrap();
reader.join().unwrap();
}