#![cfg(feature = "std")]
use graphitesql::{Connection, Error, Value};
use std::time::Duration;
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn nproc() -> usize {
env_usize("GSQL_MP_WRITERS", 5)
}
fn txns() -> usize {
env_usize("GSQL_MP_TXNS", 250)
}
fn pool() -> usize {
env_usize("GSQL_MP_POOL", 80)
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
}
const SCHEMA: &str = "\
CREATE TABLE IF NOT EXISTS frag(uid TEXT PRIMARY KEY, job TEXT, n INTEGER, at TEXT);\
CREATE INDEX IF NOT EXISTS frag_job ON frag(job);\
CREATE TABLE IF NOT EXISTS hit(job TEXT, uid TEXT, k INTEGER, v TEXT);\
CREATE INDEX IF NOT EXISTS hit_job ON hit(job);\
CREATE INDEX IF NOT EXISTS hit_uid ON hit(uid);";
fn writer_child() {
let db = std::env::var("GSQL_MP_DB").expect("GSQL_MP_DB");
let id: u64 = std::env::var("GSQL_MP_ID")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let mut rng = Rng(0x9E3779B97F4A7C15 ^ (id.wrapping_mul(0x1000)));
let mut c = loop {
match Connection::open(&db) {
Ok(c) => break c,
Err(Error::Busy) => std::thread::sleep(Duration::from_millis(1 + rng.next() % 8)),
Err(e) => panic!("child open: {e:?}"),
}
};
let _ = c.execute("PRAGMA busy_timeout=4000");
let run = |c: &mut Connection, sql: &str, rng: &mut Rng| loop {
match c.execute(sql) {
Ok(_) => break,
Err(Error::Busy) => std::thread::sleep(Duration::from_millis(1 + rng.next() % 8)),
Err(e) => panic!("child stmt failed: {e:?} on {sql}"),
}
};
for i in 0..txns() {
let uid = format!("uid-{:04}", (id as usize * 13 + i) % pool());
let job = format!("job-{}", i % 20);
run(&mut c, "BEGIN", &mut rng);
run(
&mut c,
&format!(
"INSERT INTO frag(uid,job,n,at) VALUES('{uid}','{job}',{i},datetime('now')) \
ON CONFLICT(uid) DO UPDATE SET job='{job}',n={i},at=datetime('now')"
),
&mut rng,
);
run(
&mut c,
&format!("DELETE FROM hit WHERE uid='{uid}'"),
&mut rng,
);
for h in 0..(i % 6) {
run(
&mut c,
&format!(
"INSERT INTO hit(job,uid,k,v) VALUES('{job}','{uid}',{},'val-{i}-{h}-padpadpadpad')",
i * 100 + h
),
&mut rng,
);
}
run(&mut c, "COMMIT", &mut rng);
}
println!("done {id}");
}
#[test]
#[ignore = "spawned as a writer child; no-op otherwise"]
fn multiprocess_writer_child() {
if std::env::var("GSQL_MP_CHILD").is_ok() {
writer_child();
}
}
#[test]
#[ignore = "heavy: spawns several writer processes hammering one db (fsync-per-txn); \
run via the stress workflow or `--ignored`. Env: GSQL_MP_WRITERS/TXNS/POOL."]
fn concurrent_process_writes_keep_the_db_consistent() {
let dir = std::env::temp_dir();
let db = dir
.join(format!("gsql-mp-{}.db", std::process::id()))
.to_string_lossy()
.into_owned();
for suffix in ["", "-journal", "-wal", "-shm"] {
let _ = std::fs::remove_file(format!("{db}{suffix}"));
}
{
let mut c = Connection::create(&db).expect("create db");
c.execute_batch(SCHEMA).expect("schema");
}
let exe = std::env::current_exe().expect("current_exe");
let mut kids: Vec<std::process::Child> = (0..nproc())
.map(|id| {
std::process::Command::new(&exe)
.args([
"--exact",
"multiprocess_writer_child",
"--ignored",
"--nocapture",
"--test-threads=1",
])
.env("GSQL_MP_CHILD", "1")
.env("GSQL_MP_DB", &db)
.env("GSQL_MP_ID", id.to_string())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn writer child")
})
.collect();
let mut failures = Vec::new();
for (id, k) in kids.iter_mut().enumerate() {
let out = k.wait_with_output_ref();
if !out.0 {
failures.push(format!("child {id} exited non-zero: {}", out.1));
}
}
for suffix in ["-journal", "-wal", "-shm"] {
let _ = std::fs::remove_file(format!("{db}{suffix}"));
}
let integrity = {
let c = Connection::open(&db).expect("reopen for integrity");
c.query("PRAGMA integrity_check")
.expect("integrity_check")
.rows
};
let _ = std::fs::remove_file(&db);
assert!(
failures.is_empty(),
"writer child(ren) failed (a Busy/corruption during the run): {failures:?}"
);
assert_eq!(
integrity,
vec![vec![Value::Text("ok".into())]],
"integrity_check found corruption after concurrent multi-process writes: {integrity:?}"
);
}
trait WaitOutput {
fn wait_with_output_ref(&mut self) -> (bool, String);
}
impl WaitOutput for std::process::Child {
fn wait_with_output_ref(&mut self) -> (bool, String) {
use std::io::Read;
let mut err = String::new();
if let Some(mut s) = self.stderr.take() {
let _ = s.read_to_string(&mut err);
}
let ok = self.wait().map(|s| s.success()).unwrap_or(false);
(ok, err)
}
}