use mongreldb_core::{schema::*, Database, MongrelError, Value};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use tempfile::tempdir;
fn pk_schema(name: &str) -> Schema {
Schema {
schema_id: 1,
columns: vec![ColumnDef {
id: 1,
name: name.into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
default_value: None,
}],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
#[test]
fn concurrent_disjoint_writers_all_commit() {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("a", pk_schema("v")).unwrap();
db.create_table("b", pk_schema("v")).unwrap();
let n = 8;
let per = 50;
let mut handles = Vec::new();
for i in 0..n {
let db = Arc::clone(&db);
handles.push(thread::spawn(move || {
for j in 0..per {
let pk = (i * per + j) as i64;
let table = if i % 2 == 0 { "a" } else { "b" };
db.transaction(|t| {
t.put(table, vec![(1, Value::Int64(pk))])?;
Ok(())
})
.unwrap();
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(db.table("a").unwrap().lock().count(), (n / 2 * per) as u64);
assert_eq!(db.table("b").unwrap().lock().count(), (n / 2 * per) as u64);
}
#[test]
fn same_pk_concurrent_insert_conflicts_exactly_one_wins() {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("t", pk_schema("v")).unwrap();
let begun = Arc::new(std::sync::Barrier::new(2));
let commit = Arc::new(std::sync::Barrier::new(2));
let mut handles = Vec::new();
for _ in 0..2 {
let db = Arc::clone(&db);
let begun = Arc::clone(&begun);
let commit = Arc::clone(&commit);
handles.push(thread::spawn(move || {
let mut tx = db.begin();
tx.put("t", vec![(1, Value::Int64(42))]).unwrap();
begun.wait(); commit.wait(); tx.commit()
}));
}
let mut ok = 0;
let mut conflicts = 0;
for h in handles {
match h.join().unwrap() {
Ok(_) => ok += 1,
Err(MongrelError::Conflict(_)) => conflicts += 1,
Err(e) => panic!("unexpected error: {e:?}"),
}
}
assert_eq!(ok, 1, "exactly one insert must succeed");
assert_eq!(conflicts, 1, "exactly one must conflict");
assert_eq!(db.table("t").unwrap().lock().count(), 1);
db.transaction(|t| {
t.put("t", vec![(1, Value::Int64(999))])?;
Ok(())
})
.unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), 2);
}
#[test]
fn aborted_txn_consumes_no_epoch_and_visible_does_not_stall() {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("t", pk_schema("v")).unwrap();
db.transaction(|t| {
t.put("t", vec![(1, Value::Int64(1))])?;
Ok(())
})
.unwrap();
let after_first = db.visible_epoch().0;
let mut tx1 = db.begin();
let mut tx2 = db.begin();
tx1.put("t", vec![(1, Value::Int64(2))]).unwrap();
tx2.put("t", vec![(1, Value::Int64(2))]).unwrap();
tx1.commit().unwrap();
let result = tx2.commit();
assert!(matches!(result, Err(MongrelError::Conflict(_))));
let vis = db.visible_epoch().0;
assert!(
vis > after_first,
"visible must not stall after a conflict abort"
);
db.transaction(|t| {
t.put("t", vec![(1, Value::Int64(3))])?;
Ok(())
})
.unwrap();
let vis2 = db.visible_epoch().0;
assert!(vis2 > vis);
}
#[test]
fn flush_under_concurrent_writes_loses_no_rows() {
use std::time::Duration;
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("t", pk_schema("v")).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let total = Arc::new(AtomicU64::new(0));
let db_w = Arc::clone(&db);
let stop_w = Arc::clone(&stop);
let total_w = Arc::clone(&total);
let writer = thread::spawn(move || {
let mut i: i64 = 0;
while !stop_w.load(Ordering::Relaxed) {
db_w.transaction(|t| {
t.put("t", vec![(1, Value::Int64(i))])?;
Ok(())
})
.unwrap();
i += 1;
total_w.fetch_add(1, Ordering::Relaxed);
}
});
for _ in 0..3 {
thread::sleep(Duration::from_millis(20));
let _ = db.table("t").unwrap().lock().flush();
}
stop.store(true, Ordering::Relaxed);
writer.join().unwrap();
let expected = total.load(Ordering::Relaxed);
let actual = db.table("t").unwrap().lock().count();
assert_eq!(actual, expected, "rows lost during concurrent flush");
}
#[test]
fn group_commit_batches_fsyncs_under_concurrency() {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("t", pk_schema("v")).unwrap();
let threads = 16u64;
let per = 20u64;
let total = threads * per;
let start = db.__wal_group_sync_count();
let barrier = Arc::new(std::sync::Barrier::new(threads as usize));
let mut handles = Vec::new();
for ti in 0..threads {
let db = Arc::clone(&db);
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
b.wait();
for j in 0..per {
let pk = (ti * per + j) as i64;
db.transaction(|t| {
t.put("t", vec![(1, Value::Int64(pk))])?;
Ok(())
})
.unwrap();
}
}));
}
for h in handles {
h.join().unwrap();
}
let fsyncs = db.__wal_group_sync_count() - start;
assert_eq!(
db.table("t").unwrap().lock().count(),
total,
"all committed rows must be durable"
);
assert!(
fsyncs < total,
"group commit must batch: {fsyncs} fsyncs for {total} commits"
);
}
#[test]
fn single_table_commit_is_not_visible_before_its_commit_epoch() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("a", pk_schema("v")).unwrap();
db.create_table("b", pk_schema("v")).unwrap();
{
let h = db.table("a").unwrap();
let mut g = h.lock();
g.put(vec![(1, Value::Int64(100))]).unwrap();
}
for i in 0..5 {
let h = db.table("b").unwrap();
let mut g = h.lock();
g.put(vec![(1, Value::Int64(i))]).unwrap();
g.commit().unwrap();
}
assert_eq!(
db.table("a").unwrap().lock().count(),
0,
"uncommitted row must not be visible before its commit epoch"
);
db.table("a").unwrap().lock().commit().unwrap();
assert_eq!(db.table("a").unwrap().lock().count(), 1);
}
#[test]
fn poisoned_db_fails_data_and_ddl_writes_fast() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema("v")).unwrap();
let before = db.__wal_group_sync_count();
db.__poison();
let data = db.transaction(|t| {
t.put("t", vec![(1, Value::Int64(1))])?;
Ok(())
});
assert!(matches!(data, Err(MongrelError::Other(_))), "data commit");
let create = db.create_table("t2", pk_schema("v"));
assert!(
matches!(create, Err(MongrelError::Other(_))),
"create_table"
);
let drop = db.drop_table("t");
assert!(matches!(drop, Err(MongrelError::Other(_))), "drop_table");
assert_eq!(
db.__wal_group_sync_count(),
before,
"a poisoned DB must issue no further fsyncs"
);
}