use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use spg_wire::{Op, build_query, encode, parse_error_response};
use crate::common;
const READ_TIMEOUT: Duration = Duration::from_secs(120);
const FREEZE_QUIESCENCE_TIMEOUT: Duration = Duration::from_secs(300);
fn unique_tmpdir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let p = std::env::temp_dir().join(format!("spg-e2e-1b-{label}-{nanos}"));
std::fs::create_dir_all(&p).unwrap();
p
}
fn send_query(s: &mut TcpStream, sql: &str) {
let q = build_query(sql);
let mut out = Vec::new();
encode(&q, &mut out).unwrap();
s.write_all(&out).unwrap();
}
fn drain_until_cc(s: &mut TcpStream, sql: &str) {
loop {
let mut header = [0u8; spg_wire::FRAME_HEADER_LEN];
s.read_exact(&mut header).unwrap();
let len = u32::from_le_bytes([header[0], header[1], header[2], header[3]]) as usize;
let op = Op::from_byte(header[4]).unwrap();
let mut body = vec![0u8; len];
if len > 0 {
s.read_exact(&mut body).unwrap();
}
match op {
Op::CommandComplete => return,
Op::ErrorResponse | Op::Error => {
let f = spg_wire::Frame { op, payload: body };
panic!(
"SQL failed: {sql:?} → {}",
parse_error_response(&f).unwrap_or("<undecodable>")
);
}
_ => continue,
}
}
}
fn exec_ok(s: &mut TcpStream, sql: &str) {
send_query(s, sql);
drain_until_cc(s, sql);
}
fn count_table(s: &mut TcpStream, sql: &str) -> usize {
send_query(s, sql);
let mut total = 0usize;
loop {
let mut header = [0u8; spg_wire::FRAME_HEADER_LEN];
s.read_exact(&mut header).unwrap();
let len = u32::from_le_bytes([header[0], header[1], header[2], header[3]]) as usize;
let op = Op::from_byte(header[4]).unwrap();
let mut body = vec![0u8; len];
if len > 0 {
s.read_exact(&mut body).unwrap();
}
match op {
Op::DataRow => total += 1,
Op::DataRowBatch => {
let f = spg_wire::Frame { op, payload: body };
total += spg_wire::parse_data_row_batch(&f)
.map(|r| r.len())
.unwrap_or(0);
}
Op::CommandComplete => return total,
Op::ErrorResponse | Op::Error => {
let f = spg_wire::Frame { op, payload: body };
panic!(
"select failed: {sql} → {}",
parse_error_response(&f).unwrap_or("<undecodable>")
);
}
_ => continue,
}
}
}
fn pk_resolves(s: &mut TcpStream, id: i64) -> bool {
count_table(s, &format!("SELECT id FROM t WHERE id = {id}")) > 0
}
fn wait_for_freezer_quiescence(s: &mut TcpStream) {
let deadline = Instant::now() + FREEZE_QUIESCENCE_TIMEOUT;
let mut last = count_table(s, "SELECT * FROM spg_stat_segment");
loop {
std::thread::sleep(Duration::from_millis(400));
let now = count_table(s, "SELECT * FROM spg_stat_segment");
if now == last {
return;
}
last = now;
if Instant::now() > deadline {
panic!("freezer never quiesced (last segment count = {last})");
}
}
}
fn graceful_stop(child: &mut std::process::Child) {
#[cfg(unix)]
{
let pid = child.id() as libc::pid_t;
let _ = unsafe { libc::kill(pid, libc::SIGTERM) };
}
let _ = child.wait();
}
fn run_pipeline_and_measure_cold_start(rows: i64, db: &Path, wal: &Path) -> Duration {
{
let (mut raw, addrs) = common::ServerBuilder::new()
.arg_path(db)
.arg("-")
.arg_path(wal)
.env("SPG_HOT_TIER_BYTES", "4096")
.env("SPG_FREEZER_TICK_MS", "20")
.env("SPG_FREEZER_BATCH_ROWS", "256")
.env("SPG_FREEZER_WORKERS", "4")
.spawn();
{
let mut s = common::connect_to(&addrs.native);
s.set_read_timeout(Some(READ_TIMEOUT)).unwrap();
exec_ok(
&mut s,
"CREATE TABLE t (id INT NOT NULL, name TEXT NOT NULL)",
);
exec_ok(&mut s, "CREATE INDEX by_id ON t (id)");
const BATCH: i64 = 256;
let mut i: i64 = 0;
while i < rows {
let upper = (i + BATCH).min(rows);
let mut sql = String::with_capacity(64 * BATCH as usize);
sql.push_str("INSERT INTO t VALUES ");
let mut first = true;
for k in i..upper {
if !first {
sql.push(',');
}
first = false;
sql.push_str(&format!("({k}, 'r-{k}')"));
}
exec_ok(&mut s, &sql);
i = upper;
}
wait_for_freezer_quiescence(&mut s);
exec_ok(&mut s, "COMPACT COLD SEGMENTS");
exec_ok(&mut s, "CHECKPOINT");
}
graceful_stop(&mut raw);
}
let t0 = Instant::now();
let (raw, addrs) = common::ServerBuilder::new()
.arg_path(db)
.arg("-")
.arg_path(wal)
.env("SPG_FREEZER_DISABLE", "1")
.env("SPG_PREFETCH_WORKERS", "4")
.spawn();
let _guard = common::ChildGuard(raw);
let mut s = common::connect_to(&addrs.native);
s.set_read_timeout(Some(READ_TIMEOUT)).unwrap();
let probe_ids = [0i64, rows / 2, rows - 1];
let deadline = Instant::now() + READ_TIMEOUT;
loop {
if probe_ids.iter().all(|&id| pk_resolves(&mut s, id)) {
return t0.elapsed();
}
if Instant::now() > deadline {
panic!("cold-start: PK seek never resolved within {READ_TIMEOUT:?}");
}
std::thread::sleep(Duration::from_millis(50));
}
}
#[test]
fn pipeline_sanity_50k_rows() {
let _lock = crate::perf_lock();
let dir = unique_tmpdir("sanity");
let db = dir.join("spg.db");
let wal = dir.join("wal.log");
let cold_start = run_pipeline_and_measure_cold_start(50_000, &db, &wal);
eprintln!("perf_1b_rows pipeline_sanity_50k_rows: cold_start={cold_start:?}");
assert!(
cold_start < Duration::from_secs(60),
"50k-row sanity cold-start took {cold_start:?} (sanity ceiling 60s)"
);
}
#[test]
#[ignore]
fn cold_start_under_120s() {
if std::env::var_os("SPG_SOAK_TESTS").is_none() {
eprintln!("skipping cold_start_under_120s: heavy soak — set SPG_SOAK_TESTS=1 to run it");
return;
}
let _lock = crate::perf_lock();
let rows: i64 = std::env::var("SPG_PERF_1B_ROW_BUDGET")
.ok()
.and_then(|s| s.parse().ok())
.filter(|n: &i64| *n > 0)
.unwrap_or(1_000_000);
let dir = unique_tmpdir("perf");
let db = dir.join("spg.db");
let wal = dir.join("wal.log");
let cold_start = run_pipeline_and_measure_cold_start(rows, &db, &wal);
println!("perf_1b_rows cold_start_under_120s (rows={rows}): cold_start={cold_start:?}");
assert!(
cold_start < Duration::from_secs(120),
"{rows}-row cold-start took {cold_start:?} (gate 120s)"
);
}