use std::io::{Read, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
fn req(parts: &[&[u8]]) -> Vec<u8> {
let mut v = format!("*{}\r\n", parts.len()).into_bytes();
for p in parts {
v.extend_from_slice(format!("${}\r\n", p.len()).as_bytes());
v.extend_from_slice(p);
v.extend_from_slice(b"\r\n");
}
v
}
fn read_reply(s: &mut std::net::TcpStream, expected: &[u8]) {
let mut buf = vec![0u8; expected.len()];
s.read_exact(&mut buf).unwrap();
assert_eq!(
&buf,
expected,
"expected {:?}",
String::from_utf8_lossy(expected)
);
}
fn with_runtime(port: u16, dir: &std::path::Path, nshards: usize, body: impl FnOnce(u16)) {
let stop = Arc::new(AtomicBool::new(false));
let stop_t = stop.clone();
let dir = dir.to_path_buf();
let handle = std::thread::spawn(move || {
let rt = kevy_rt::Runtime::new([127, 0, 0, 1], port, nshards, kevy::KevyCommands)
.with_data_dir(dir);
rt.run(stop_t).unwrap();
});
let mut up = false;
for _ in 0..200 {
if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() {
up = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(up, "runtime did not start");
body(port);
stop.store(true, Ordering::Relaxed);
let _ = handle.join();
}
#[test]
fn data_survives_restart_via_save() {
let dir = std::env::temp_dir().join(format!(
"kevy-persist-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let nshards = 4;
let port = free_port();
with_runtime(port, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..100u32 {
c.write_all(&req(&[
b"SET",
format!("k{i}").as_bytes(),
format!("v{i}").as_bytes(),
]))
.unwrap();
read_reply(&mut c, b"+OK\r\n");
}
c.write_all(&req(&[b"SAVE"])).unwrap();
read_reply(&mut c, b"+OK\r\n");
});
let dumps = (0..nshards)
.filter(|i| dir.join(format!("dump-{i}.rdb")).exists())
.count();
assert!(dumps > 0, "no snapshot files were written");
let port2 = free_port();
with_runtime(port2, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..100u32 {
c.write_all(&req(&[b"GET", format!("k{i}").as_bytes()]))
.unwrap();
let want = format!("v{i}");
read_reply(
&mut c,
format!("${}\r\n{}\r\n", want.len(), want).as_bytes(),
);
}
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn bgrewriteaof_shrinks_log_and_preserves_data() {
let dir = std::env::temp_dir().join(format!(
"kevy-bgrewrite-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let nshards = 4;
let port = free_port();
let mut post_size: u64 = 0;
with_runtime(port, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..40u32 {
for rev in 0..50u32 {
c.write_all(&req(&[
b"SET",
format!("k{i}").as_bytes(),
format!("v{i}-r{rev}").as_bytes(),
]))
.unwrap();
read_reply(&mut c, b"+OK\r\n");
}
}
c.write_all(&req(&[b"BGREWRITEAOF"])).unwrap();
read_reply(&mut c, b"+OK\r\n");
post_size = (0..nshards)
.map(|s| {
std::fs::metadata(dir.join(format!("aof-{s}.aof")))
.map(|m| m.len())
.unwrap_or(0)
})
.sum();
assert!(
post_size < 10_000,
"rewritten AOF unexpectedly large: {post_size} bytes"
);
assert!(post_size > 0, "rewritten AOF should not be empty");
});
let port2 = free_port();
with_runtime(port2, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..40u32 {
c.write_all(&req(&[b"GET", format!("k{i}").as_bytes()]))
.unwrap();
let want = format!("v{i}-r49");
read_reply(
&mut c,
format!("${}\r\n{}\r\n", want.len(), want).as_bytes(),
);
}
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn aof_truncated_tail_is_tolerated_on_restart() {
let dir = std::env::temp_dir().join(format!(
"kevy-truncated-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let nshards = 1; let port = free_port();
with_runtime(port, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..20u32 {
c.write_all(&req(&[
b"SET",
format!("survivor{i}").as_bytes(),
b"v".to_vec().as_slice(),
]))
.unwrap();
read_reply(&mut c, b"+OK\r\n");
}
c.write_all(&req(&[b"BGREWRITEAOF"])).unwrap();
read_reply(&mut c, b"+OK\r\n");
});
let aof_path = dir.join("aof-0.aof");
let mut bytes = std::fs::read(&aof_path).unwrap();
let prefix_len = bytes.len();
bytes.extend_from_slice(b"*3\r\n$3\r\nSET\r\n$5\r\nfoo");
std::fs::write(&aof_path, &bytes).unwrap();
let corrupted_len = bytes.len();
assert!(corrupted_len > prefix_len, "test should have appended garbage");
let port2 = free_port();
with_runtime(port2, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..20u32 {
c.write_all(&req(&[b"GET", format!("survivor{i}").as_bytes()]))
.unwrap();
read_reply(&mut c, b"$1\r\nv\r\n");
}
c.write_all(&req(&[b"GET", b"foo"])).unwrap();
read_reply(&mut c, b"$-1\r\n");
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn data_survives_restart_via_aof_without_save() {
let dir = std::env::temp_dir().join(format!(
"kevy-aof-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let nshards = 4;
let port = free_port();
with_runtime(port, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..100u32 {
c.write_all(&req(&[
b"SET",
format!("a{i}").as_bytes(),
format!("b{i}").as_bytes(),
]))
.unwrap();
read_reply(&mut c, b"+OK\r\n");
}
for i in 1..=5u32 {
c.write_all(&req(&[b"INCR", b"counter"])).unwrap();
let want = format!(":{i}\r\n");
read_reply(&mut c, want.as_bytes());
}
});
assert!(!dir.join("dump-0.rdb").exists());
let port2 = free_port();
with_runtime(port2, &dir, nshards, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
for i in 0..100u32 {
c.write_all(&req(&[b"GET", format!("a{i}").as_bytes()]))
.unwrap();
let want = format!("b{i}");
read_reply(
&mut c,
format!("${}\r\n{}\r\n", want.len(), want).as_bytes(),
);
}
c.write_all(&req(&[b"GET", b"counter"])).unwrap();
read_reply(&mut c, b"$1\r\n5\r\n");
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn restart_tolerates_corrupt_snapshot() {
let dir = std::env::temp_dir().join(format!(
"kevy-corrupt-snap-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("dump-0.rdb"), b"NOT A REAL KEVY SNAPSHOT").unwrap();
let port = free_port();
with_runtime(port, &dir, 1, |p| {
let mut c = std::net::TcpStream::connect(("127.0.0.1", p)).unwrap();
c.write_all(&req(&[b"PING"])).unwrap();
read_reply(&mut c, b"+PONG\r\n");
c.write_all(&req(&[b"SET", b"after-corrupt", b"ok"])).unwrap();
read_reply(&mut c, b"+OK\r\n");
c.write_all(&req(&[b"GET", b"after-corrupt"])).unwrap();
read_reply(&mut c, b"$2\r\nok\r\n");
});
let _ = std::fs::remove_dir_all(&dir);
}