use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use regolith::{Db, Options};
use tempfile::TempDir;
const KEYS: usize = 6_000;
fn value_for(i: usize) -> Vec<u8> {
let mut v = format!("value-{i:08}-").into_bytes();
v.resize(160, (i % 251) as u8);
v
}
fn key_for(i: usize) -> Vec<u8> {
format!("key{i:08}").into_bytes()
}
fn base(partitioned: bool) -> Options {
Options {
write_buffer_size: 64 * 1024,
block_size: 512,
metadata_block_size: 512,
target_file_size: 128 * 1024,
partitioned_index: partitioned,
block_cache_num_shard_bits: 0,
..Options::default()
}
}
fn seeded(partitioned: bool) -> TempDir {
let dir = TempDir::new().unwrap();
let db = Db::open(dir.path(), base(partitioned)).unwrap();
for i in 0..KEYS {
db.put(&key_for(i), &value_for(i)).unwrap();
}
db.compact_range(None, None).unwrap();
db.close().unwrap();
dir
}
fn verify_everything(db: &Db, label: &str) {
for i in 0..KEYS {
assert_eq!(
db.get(&key_for(i)).unwrap().as_ref(),
Some(&value_for(i)),
"{label}: key {i} read back wrong"
);
assert!(db.has(&key_for(i)).unwrap(), "{label}: has() lost key {i}");
assert_eq!(
db.get_size(&key_for(i)).unwrap(),
Some(value_for(i).len()),
"{label}: get_size() wrong for key {i}"
);
}
for i in 0..500 {
let miss = format!("absent{i:08}").into_bytes();
assert_eq!(db.get(&miss).unwrap(), None, "{label}: invented a miss");
assert!(!db.has(&miss).unwrap(), "{label}: has() invented a miss");
}
let scanned = db.scan(None, None).unwrap();
assert_eq!(scanned.len(), KEYS, "{label}: scan lost entries");
for (i, (k, v)) in scanned.iter().enumerate() {
assert_eq!(k, &key_for(i), "{label}: scan out of order at {i}");
assert_eq!(v, &value_for(i), "{label}: scan value wrong at {i}");
}
}
fn assert_cache_is_far_too_small(dir: &TempDir, partitioned: bool, cache_bytes: usize) {
let pinned = Db::open(
dir.path(),
Options {
block_cache_size: 32 * 1024 * 1024,
cache_index_and_filter_blocks: false,
..base(partitioned)
},
)
.unwrap();
let metadata = pinned
.get_int_property("regolith.pinned-metadata-bytes")
.expect("regolith.pinned-metadata-bytes is a known property");
pinned.close().unwrap();
assert!(
metadata > (cache_bytes as u64) * 4,
"partitioned={partitioned}: metadata is {metadata} B against a {cache_bytes} B cache, \
which is not enough pressure to force eviction"
);
}
#[test]
fn a_flat_index_stays_correct_when_the_cache_cannot_hold_it() {
let dir = seeded(false);
assert_cache_is_far_too_small(&dir, false, 8 * 1024);
let opts = Options {
block_cache_size: 8 * 1024,
cache_index_and_filter_blocks: true,
..base(false)
};
let db = Db::open(dir.path(), opts).unwrap();
verify_everything(&db, "flat index, 8 KiB cache");
}
#[test]
fn a_partitioned_index_stays_correct_when_the_cache_cannot_hold_it() {
let dir = seeded(true);
assert_cache_is_far_too_small(&dir, true, 4 * 1024);
let opts = Options {
block_cache_size: 4 * 1024,
cache_index_and_filter_blocks: true,
..base(true)
};
let db = Db::open(dir.path(), opts).unwrap();
verify_everything(&db, "partitioned index, 4 KiB cache");
}
#[test]
fn a_strict_capacity_cache_that_refuses_the_index_still_reads_correctly() {
let dir = seeded(false);
let opts = Options {
block_cache_size: 4 * 1024,
strict_capacity_limit: true,
cache_index_and_filter_blocks: true,
..base(false)
};
let db = Db::open(dir.path(), opts).unwrap();
verify_everything(&db, "strict capacity, 4 KiB cache");
}
#[test]
fn evicted_metadata_answers_identically_to_pinned_metadata() {
for partitioned in [false, true] {
let dir = seeded(partitioned);
let pinned = Db::open(
dir.path(),
Options {
block_cache_size: 32 * 1024 * 1024,
cache_index_and_filter_blocks: false,
..base(partitioned)
},
)
.unwrap();
let pinned_scan = pinned.scan(None, None).unwrap();
let pinned_reads: Vec<Option<Vec<u8>>> = (0..KEYS)
.map(|i| pinned.get(&key_for(i)).unwrap())
.collect();
pinned.close().unwrap();
drop(pinned);
let evicting = Db::open(
dir.path(),
Options {
block_cache_size: 8 * 1024,
cache_index_and_filter_blocks: true,
..base(partitioned)
},
)
.unwrap();
let evicting_scan = evicting.scan(None, None).unwrap();
let evicting_reads: Vec<Option<Vec<u8>>> = (0..KEYS)
.map(|i| evicting.get(&key_for(i)).unwrap())
.collect();
assert_eq!(
pinned_scan, evicting_scan,
"partitioned={partitioned}: scans differ between pinned and evicting metadata"
);
assert_eq!(
pinned_reads, evicting_reads,
"partitioned={partitioned}: point reads differ between pinned and evicting metadata"
);
}
}
#[test]
fn concurrent_readers_survive_metadata_eviction() {
let dir = seeded(true);
let db = Arc::new(
Db::open(
dir.path(),
Options {
block_cache_size: 8 * 1024,
cache_index_and_filter_blocks: true,
..base(true)
},
)
.unwrap(),
);
let wrong = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for t in 0..8 {
let db = Arc::clone(&db);
let wrong = Arc::clone(&wrong);
handles.push(thread::spawn(move || {
for step in 0..2_000usize {
let i = (step * 7 + t * 613) % KEYS;
match db.get(&key_for(i)).unwrap() {
Some(v) if v == value_for(i) => {}
_ => {
wrong.fetch_add(1, Ordering::Relaxed);
}
}
if db.get_slice(&key_for(i)).unwrap().map(|s| s.to_vec()) != Some(value_for(i)) {
wrong.fetch_add(1, Ordering::Relaxed);
}
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(
wrong.load(Ordering::Relaxed),
0,
"metadata eviction produced wrong reads under concurrency"
);
}
#[test]
fn iteration_stays_correct_when_metadata_is_evicted() {
let dir = seeded(true);
let db = Db::open(
dir.path(),
Options {
block_cache_size: 8 * 1024,
cache_index_and_filter_blocks: true,
..base(true)
},
)
.unwrap();
let mut it = db.iter();
it.seek_to_last();
let mut back = Vec::new();
while it.valid() {
back.push((it.key().unwrap().to_vec(), it.value().unwrap().to_vec()));
it.prev();
}
it.status().unwrap();
back.reverse();
assert_eq!(back.len(), KEYS, "reverse iteration lost entries");
for (i, (k, v)) in back.iter().enumerate() {
assert_eq!(k, &key_for(i), "reverse iteration out of order at {i}");
assert_eq!(v, &value_for(i), "reverse iteration value wrong at {i}");
}
for i in (0..KEYS).step_by(211) {
let mut it = db.iter();
it.seek(&key_for(i));
assert!(it.valid(), "seek to key {i} landed nowhere");
assert_eq!(it.key().unwrap(), key_for(i).as_slice());
assert_eq!(it.value().unwrap(), value_for(i).as_slice());
it.seek_for_prev(&key_for(i));
assert!(it.valid());
assert_eq!(it.key().unwrap(), key_for(i).as_slice());
}
}