use powdb_query::executor::Engine;
use powdb_query::result::QueryResult;
use powdb_storage::types::Value;
use std::sync::{Arc, Barrier, RwLock};
fn temp_dir(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"powdb_group_commit_{name}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
fn exec(engine: &mut Engine, query: &str) -> QueryResult {
engine
.execute_powql(query)
.unwrap_or_else(|e| panic!("failed to execute `{query}`: {e}"))
}
fn count(engine: &mut Engine, query: &str) -> i64 {
match exec(engine, query) {
QueryResult::Scalar(Value::Int(n)) => n,
QueryResult::Rows { rows, .. } if rows.len() == 1 && rows[0].len() == 1 => {
match &rows[0][0] {
Value::Int(n) => *n,
other => panic!("count returned non-int {other:?}"),
}
}
other => panic!("expected scalar count, got {other:?}"),
}
}
#[test]
fn test_sequential_committer_fsyncs_once_per_commit() {
let dir = temp_dir("sequential");
std::fs::create_dir_all(&dir).unwrap();
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type S { required id: int, v: int }");
let base = engine.wal_fsync_count();
for i in 0..20i64 {
exec(&mut engine, &format!("insert S {{ id := {i}, v := {i} }}"));
}
assert_eq!(
engine.wal_fsync_count() - base,
20,
"a lone sequential committer must fsync exactly once per commit"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_coalesced_batch_single_fsync_and_crash_recovery() {
let dir = temp_dir("coalesced_batch");
std::fs::create_dir_all(&dir).unwrap();
{
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type B { required id: int, v: int }");
let mut tickets = Vec::new();
for i in 0..10i64 {
let (res, ticket) = engine.run_with_deferred_durability(|e| {
e.execute_powql(&format!("insert B {{ id := {i}, v := {i} }}"))
});
res.unwrap();
tickets.push(ticket.expect("Full mode must hand out a durability ticket"));
}
let base = engine.wal_fsync_count();
for ticket in tickets {
ticket.wait().unwrap();
}
assert_eq!(
engine.wal_fsync_count() - base,
1,
"ten queued commits must be covered by a single fsync"
);
assert_eq!(count(&mut engine, "count(B)"), 10);
std::mem::forget(engine); }
{
let mut engine = Engine::new(&dir).unwrap();
assert_eq!(
count(&mut engine, "count(B)"),
10,
"every commit acknowledged after the coalesced fsync must survive a hard crash"
);
for i in 0..10i64 {
assert_eq!(
count(&mut engine, &format!("count(B filter .id = {i})")),
1,
"row {i} missing after replay of a coalesced batch"
);
}
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_concurrent_committers_coalesce_and_survive_crash() {
const THREADS: usize = 8;
const PER_THREAD: i64 = 20;
const TOTAL: i64 = THREADS as i64 * PER_THREAD;
let dir = temp_dir("concurrent");
std::fs::create_dir_all(&dir).unwrap();
{
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type C { required id: int, v: int }");
let base = engine.wal_fsync_count();
let engine = Arc::new(RwLock::new(engine));
let barrier = Arc::new(Barrier::new(THREADS));
let mut handles = Vec::new();
for t in 0..THREADS {
let engine = Arc::clone(&engine);
let barrier = Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
barrier.wait();
for i in 0..PER_THREAD {
let id = t as i64 * PER_THREAD + i;
let (res, ticket) = {
let mut eng = engine.write().unwrap();
eng.run_with_deferred_durability(|e| {
e.execute_powql(&format!("insert C {{ id := {id}, v := {id} }}"))
})
};
res.unwrap();
ticket
.expect("Full mode must hand out a durability ticket")
.wait()
.unwrap();
}
}));
}
for h in handles {
h.join().unwrap();
}
let mut engine = Arc::try_unwrap(engine)
.unwrap_or_else(|_| panic!("engine still shared after join"))
.into_inner()
.unwrap();
let fsyncs = engine.wal_fsync_count() - base;
assert_eq!(count(&mut engine, "count(C)"), TOTAL);
assert!(
fsyncs < TOTAL as u64,
"{TOTAL} overlapping commits must share fsyncs (got {fsyncs})"
);
std::mem::forget(engine); }
{
let mut engine = Engine::new(&dir).unwrap();
assert_eq!(
count(&mut engine, "count(C)"),
TOTAL,
"all acknowledged concurrent commits must survive a hard crash"
);
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_deferral_scope_is_tight() {
let dir = temp_dir("scope");
std::fs::create_dir_all(&dir).unwrap();
let mut engine = Engine::new(&dir).unwrap();
exec(&mut engine, "type T { required id: int }");
let (res, ticket) =
engine.run_with_deferred_durability(|e| e.execute_powql("insert T { id := 1 }"));
res.unwrap();
let ticket = ticket.expect("deferred Full-mode commit must produce a ticket");
let base = engine.wal_fsync_count();
exec(&mut engine, "insert T { id := 2 }");
assert!(
engine.wal_fsync_count() > base,
"inline statement must fsync before returning"
);
ticket.wait().unwrap();
let (res, ticket) = engine.run_with_deferred_durability(|e| e.execute_powql("count(T)"));
res.unwrap();
assert!(
ticket.is_none(),
"reads must not register durability claims"
);
std::fs::remove_dir_all(&dir).ok();
}