use mongreldb_core::{schema::*, Database, MongrelError, RowId, Value};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tempfile::tempdir;
static SERIAL: Mutex<()> = Mutex::new(());
fn env_usize(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(default)
}
fn pk_schema() -> Schema {
Schema {
schema_id: 1,
columns: vec![ColumnDef {
id: 1,
name: "id".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
default_value: None,
embedding_source: None,
}],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
fn percentile(sorted_or_unsorted: &mut [u128], fraction: f64) -> u128 {
sorted_or_unsorted.sort_unstable();
sorted_or_unsorted[((sorted_or_unsorted.len() - 1) as f64 * fraction).round() as usize]
}
fn rss_bytes(field: &str) -> Option<u64> {
std::fs::read_to_string("/proc/self/status")
.ok()?
.lines()
.find_map(|line| line.strip_prefix(field))?
.split_whitespace()
.next()?
.parse::<u64>()
.ok()
.map(|kib| kib * 1024)
}
fn peak_rss_bytes() -> Option<u64> {
rss_bytes("VmHWM:")
}
fn current_rss_bytes() -> Option<u64> {
rss_bytes("VmRSS:")
}
fn rss_bound_bytes(rows: usize) -> u64 {
const PROCESS_BASE: u64 = 512 * 1024 * 1024;
const PER_ROW: u64 = 2 * 1024;
PROCESS_BASE + rows as u64 * PER_ROW
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0 >> 11
}
}
fn seed_rows(db: &Database, rows: usize) -> Vec<RowId> {
const SEED_BATCH: usize = 1_000;
let mut row_ids = Vec::with_capacity(rows);
let mut next = 0i64;
while (next as usize) < rows {
let take = SEED_BATCH.min(rows - next as usize) as i64;
let chunk_start = next;
let (_, ids) = db
.transaction_with_row_ids(|t| {
for pk in chunk_start..chunk_start + take {
t.put("t", vec![(1, Value::Int64(pk))])?;
}
Ok(())
})
.expect("seed commit");
assert_eq!(ids.len(), take as usize);
row_ids.extend_from_slice(&ids);
next += take;
}
row_ids
}
#[test]
fn overlapping_read_write_bounded_rss_and_commit_p99() {
let _serial = SERIAL.lock().unwrap();
let rows = env_usize("MONGRELDB_QUAL_ROWS", 100_000);
let writers = env_usize("MONGRELDB_QUAL_WRITERS", 4);
let readers = env_usize("MONGRELDB_QUAL_READERS", 4);
let commit_p99_max =
Duration::from_millis(env_usize("MONGRELDB_QUAL_COMMIT_P99_MS", 500) as u64);
assert!(rows >= 1_000, "the workload needs a meaningful row count");
assert!(writers > 0 && readers > 0);
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("t", pk_schema()).unwrap();
let seeded = rows / 2;
let overlap = rows - seeded;
let row_ids = Arc::new(seed_rows(&db, seeded));
let rss_after_seed = current_rss_bytes();
const COMMIT_BATCH: usize = 100;
const READS_PER_SNAPSHOT: usize = 16;
let next_pk = Arc::new(AtomicUsize::new(seeded));
let writers_done = Arc::new(AtomicBool::new(false));
let started = Instant::now();
let mut writer_handles = Vec::with_capacity(writers);
for _ in 0..writers {
let db = Arc::clone(&db);
let next_pk = Arc::clone(&next_pk);
writer_handles.push(std::thread::spawn(move || {
let mut commit_us = Vec::new();
loop {
let start = next_pk.fetch_add(COMMIT_BATCH, Ordering::Relaxed);
if start >= seeded + overlap {
break;
}
let stop = (start + COMMIT_BATCH).min(seeded + overlap);
let mut tx = db.begin();
for pk in start..stop {
tx.put("t", vec![(1, Value::Int64(pk as i64))])?;
}
let commit_started = Instant::now();
tx.commit()?;
commit_us.push(commit_started.elapsed().as_micros());
}
Ok::<_, MongrelError>(commit_us)
}));
}
let mut reader_handles = Vec::with_capacity(readers);
for _ in 0..readers {
let db = Arc::clone(&db);
let row_ids = Arc::clone(&row_ids);
let writers_done = Arc::clone(&writers_done);
reader_handles.push(std::thread::spawn(move || {
let mut lcg = Lcg(0x9E3779B97F4A7C15);
let mut reads = 0u64;
while !writers_done.load(Ordering::Acquire) {
let mut tx = db.begin();
for _ in 0..READS_PER_SNAPSHOT {
let i = (lcg.next() % seeded as u64) as usize;
let row = tx.get("t", row_ids[i])?;
let row = row.ok_or_else(|| {
MongrelError::Other(format!("seeded row {i} not visible"))
})?;
assert_eq!(
row.columns[0].1,
Value::Int64(i as i64),
"point read returned the wrong row"
);
reads += 1;
}
tx.rollback();
}
Ok::<_, MongrelError>(reads)
}));
}
let mut commit_us: Vec<u128> = Vec::new();
let mut write_errors = 0u64;
for handle in writer_handles {
match handle.join().expect("writer thread panicked") {
Ok(mut samples) => commit_us.append(&mut samples),
Err(error) => {
eprintln!("writer error: {error}");
write_errors += 1;
}
}
}
writers_done.store(true, Ordering::Release);
let mut total_reads = 0u64;
let mut read_errors = 0u64;
for handle in reader_handles {
match handle.join().expect("reader thread panicked") {
Ok(reads) => total_reads += reads,
Err(error) => {
eprintln!("reader error: {error}");
read_errors += 1;
}
}
}
let elapsed = started.elapsed();
let peak_rss = peak_rss_bytes();
let rss_bound = rss_bound_bytes(rows);
let commit_p50 = percentile(&mut commit_us.clone(), 0.50);
let commit_p99 = percentile(&mut commit_us, 0.99);
let commits = commit_us.len();
println!(
"{}",
serde_json::json!({
"test": "overlapping_read_write",
"profile": if cfg!(debug_assertions) { "debug" } else { "release" },
"rows": rows,
"seeded_rows": seeded,
"overlap_rows": overlap,
"writers": writers,
"readers": readers,
"commits": commits,
"point_reads": total_reads,
"elapsed_ms": elapsed.as_millis(),
"commit_latency": {"p50_us": commit_p50, "p99_us": commit_p99},
"commit_p99_bound_ms": commit_p99_max.as_millis(),
"rss_after_seed_bytes": rss_after_seed,
"peak_rss_bytes": peak_rss,
"peak_rss_bound_bytes": rss_bound,
"write_errors": write_errors,
"read_errors": read_errors,
})
);
assert_eq!(write_errors, 0, "writers must commit without errors");
assert_eq!(
read_errors, 0,
"readers must observe seeded rows without errors"
);
assert!(
total_reads > 0,
"readers must actually overlap the write phase"
);
assert_eq!(
commits,
overlap.div_ceil(COMMIT_BATCH),
"every overlap batch must commit exactly once"
);
assert_eq!(
db.table("t").unwrap().lock().count(),
rows as u64,
"all committed rows must be durable and visible"
);
assert!(
Duration::from_micros(commit_p99 as u64) <= commit_p99_max,
"commit p99 {commit_p99}us exceeds the {}ms bound",
commit_p99_max.as_millis()
);
if let Some(peak) = peak_rss {
assert!(
peak <= rss_bound,
"peak RSS {peak} bytes exceeds the bounded-RSS gate bound of {rss_bound} bytes"
);
} else {
eprintln!("peak RSS unavailable on this platform; RSS bound not asserted");
}
}
#[test]
fn warm_point_query_p95_baseline() {
let _serial = SERIAL.lock().unwrap();
const ROWS: usize = 100_000;
const QUERIES: usize = 10_000;
const P95_SANITY_MAX: Duration = Duration::from_millis(20);
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
let row_ids = seed_rows(&db, ROWS);
{
let mut tx = db.begin();
for &row_id in &row_ids {
std::hint::black_box(tx.get("t", row_id).unwrap());
}
tx.rollback();
}
let mut lcg = Lcg(0x2545F4914F6CDD1D);
let mut samples_ns = Vec::with_capacity(QUERIES);
for _ in 0..QUERIES {
let i = (lcg.next() % ROWS as u64) as usize;
let query_started = Instant::now();
let mut tx = db.begin();
let row = tx.get("t", row_ids[i]).unwrap();
tx.rollback();
samples_ns.push(query_started.elapsed().as_nanos());
assert_eq!(
row.map(|row| row.columns[0].1.clone()),
Some(Value::Int64(i as i64)),
"point query returned the wrong row"
);
}
let p50_ns = percentile(&mut samples_ns.clone(), 0.50);
let p95_ns = percentile(&mut samples_ns.clone(), 0.95);
let p99_ns = percentile(&mut samples_ns, 0.99);
println!(
"{}",
serde_json::json!({
"test": "warm_point_query",
"profile": if cfg!(debug_assertions) { "debug" } else { "release" },
"rows": ROWS,
"queries": QUERIES,
"point_query_latency": {"p50_us": p50_ns as f64 / 1e3, "p95_us": p95_ns as f64 / 1e3, "p99_us": p99_ns as f64 / 1e3},
"peak_rss_bytes": peak_rss_bytes(),
})
);
assert!(
Duration::from_nanos(p95_ns as u64) <= P95_SANITY_MAX,
"point-query p95 {p95_ns}ns exceeds the {}ms sanity bound",
P95_SANITY_MAX.as_millis()
);
}