#![cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use regolith::{
CompactionJobInfo, Db, Error, EventListener, FlushJobInfo, Options, WriteBatch, WriteOptions,
};
fn foreground_options() -> Options {
Options {
write_buffer_size: 32 * 1024,
block_size: 4 * 1024,
block_cache_size: 64 * 1024,
target_file_size: 64 * 1024,
level_base_bytes: 128 * 1024,
l0_compaction_trigger: 2,
level0_slowdown_writes_trigger: 4,
level0_stop_writes_trigger: 8,
max_background_compactions: 0,
..Options::default()
}
}
fn value(i: usize) -> Vec<u8> {
format!("{i:0>512}").into_bytes()
}
#[derive(Default)]
struct JobCounter {
flushes: AtomicUsize,
compactions: AtomicUsize,
}
impl EventListener for JobCounter {
fn on_flush_completed(&self, _info: &FlushJobInfo) {
self.flushes.fetch_add(1, Ordering::Relaxed);
}
fn on_compaction_completed(&self, _info: &CompactionJobInfo) {
self.compactions.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn open_succeeds_with_zero_background_compactions() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path(), foreground_options()).unwrap();
db.put(b"k", b"v").unwrap();
assert_eq!(db.get(b"k").unwrap().as_deref(), Some(&b"v"[..]));
}
#[test]
fn zero_workers_do_not_wedge_at_the_stop_trigger() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path(), foreground_options()).unwrap();
for i in 0..8_000usize {
db.put(format!("key{i:06}").as_bytes(), &value(i)).unwrap();
}
for i in (0..8_000usize).step_by(97) {
assert_eq!(
db.get(format!("key{i:06}").as_bytes()).unwrap(),
Some(value(i)),
"key {i} lost"
);
}
let l0: u64 = db.get_int_property("regolith.num-files-at-level0").unwrap();
assert!(
l0 < 8,
"L0 grew to {l0} files, at or past the stop trigger: inline compaction is not keeping up"
);
}
#[test]
fn inline_compaction_bounds_the_work_one_write_performs() {
let counter = Arc::new(JobCounter::default());
let dir = tempfile::tempdir().unwrap();
let db = Db::open(
dir.path(),
Options {
listeners: vec![counter.clone()],
..foreground_options()
},
)
.unwrap();
let mut worst = 0usize;
for i in 0..4_000usize {
let before = counter.compactions.load(Ordering::Relaxed);
db.put(format!("key{i:06}").as_bytes(), &value(i)).unwrap();
worst = worst.max(counter.compactions.load(Ordering::Relaxed) - before);
}
assert!(
worst <= 32,
"one write performed {worst} compaction jobs, past the inline cap"
);
assert!(
counter.compactions.load(Ordering::Relaxed) > 0,
"no compaction ran at all, so the bound proves nothing"
);
assert!(
counter.flushes.load(Ordering::Relaxed) > 0,
"no memtable was flushed, so the writes never reached L0"
);
}
#[test]
fn a_stalled_write_reports_busy_instead_of_blocking() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(
dir.path(),
Options {
level0_stop_writes_trigger: 2,
level0_slowdown_writes_trigger: 2,
l0_compaction_trigger: 64,
..foreground_options()
},
)
.unwrap();
let _snapshot = db.snapshot();
let deadline = Instant::now() + Duration::from_secs(60);
let mut outcome = Ok(());
for i in 0..4_000usize {
outcome = db.put(format!("key{i:06}").as_bytes(), &value(i));
if outcome.is_err() {
break;
}
assert!(
Instant::now() < deadline,
"writes neither progressed nor failed within 60s: the writer is wedged"
);
}
match outcome {
Err(Error::Busy(_)) => {}
Err(other) => panic!("expected Error::Busy, got {other:?}"),
Ok(()) => panic!("expected the stall to surface as Error::Busy"),
}
}
#[test]
fn no_slowdown_still_returns_busy_immediately() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(
dir.path(),
Options {
level0_slowdown_writes_trigger: 1,
level0_stop_writes_trigger: 2,
l0_compaction_trigger: 64,
..foreground_options()
},
)
.unwrap();
let opts = WriteOptions {
no_slowdown: true,
..WriteOptions::default()
};
let mut saw_busy = false;
for i in 0..4_000usize {
match db.put_opt(&opts, format!("key{i:06}").as_bytes(), &value(i)) {
Ok(()) => {}
Err(Error::Busy(_)) => {
saw_busy = true;
break;
}
Err(other) => panic!("expected Error::Busy, got {other:?}"),
}
}
assert!(saw_busy, "no_slowdown never reported a stall");
}
#[test]
fn a_full_memtable_becomes_an_l0_file_with_no_worker() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(
dir.path(),
Options {
l0_compaction_trigger: 64,
level0_slowdown_writes_trigger: 0,
level0_stop_writes_trigger: 0,
..foreground_options()
},
)
.unwrap();
assert_eq!(db.get_int_property("regolith.num-files-at-level0"), Some(0));
for i in 0..400usize {
db.put(format!("key{i:06}").as_bytes(), &value(i)).unwrap();
}
assert!(
db.get_int_property("regolith.num-files-at-level0").unwrap() > 0,
"the memtable filled but no L0 file was written"
);
}
#[test]
fn explicit_flush_writes_an_l0_file_and_is_idempotent() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(
dir.path(),
Options {
l0_compaction_trigger: 64,
..foreground_options()
},
)
.unwrap();
db.put(b"a", b"1").unwrap();
db.flush().unwrap();
let after_first = db.get_int_property("regolith.num-files-at-level0").unwrap();
assert_eq!(after_first, 1);
db.flush().unwrap();
assert_eq!(
db.get_int_property("regolith.num-files-at-level0").unwrap(),
after_first
);
assert_eq!(db.get(b"a").unwrap().as_deref(), Some(&b"1"[..]));
}
#[test]
fn compact_step_reports_whether_it_did_work() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path(), foreground_options()).unwrap();
assert!(
!db.compact_step().unwrap(),
"an empty database has nothing to compact"
);
for i in 0..600usize {
db.put(format!("key{i:06}").as_bytes(), &value(i)).unwrap();
}
db.flush().unwrap();
assert!(
db.compact_step().unwrap(),
"L0 is over the compaction trigger but compact_step found no work"
);
let mut steps = 0;
while db.compact_step().unwrap() {
steps += 1;
assert!(steps < 1_000, "compact_step never drained");
}
for i in (0..600usize).step_by(37) {
assert_eq!(
db.get(format!("key{i:06}").as_bytes()).unwrap(),
Some(value(i))
);
}
}
#[test]
fn compact_step_works_with_a_background_worker_running() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(
dir.path(),
Options {
max_background_compactions: 2,
..foreground_options()
},
)
.unwrap();
for i in 0..2_000usize {
db.put(format!("key{i:06}").as_bytes(), &value(i)).unwrap();
if i % 250 == 0 {
db.compact_step().unwrap();
}
}
for i in (0..2_000usize).step_by(53) {
assert_eq!(
db.get(format!("key{i:06}").as_bytes()).unwrap(),
Some(value(i))
);
}
}
#[test]
fn full_lifecycle_with_zero_workers() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path(), foreground_options()).unwrap();
for i in 0..2_000usize {
db.put(format!("key{i:06}").as_bytes(), &value(i)).unwrap();
}
let snapshot = db.snapshot();
db.delete(b"key000005").unwrap();
assert_eq!(db.get(b"key000005").unwrap(), None);
assert_eq!(
snapshot.get(b"key000005").unwrap(),
Some(value(5)),
"the snapshot lost its point-in-time view"
);
let mut batch = WriteBatch::new();
batch.put(b"batched", b"yes");
batch.delete(b"key000006");
db.write(batch).unwrap();
assert_eq!(db.get(b"batched").unwrap().as_deref(), Some(&b"yes"[..]));
assert_eq!(db.get(b"key000006").unwrap(), None);
let scanned = db.scan(Some(b"key000100"), Some(b"key000110")).unwrap();
assert_eq!(scanned.len(), 10);
assert_eq!(scanned[0].0, b"key000100".to_vec());
let mut iter = db.iter();
iter.seek(b"key001000");
let mut walked = 0;
while iter.valid() && walked < 50 {
assert!(iter.key().unwrap() >= b"key001000".as_slice());
iter.next();
walked += 1;
}
assert_eq!(walked, 50);
drop(iter);
drop(snapshot);
db.flush().unwrap();
db.compact_range(None, None).unwrap();
while db.compact_step().unwrap() {}
db.close().unwrap();
drop(db);
let db = Db::open(dir.path(), foreground_options()).unwrap();
assert_eq!(db.get(b"key000000").unwrap(), Some(value(0)));
assert_eq!(db.get(b"key001999").unwrap(), Some(value(1999)));
assert_eq!(db.get(b"key000005").unwrap(), None);
assert_eq!(db.get(b"key000006").unwrap(), None);
assert_eq!(db.get(b"batched").unwrap().as_deref(), Some(&b"yes"[..]));
}