#![cfg(unix)]
use std::io::Write as _;
use std::path::PathBuf;
use std::time::Duration;
use powdb_storage::catalog::Catalog;
use powdb_storage::types::{ColumnDef, Row, Schema, TypeId, Value};
const CHILD_ENV: &str = "POWDB_REPLICA_KILL9_CHILD";
fn tmp(tag: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!(
"powdb_replica_kill9_{tag}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = std::fs::remove_dir_all(&p);
p
}
fn schema_users() -> Schema {
Schema {
table_name: "User".into(),
columns: vec![
ColumnDef {
name: "id".into(),
type_id: TypeId::Int,
required: true,
position: 0,
},
ColumnDef {
name: "email".into(),
type_id: TypeId::Str,
required: false,
position: 1,
},
],
}
}
fn user_row(id: i64) -> Row {
vec![Value::Int(id), Value::Str(format!("user{id}@example.com"))]
}
fn rows(cat: &Catalog) -> Vec<i64> {
let mut ids: Vec<i64> = cat
.scan("User")
.unwrap()
.map(|item| match &item.unwrap().1[0] {
Value::Int(id) => *id,
other => panic!("expected int id, got {other:?}"),
})
.collect();
ids.sort_unstable();
ids
}
#[test]
fn helper_child_applies_in_small_chunks() {
let Ok(spec) = std::env::var(CHILD_ENV) else {
return;
};
let parts: Vec<&str> = spec.split('\x1f').collect();
let (replica, retained, snapshot_lsn, remote_lsn): (PathBuf, PathBuf, u64, u64) = (
parts[0].into(),
parts[1].into(),
parts[2].parse().unwrap(),
parts[3].parse().unwrap(),
);
let mut cat = powdb_sync::open_preserving_retained_segments(&replica).unwrap();
let identity = powdb_sync::read_identity(&replica)
.unwrap()
.segment_identity();
std::fs::write(replica.join("child-started"), b"1").unwrap();
let mut boundary = snapshot_lsn;
while boundary < remote_lsn {
let units =
powdb_sync::segment::read_units_through(&retained, identity, boundary, remote_lsn, 2)
.unwrap();
if units.is_empty() {
break;
}
let summary =
powdb_sync::apply_retained_units_chunk(&mut cat, identity, boundary, &units).unwrap();
boundary = summary.through_lsn;
std::thread::sleep(Duration::from_millis(1));
}
}
#[test]
fn sigkill_mid_chunked_apply_reopens_and_resumes_to_convergence() {
let primary = tmp("primary");
let mut primary_cat = Catalog::create(&primary).unwrap();
primary_cat.create_table(schema_users()).unwrap();
for id in 0..8 {
primary_cat.insert("User", &user_row(id)).unwrap();
}
primary_cat.commit_autocommit().unwrap();
primary_cat.sync_wal().unwrap();
primary_cat.create_index_unique("User", "id", true).unwrap();
let identity = powdb_sync::open_or_create_identity(&primary).unwrap();
let backup = tmp("backup");
powdb_backup::full_backup(&mut primary_cat, &backup).unwrap();
for id in 8..408 {
primary_cat.insert("User", &user_row(id)).unwrap();
primary_cat.commit_autocommit().unwrap();
primary_cat.sync_wal().unwrap();
}
let primary_rows = rows(&primary_cat);
let replica = tmp("replica");
let bootstrap = powdb_backup::bootstrap_replica_from_full_backup(
&mut primary_cat,
&backup,
&replica,
"replica-kill9",
)
.unwrap();
let retained = powdb_sync::retained_segments_dir(&primary);
let spec = format!(
"{}\x1f{}\x1f{}\x1f{}",
replica.display(),
retained.display(),
bootstrap.snapshot_lsn,
bootstrap.remote_lsn
);
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"helper_child_applies_in_small_chunks",
"--test-threads=1",
])
.env(CHILD_ENV, &spec)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.unwrap();
let started = replica.join("child-started");
let deadline = std::time::Instant::now() + Duration::from_secs(30);
while !started.exists() {
assert!(
std::time::Instant::now() < deadline,
"child never started applying"
);
std::thread::sleep(Duration::from_millis(5));
}
std::thread::sleep(Duration::from_millis(100));
child.kill().unwrap(); child.wait().unwrap();
let reopened = powdb_sync::open_preserving_retained_segments(&replica).unwrap();
let after_kill = rows(&reopened);
assert!(
after_kill.len() >= 8,
"replica lost bootstrapped rows: {}",
after_kill.len()
);
assert!(
after_kill.len() < primary_rows.len(),
"child finished before the kill ({} rows); the test is vacuous — \
widen the tail or shorten the delay",
after_kill.len()
);
let mut cat = reopened;
let sid = identity.segment_identity();
let resume_from = cat.max_lsn();
let applied = powdb_sync::apply_retained_tail(
&mut cat,
&retained,
sid,
resume_from,
bootstrap.remote_lsn,
)
.unwrap();
assert_eq!(applied.through_lsn, bootstrap.remote_lsn);
assert_eq!(
rows(&cat),
primary_rows,
"replica must converge after resume"
);
assert_eq!(
cat.index_lookup("User", "id", &Value::Int(300))
.unwrap()
.unwrap()[0],
Value::Int(300),
"indexes must be consistent after the resumed apply"
);
drop(cat);
let final_cat = powdb_sync::open_preserving_retained_segments(&replica).unwrap();
assert_eq!(rows(&final_cat), primary_rows);
let _ = writeln!(
std::io::stdout(),
"killed at {} rows, converged at {}",
after_kill.len(),
primary_rows.len()
);
for dir in [&primary, &backup, &replica] {
let _ = std::fs::remove_dir_all(dir);
}
}