#![cfg(target_os = "linux")]
use seerdb::{DBOptions, DB};
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::process::Command;
fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
const LOOP_SIZE_MB: usize = 64;
fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(cmd)
.args(args)
.output()
.map_err(|e| format!("Failed to run {}: {}", cmd, e))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(format!(
"{} failed: {}",
cmd,
String::from_utf8_lossy(&output.stderr)
))
}
}
fn get_sectors(loop_dev: &str) -> Result<u64, String> {
let output = run_cmd("blockdev", &["--getsz", loop_dev])?;
output
.trim()
.parse()
.map_err(|e| format!("Failed to parse sector count: {}", e))
}
fn path_str(path: &Path) -> Result<&str, String> {
path.to_str().ok_or_else(|| "Invalid path".to_string())
}
struct DmFlakeyHarness {
backing_file: PathBuf,
loop_device: Option<String>,
dm_name: String,
mount_point: PathBuf,
in_crash_mode: bool,
}
impl DmFlakeyHarness {
fn new(test_name: &str) -> Result<Self, String> {
if !is_root() {
return Err("Root privileges required for dm-flakey tests".to_string());
}
let base_dir = PathBuf::from("/tmp/seerdb_power_test");
fs::create_dir_all(&base_dir).map_err(|e| format!("Failed to create test dir: {}", e))?;
let backing_file = base_dir.join(format!("{}.img", test_name));
let mount_point = base_dir.join(format!("{}_mount", test_name));
let dm_name = format!("seerdb_test_{}", test_name);
Ok(Self {
backing_file,
loop_device: None,
dm_name,
mount_point,
in_crash_mode: false,
})
}
fn setup(&mut self) -> Result<(), String> {
let file = File::create(&self.backing_file)
.map_err(|e| format!("Failed to create backing file: {}", e))?;
file.set_len((LOOP_SIZE_MB * 1024 * 1024) as u64)
.map_err(|e| format!("Failed to set file size: {}", e))?;
let output = run_cmd("losetup", &["-f", "--show", path_str(&self.backing_file)?])?;
let loop_dev = output.trim().to_string();
self.loop_device = Some(loop_dev.clone());
let sectors = get_sectors(&loop_dev)?;
let table = format!("0 {} flakey {} 0 3600 0", sectors, loop_dev);
run_cmd("dmsetup", &["create", &self.dm_name, "--table", &table])?;
let dm_path = format!("/dev/mapper/{}", self.dm_name);
run_cmd("mkfs.ext4", &["-q", &dm_path])?;
fs::create_dir_all(&self.mount_point)
.map_err(|e| format!("Failed to create mount point: {}", e))?;
run_cmd("mount", &[&dm_path, path_str(&self.mount_point)?])?;
Ok(())
}
fn data_path(&self) -> PathBuf {
self.mount_point.join("seerdb_data")
}
fn simulate_crash(&mut self) -> Result<(), String> {
if self.in_crash_mode {
return Ok(());
}
let _ = run_cmd("umount", &["-l", path_str(&self.mount_point)?]);
let loop_dev = self.loop_device.as_ref().ok_or("No loop device")?;
let sectors = get_sectors(loop_dev)?;
let table = format!("0 {} flakey {} 0 0 3600 1 drop_writes", sectors, loop_dev);
run_cmd("dmsetup", &["reload", &self.dm_name, "--table", &table])?;
run_cmd("dmsetup", &["suspend", &self.dm_name])?;
run_cmd("dmsetup", &["resume", &self.dm_name])?;
self.in_crash_mode = true;
Ok(())
}
fn recover(&mut self) -> Result<(), String> {
if !self.in_crash_mode {
return Ok(());
}
let loop_dev = self.loop_device.as_ref().ok_or("No loop device")?;
let sectors = get_sectors(loop_dev)?;
let table = format!("0 {} flakey {} 0 3600 0", sectors, loop_dev);
run_cmd("dmsetup", &["reload", &self.dm_name, "--table", &table])?;
run_cmd("dmsetup", &["suspend", &self.dm_name])?;
run_cmd("dmsetup", &["resume", &self.dm_name])?;
let dm_path = format!("/dev/mapper/{}", self.dm_name);
run_cmd("mount", &[&dm_path, path_str(&self.mount_point)?])?;
self.in_crash_mode = false;
Ok(())
}
fn cleanup(&mut self) {
if let Ok(mount_path) = path_str(&self.mount_point) {
let _ = run_cmd("umount", &["-l", mount_path]);
}
let _ = run_cmd("dmsetup", &["remove", &self.dm_name]);
if let Some(ref loop_dev) = self.loop_device {
let _ = run_cmd("losetup", &["-d", loop_dev]);
}
let _ = fs::remove_file(&self.backing_file);
let _ = fs::remove_dir(&self.mount_point);
}
}
impl Drop for DmFlakeyHarness {
fn drop(&mut self) {
self.cleanup();
}
}
#[test]
#[ignore] fn test_crash_during_put() {
if !is_root() {
eprintln!("Skipping: requires root privileges");
return;
}
let mut harness = DmFlakeyHarness::new("crash_put").expect("Failed to create harness");
harness.setup().expect("Failed to setup harness");
{
let db = DB::open(&harness.data_path()).expect("Failed to open DB");
for i in 0..100 {
db.put(format!("committed_{:04}", i).as_bytes(), b"value")
.expect("Put failed");
}
db.flush().expect("Flush failed");
for i in 0..50 {
db.put(format!("uncommitted_{:04}", i).as_bytes(), b"value")
.expect("Put failed");
}
harness.simulate_crash().expect("Failed to simulate crash");
}
harness.recover().expect("Failed to recover");
{
let db = DB::open(&harness.data_path()).expect("Failed to reopen DB after crash");
for i in 0..100 {
let key = format!("committed_{:04}", i);
let value = db.get(key.as_bytes()).expect("Get failed");
assert!(value.is_some(), "Committed key {} missing after crash", key);
}
println!("✓ Crash during put: recovery successful");
}
}
#[test]
#[ignore]
fn test_crash_during_flush() {
if !is_root() {
eprintln!("Skipping: requires root privileges");
return;
}
let mut harness = DmFlakeyHarness::new("crash_flush").expect("Failed to create harness");
harness.setup().expect("Failed to setup harness");
{
let db = DB::open(&harness.data_path()).expect("Failed to open DB");
let value = vec![b'v'; 100];
for i in 0..200 {
db.put(format!("key_{:04}", i).as_bytes(), &value)
.expect("Put failed");
}
db.flush().expect("Flush failed");
harness.simulate_crash().expect("Failed to simulate crash");
}
harness.recover().expect("Failed to recover");
{
let db = DB::open(&harness.data_path()).expect("Failed to reopen DB after crash");
let mut recovered = 0;
for i in 0..200 {
let key = format!("key_{:04}", i);
if db.get(key.as_bytes()).expect("Get failed").is_some() {
recovered += 1;
}
}
println!("✓ Crash during flush: recovered {}/200 keys", recovered);
assert!(
recovered >= 190,
"Too much data loss: only {}/200 recovered",
recovered
);
}
}
#[test]
#[ignore]
fn test_repeated_crash_recovery() {
if !is_root() {
eprintln!("Skipping: requires root privileges");
return;
}
let mut harness = DmFlakeyHarness::new("repeated_crash").expect("Failed to create harness");
harness.setup().expect("Failed to setup harness");
for cycle in 0..5 {
{
let db = DB::open(&harness.data_path()).expect("Failed to open DB");
for i in 0..50 {
let key = format!("cycle{}_{:04}", cycle, i);
db.put(key.as_bytes(), b"value").expect("Put failed");
}
db.flush().expect("Flush failed");
}
harness.simulate_crash().expect("Failed to simulate crash");
harness.recover().expect("Failed to recover");
}
{
let db = DB::open(&harness.data_path()).expect("Failed to open DB");
for cycle in 0..5 {
for i in 0..50 {
let key = format!("cycle{}_{:04}", cycle, i);
let value = db.get(key.as_bytes()).expect("Get failed");
assert!(
value.is_some(),
"Key {} missing after repeated crashes",
key
);
}
}
println!("✓ Repeated crash recovery: all 250 keys recovered");
}
}
#[test]
#[ignore]
fn test_crash_during_compaction() {
if !is_root() {
eprintln!("Skipping: requires root privileges");
return;
}
let mut harness = DmFlakeyHarness::new("crash_compact").expect("Failed to create harness");
harness.setup().expect("Failed to setup harness");
{
let db = DBOptions::default()
.memtable_capacity(1024 * 64) .open(&harness.data_path())
.expect("Failed to open DB");
let value = vec![b'v'; 100];
for batch in 0..10 {
for i in 0..100 {
let key = format!("batch{}_{:04}", batch, i);
db.put(key.as_bytes(), &value).expect("Put failed");
}
db.flush().expect("Flush failed");
}
harness.simulate_crash().expect("Failed to simulate crash");
}
harness.recover().expect("Failed to recover");
{
let db = DB::open(&harness.data_path()).expect("Failed to reopen DB after crash");
let mut found = 0;
for batch in 0..10 {
for i in 0..100 {
let key = format!("batch{}_{:04}", batch, i);
if db.get(key.as_bytes()).expect("Get failed").is_some() {
found += 1;
}
}
}
println!("✓ Crash during compaction: recovered {}/1000 keys", found);
assert!(
found >= 950,
"Too much data loss: only {}/1000 recovered",
found
);
}
}
#[allow(dead_code)]
fn verify_db_state(data_path: &Path) -> Result<(usize, usize), String> {
let _db = DB::open(data_path).map_err(|e| format!("Failed to open DB: {}", e))?;
let sst_count = fs::read_dir(data_path)
.map_err(|e| format!("Failed to read dir: {}", e))?
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".sst"))
.count();
let wal_count = fs::read_dir(data_path)
.map_err(|e| format!("Failed to read dir: {}", e))?
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".wal"))
.count();
Ok((sst_count, wal_count))
}